#include "version.h" #include "digest/digest.h" #include "l2su.h" #include "macros.h" #include "endian.h" #include "command.h" #include #include #include #include #include #include #include #include const char *const l2_version__messages[] = { "Success", "Malformed version manifest", "Version manifest JSON error", "Allocation failed", "OS error", "Error downloading version manifest", "Version not found", "Max recursion depth exceeded", NULL }; #define L2_VERSION_MANIFEST_ETAG_PATH "/.l2_vermanifest_etag" #define L2_VERSION_MANIFEST_PATH "/version_manifest_v2.json" #define L2_VERSION_MANIFEST_EXP_TIME ((time_t)120) unsigned l2_version__load_manifest(json_t **out_json); unsigned l2_version__load_all_from_json(json_t *json); unsigned l2_version__add_remote(json_t *obj); unsigned l2_version_load_remote(void) { json_t *vers = NULL; unsigned r = l2_version__load_manifest(&vers); if (r != VERSION_SUCCESS) goto done; r = l2_version__load_all_from_json(vers); done: json_decref(vers); return r; } void l2_version__load_local(const char *id, size_t expectsz, l2_sha1_digest_t *digest, json_t **ojson); bool l2_version__download_remote(struct l2_version_remote *remote, json_t **ojson); unsigned l2_version__load_or_download(const char *name, json_t **ojs) { json_t *js = NULL; struct l2_version_remote *remote = NULL; for (struct l2_version_remote *cur = l2_state.ver_remote_head; cur; cur = cur->next) { if (!strcmp(cur->id, name)) { remote = cur; break; } } if (remote) { l2_version__load_local(remote->id, 0, &remote->sha1, &js); if (!js) { CMD_DEBUG0("local version not found, downloading"); if (!l2_version__download_remote(remote, &js) || !js) return VERSION_EDOWNLOAD; } } else { l2_version__load_local(remote->id, 0, NULL, &js); if (!js) return VERSION_ENOTFOUND; } *ojs = js; return VERSION_SUCCESS; } unsigned l2_version__load_recursive(const char *name, json_t *target, int depth) { json_t *other = NULL; json_t *inherit; if (depth <= 0) return VERSION_ERECURSE; unsigned res = l2_version__load_or_download(name, &other); if (res != VERSION_SUCCESS) return res; l2_json_merge_objects(target, other); inherit = json_object_get(other, "inheritsFrom"); if (!json_is_string(inherit)) { json_decref(other); return VERSION_SUCCESS; } json_decref(other); return l2_version__load_recursive(json_string_value(inherit), target, depth - 1); } unsigned l2_version_load_local(const char *name, json_t **ojson) { json_t *js = json_object(); if (!js) return VERSION_EJSON; unsigned res = l2_version__load_recursive(name, js, 10); *ojson = js; return res; } void l2_version__load_local(const char *id, size_t expectsz, l2_sha1_digest_t *digest, json_t **ojson) { char *path; size_t temp; L2_ASPRINTF(path, temp, "%s/versions/%s/%s.json", l2_state.paths.data, id, id); FILE *ver = fopen(path, "r"); if (!ver) return; int res = l2_version_check_integrity(ver, digest, expectsz); if (res < 0) { CMD_WARN("Error checking version file integrity for %s", id); return; } else if (res == 0) { CMD_DEBUG("Version %s (%s) has bad integrity", id, path); return; } json_t *js; json_error_t jerr; rewind(ver); js = json_loadf(ver, JSON_REJECT_DUPLICATES, &jerr); if (!js) { CMD_WARN("JSON error loading %s: %s", id, jerr.text); return; } *ojson = js; } /* downloads a remote version */ bool l2_version__download_remote(struct l2_version_remote *remote, json_t **ojson) { char *dirname; char *fname; size_t temp; FILE *save = NULL; json_t *js = NULL; L2_ASPRINTF(dirname, temp, "%s/versions/%s", l2_state.paths.data, remote->id); L2_ASPRINTF(fname, temp, "%s/%s.json", dirname, remote->id); if (l2_launcher_mkdir_parents(dirname) < 0) { CMD_ERROR("Could not create directory for %s: %s", remote->id, strerror(errno)); return false; } CURL *cu = curl_easy_init(); l2_sha1_state_t st; l2_sha1_digest_t dig; json_error_t err; if (!cu) return false; void *data = NULL; char errbuf[CURL_ERROR_SIZE]; size_t sz; memset(errbuf, 0, CURL_ERROR_SIZE * sizeof(char)); curl_easy_setopt(cu, CURLOPT_ERRORBUFFER, errbuf); CURLcode code = l2_launcher_download(cu, remote->url, &data, &sz); if (code != CURLE_OK) { CMD_WARN("Error downloading version %s: %s: %s", remote->id, curl_easy_strerror(code), errbuf); goto cleanup; } l2_sha1_init(&st); l2_sha1_update(&st, data, sz); l2_sha1_finalize(&st, &dig); if (l2_sha1_digest_compare(&dig, &remote->sha1)) { char d1[L2_SHA1_DIGESTLEN + 1]; char d2[L2_SHA1_DIGESTLEN + 1]; l2_sha1_digest_to_hex(&dig, d1); l2_sha1_digest_to_hex(&remote->sha1, d2); CMD_WARN("Downloaded file for %s has incorrect hash: expected %s, got %s!", remote->id, d2, d1); goto cleanup; } js = json_loadb(data, sz, JSON_REJECT_DUPLICATES, &err); if (!js) { CMD_WARN("JSON parse error reading version %s: %s", remote->id, err.text); goto cleanup; } save = fopen(fname, "w"); if (!save) { CMD_WARN("Error opening version file %s", fname); goto cleanup; } if (fwrite(data, sz, 1, save) < 1) { CMD_WARN("Error saving version info to %s", fname); goto cleanup; } *ojson = js; curl_easy_cleanup(cu); fclose(save); free(data); return true; cleanup: curl_easy_cleanup(cu); free(data); if (save) fclose(save); if (js) json_decref(js); return false; } char *l2_version__read_etag(time_t *lastmod) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); FILE *etagfp = fopen(etagpath, "rb"); char *etag = NULL; uint32_t buf; uint64_t ts; CMD_DEBUG0("Reading etag file"); if (!etagfp) { CMD_DEBUG0("Failed to open etag file"); return NULL; } if (fread(&buf, sizeof(uint32_t), 1, etagfp) < 1) goto cleanup; if (fread(&ts, sizeof(uint64_t), 1, etagfp) < 1) goto cleanup; buf = l2_betoh32(buf); ts = l2_betoh64(ts); etag = calloc(buf + 1, 1); if (!etag) goto cleanup; if (fread(etag, 1, buf, etagfp) < buf) goto cleanup; etag[buf] = '\0'; *lastmod = (time_t)ts; fclose(etagfp); CMD_DEBUG("Etag read: %s", etag); return etag; cleanup: free(etag); fclose(etagfp); return NULL; } /* this function does not report errors on failure because its operation is not essential */ void l2_version__write_etag(time_t modtime, const char *etag) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); size_t etaglen = strlen(etag); uint32_t etaglenbe = l2_htobe32((uint32_t)etaglen); uint64_t mt_write = l2_htobe64((uint64_t)modtime); FILE *etagfp = fopen(etagpath, "wb"); if (!etagfp) return; if (fwrite(&etaglenbe, sizeof(uint32_t), 1, etagfp) < 1) goto cleanup; if (fwrite(&mt_write, sizeof(uint64_t), 1, etagfp) < 1) goto cleanup; if (fwrite(etag, 1, etaglen, etagfp) < etaglen) goto cleanup; cleanup: fclose(etagfp); } void l2_version__delete_etag(void) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); if (unlink(etagpath) < 0) { CMD_WARN("Failed to delete etag: %s", strerror(errno)); } } int l2_version__read_manifest_file(json_t **manifest) { size_t dpathlen = strlen(l2_state.paths.data); char *manpath; L2_ASTRCAT2(manpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_PATH)); json_error_t err; json_t *man = json_load_file(manpath, JSON_REJECT_DUPLICATES, &err); if (!man) { CMD_WARN("Error loading version manifest file: %s", err.text); return -1; } *manifest = man; return 0; } int l2_version__write_manifest_file(void *data, size_t d) { size_t dpathlen = strlen(l2_state.paths.data); char *manpath; L2_ASTRCAT2(manpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_PATH)); FILE* fp = fopen(manpath, "w"); if (!fp) return -2; if (fwrite(data, 1, d, fp) < d) { fclose(fp); return -1; } fclose(fp); return 0; } unsigned l2_version__load_manifest(json_t **omanifest) { time_t lmod; char *etag = l2_version__read_etag(&lmod); time_t now = time(NULL); CMD_DEBUG("etag: %s, time: %jd", etag ? etag : "(null)", etag ? (uintmax_t)(now - lmod) : UINTMAX_C(0)); if (etag && now - lmod < L2_VERSION_MANIFEST_EXP_TIME) { CMD_DEBUG0("Not downloading (cached)."); if (l2_version__read_manifest_file(omanifest) == 0) { free(etag); return VERSION_SUCCESS; /* if it failed, redownload the file */ } } CMD_DEBUG0("Downloading"); CURL *cu = curl_easy_init(); struct curl_slist *headers = NULL; CURLcode cres; long httpres; unsigned retval = VERSION_EDOWNLOAD; char ebuf[CURL_ERROR_SIZE]; /* assuming CURL_ERROR_SIZE is small enough to fit on the stack... */ memset(ebuf, 0, sizeof(ebuf)); void *data = NULL; size_t dlen; if (!cu) { return VERSION_EDOWNLOAD; } if (etag) { char *headerstr; size_t etaglen = strlen(etag); #define H_INM "If-None-Match: " L2_ASTRCAT2(headerstr, H_INM, L2_CSTRLEN(H_INM), etag, etaglen); #undef H_INM headers = curl_slist_append(NULL, headerstr); if (!headers) { retval = VERSION_EDOWNLOAD; goto cleanup; } CMD_DEBUG("Adding etag header %s", etag); curl_easy_setopt(cu, CURLOPT_HTTPHEADER, headers); } curl_easy_setopt(cu, CURLOPT_ERRORBUFFER, ebuf); if ((cres = l2_launcher_download(cu, L2_URL_META_VERSION_MANIFEST, &data, &dlen)) != CURLE_OK) { CMD_ERROR("Error downloading version manifest: %s: %s", curl_easy_strerror(cres), ebuf); goto cleanup; } curl_easy_getinfo(cu, CURLINFO_RESPONSE_CODE, &httpres); if (httpres == 200) { CMD_DEBUG0("manifest download OK"); /* yay we're good and *data contains our info */ if (l2_version__write_manifest_file(data, dlen) < 0) { CMD_WARN0("Error writing version manifest"); } else { struct curl_header *outh; if (curl_easy_header(cu, "etag", 0, CURLH_HEADER, 0, &outh) == CURLHE_OK) { l2_version__write_etag(time(NULL), outh->value); CMD_DEBUG("New etag returned: %s", outh->value); } } json_error_t err; json_t *js = json_loadb(data, dlen, JSON_REJECT_DUPLICATES, &err); if (!js) { CMD_ERROR("Failed to parse version manifest: %s", err.text); retval = VERSION_EJSON; goto cleanup; } *omanifest = js; } else if (httpres == 304) { /* not modified */ CMD_DEBUG0("manifest download not modified"); if (l2_version__read_manifest_file(omanifest) < 0) { CMD_WARN0("Could not read cached version manifest. Please try again."); l2_version__delete_etag(); retval = VERSION_EJSON; goto cleanup; } /* write the new time because we know the file is good (at least valid json) */ CMD_DEBUG0("writing new etag (read success)"); l2_version__write_etag(time(NULL), etag); /* same etag but we know the file is still good */ } else { CMD_ERROR("Received unexpected HTTP status code: %ld", httpres); goto cleanup; } free(data); free(etag); data = NULL; if (headers) curl_slist_free_all(headers); curl_easy_cleanup(cu); return VERSION_SUCCESS; cleanup: if (headers) curl_slist_free_all(headers); if (cu) curl_easy_cleanup(cu); if (data) free(data); if (etag) free(etag); return retval; } /* parses an instance of version_manifest_v2.json */ unsigned l2_version__load_all_from_json(json_t *json) { if (!json_is_object(json)) { return VERSION_EFORMAT; } json_t *latest = json_object_get(json, "latest"); json_t *versions = json_object_get(json, "versions"); if (!json_is_object(latest) || !json_is_array(versions)) { return VERSION_EFORMAT; } const char *latestrel = json_string_value(json_object_get(latest, "release")); const char *latestsnap = json_string_value(json_object_get(latest, "snapshot")); size_t index; json_t *value; unsigned res; if (!latestrel || !latestsnap) { return VERSION_EFORMAT; } json_array_foreach(versions, index, value) { if ((res = l2_version__add_remote(value)) != VERSION_SUCCESS) return res; } return VERSION_SUCCESS; } unsigned l2_version__add_remote(json_t *js) { struct l2_version_remote *ver = NULL; struct tm ts; json_t *val; unsigned res = VERSION_SUCCESS; if (!json_is_object(js)) { return VERSION_EFORMAT; } ver = calloc(1, sizeof(struct l2_version_remote)); if (!ver) return VERSION_EALLOC; res = VERSION_EFORMAT; val = json_object_get(js, "id"); if (!json_is_string(val)) goto cleanup; ver->id = strdup(json_string_value(val)); val = json_object_get(js, "type"); if (!json_is_string(val)) goto cleanup; ver->type = strdup(json_string_value(val)); val = json_object_get(js, "url"); if (!json_is_string(val)) goto cleanup; ver->url = strdup(json_string_value(val)); val = json_object_get(js, "sha1"); if (!json_is_string(val)) goto cleanup; if (l2_sha1_digest_from_hex(&ver->sha1, json_string_value(val)) < 0) goto cleanup; val = json_object_get(js, "complianceLevel"); if (!json_is_number(val)) goto cleanup; ver->compliance_level = json_integer_value(val); val = json_object_get(js, "time"); if (!json_is_string(val)) goto cleanup; memset(&ts, 0, sizeof(struct tm)); if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; ver->update_time = mktime(&ts); val = json_object_get(js, "releaseTime"); if (!json_is_string(val)) goto cleanup; memset(&ts, 0, sizeof(struct tm)); if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; ver->release_time = mktime(&ts); /* add the thing to the global list */ if (l2_state.ver_remote_tail) { l2_state.ver_remote_tail->next = ver; ver->prev = l2_state.ver_remote_tail; l2_state.ver_remote_tail = ver; } else { l2_state.ver_remote_head = l2_state.ver_remote_tail = ver; } return VERSION_SUCCESS; cleanup: if (ver) { free(ver->id); free(ver->type); free(ver->url); } free(ver); return res; } int l2_version_check_integrity(FILE *fp, l2_sha1_digest_t *digest, size_t sz) { #define VER_READBUF_SZ (1024) size_t len = 0, nread; uint8_t buf[VER_READBUF_SZ]; l2_sha1_digest_t rdigest; l2_sha1_state_t st; l2_sha1_init(&st); while ((nread = fread(buf, 1, VER_READBUF_SZ, fp)) > 0) { len += nread; l2_sha1_update(&st, buf, nread); } if (ferror(fp)) return -1; l2_sha1_finalize(&st, &rdigest); if (sz > 0 && sz != len) return 0; if (digest) { return !l2_sha1_digest_compare(&rdigest, digest) ? 1 : 0; } else { return 1; } }