#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", 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; unsigned r = l2_version__load_manifest(&vers); json_decref(vers); return r; } unsigned l2_version_load_local(void) { return VERSION_SUCCESS; } 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; } 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; }