#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 #include const char *const l2_version__messages[] = { "Success", "Malformed version manifest", "Version manifest JSON error", "Allocation failed", "OS error", "An unspecified error has occurred", 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__download_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; return l2_version__download_manifest(&vers); } 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; if (!etagfp) return NULL; if (fread(&buf, sizeof(uint32_t), 1, etagfp) < sizeof(uint32_t)) goto cleanup; if (fread(&ts, sizeof(uint64_t), 1, etagfp) < sizeof(uint64_t)) 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); 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)); uint32_t etaglen = l2_betoh32((uint32_t)strlen(etag)); uint64_t mt_write = l2_betoh64((uint64_t)modtime); FILE *etagfp = fopen(etagpath, "wb"); if (!etagfp) return; if (fwrite(&etaglen, sizeof(uint32_t), 1, etagfp) < sizeof(uint32_t)) goto cleanup; if (fwrite(&mt_write, sizeof(uint64_t), 1, etagfp) < sizeof(uint64_t)) goto cleanup; if (fwrite(etag, 1, etaglen, etagfp) < etaglen) goto cleanup; cleanup: fclose(etagfp); } 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 manifest file: %s\n", err.text); return -1; } *manifest = man; return 0; } #if 0 unsigned l2_version__download_manifest(json_t **out_versions) { unsigned ret = VERSION_SUCCESS; CURL *pc = NULL; FILE *outfile = NULL; void *verdata = NULL; int en; struct curl_slist *headers = NULL; time_t lastmod; char *etag = l2_version__read_etag(&lastmod); if (etag) { if (lastmod + L2_VERSION_MANIFEST_EXP_TIME > time(NULL)) { /* no need to redownload */ *out_versions = NULL; return VERSION_SUCCESS; } } pc = curl_easy_init(); struct l2_dlbuf dlbuf; if (!pc) return VERSION_EUNSPEC; l2_launcher_download_init(&dlbuf); if (etag) { size_t etaglen = strlen(etag); char *etagheader; #define H_IFNONEMATCH "If-None-Match: " L2_ASTRCAT2(etagheader, H_IFNONEMATCH, L2_CSTRLEN(H_IFNONEMATCH), etag, etaglen); #undef H_IFNONEMATCH headers = curl_slist_append(NULL, etagheader); if (!headers) { ret = VERSION_EUNSPEC; goto cleanup; } curl_easy_setopt(pc, CURLOPT_HTTPHEADER, headers); } curl_easy_setopt(pc, CURLOPT_URL, L2_URL_META_VERSION_MANIFEST); curl_easy_setopt(pc, CURLOPT_WRITEFUNCTION, l2_dlcb); curl_easy_setopt(pc, CURLOPT_WRITEDATA, &dlbuf); if (curl_easy_perform(pc) != CURLE_OK) { /* TODO: set error string */ ret = VERSION_EUNSPEC; goto cleanup; } long rescode = 0; curl_easy_getinfo(pc, CURLINFO_RESPONSE_CODE, &rescode); if (rescode == 200) { char *mfpath; size_t datalen; verdata = l2_launcher_download_finalize(&dlbuf, &datalen); L2_ASTRCAT2(mfpath, l2_state.paths.data, pathlen, L2_VERSION_MANIFEST_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_PATH)); outfile = fopen(mfpath, "w"); if (!outfile) { ret = VERSION_ERRNO; goto cleanup; } if (fwrite(verdata, 1, datalen, outfile) < datalen) { ret = VERSION_EUNSPEC; goto cleanup; } fclose(outfile); outfile = NULL; } struct curl_header *ch; if (curl_easy_header(pc, "ETag", 0, CURLH_HEADER | CURLH_TRAILER | CURLH_1XX, 0, &ch) == CURLHE_OK) { l2_version__write_etag(etagpath, time(NULL), ch->value); } ret = VERSION_SUCCESS; cleanup: en = errno; if (pc) curl_easy_cleanup(pc); if (headers) curl_slist_free_all(headers); if (outfile) fclose(outfile); free(verdata); l2_launcher_download_cleanup(&dlbuf); errno = en; return ret; } #endif /* 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; }