#include "version.h" #include "config.h" #include "digest/digest.h" #include "l2su.h" #include "macros.h" #include "endian.h" #include "command.h" #include "downloadpool.h" #include #include #include #include #include #include #include #include #include #include const char *const l2_version__messages[] = { "Success", "Malformed version", "Version JSON error", "Allocation failed", "OS error", "Error downloading version", "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(name, 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_launcher_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; } unsigned l2_version__collect_libraries(json_t *jlibs, l2_subst_t *subst, struct l2_version_library **libs, l2_version_feature_match_proc_t *feature_matcher); void l2_version_free_libraries(struct l2_version_library *libs); int l2_version__get_library_artifact_path(json_t *lib, l2_subst_t *subst, const char *classifier, char **path); struct l2_version__library_dl_data { l2_sha1_state_t st; size_t sz; FILE *ofile; struct l2_version_library *lib; struct l2_version__library_dl_data *next; }; size_t l2_version__lib_writecb(char *ptr, size_t size, size_t nmemb, void *user) { struct l2_version__library_dl_data *data = user; size_t realsz = size * nmemb; if (fwrite(ptr, size, nmemb, data->ofile) < nmemb) { CMD_DEBUG("Error writing to file %s", data->lib->fullpath); return CURL_WRITEFUNC_ERROR; } data->sz += realsz; l2_sha1_update(&data->st, ptr, realsz); return realsz; } int l2_version__dlpool_libs_init(CURL *curl, void *user) { struct l2_version__library_dl_data *data = user; l2_sha1_init(&data->st); if (l2_launcher_mkdir_parents_ex(data->lib->fullpath, 1) < 0) { CMD_WARN("Failed to make directory for libraries: %s", strerror(errno)); return -1; } data->ofile = fopen(data->lib->fullpath, "wb"); if (!data->ofile) { CMD_WARN("Failed to open %s for writing: %s", data->lib->fullpath, strerror(errno)); return -1; } /* curl */ curl_easy_setopt(curl, CURLOPT_URL, data->lib->url); curl_easy_setopt(curl, CURLOPT_WRITEDATA, data); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &l2_version__lib_writecb); return 0; } int l2_version__dlpool_libs_complete(CURL *curl, CURLcode code, void *user) { long rescode; struct l2_version__library_dl_data *data = user; fclose(data->ofile); data->ofile = NULL; if (code != CURLE_OK) { CMD_WARN("Failed to download %s: %s", data->lib->url, curl_easy_strerror(code)); return -1; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rescode); if (rescode / 100 != 2) { CMD_WARN("Failed to download %s: server responded with %ld", data->lib->url, rescode); return -1; } if (data->lib->has_digest) { l2_sha1_digest_t dig; l2_sha1_finalize(&data->st, &dig); if (l2_sha1_digest_compare(&dig, &data->lib->digest)) { char expected[L2_SHA1_HEX_STRLEN + 1], got[L2_SHA1_HEX_STRLEN + 1]; l2_sha1_digest_to_hex(&dig, got); l2_sha1_digest_to_hex(&data->lib->digest, expected); CMD_WARN("Failed to download %s: SHA1 mismatch (expect: %s, got: %s)", data->lib->url, expected, got); if (unlink(data->lib->fullpath) < 0) { CMD_WARN("Failed to delete %s: %s", data->lib->fullpath, strerror(errno)); } return -1; } } if (data->lib->size > 0 && data->sz != data->lib->size) { CMD_WARN("Failed to download %s: size mismatch (expect: %zu bytes, got: %zu bytes)", data->lib->fullpath, data->lib->size, data->sz); if (unlink(data->lib->fullpath) < 0) { CMD_WARN("Failed to delete %s: %s", data->lib->fullpath, strerror(errno)); } return -1; } CMD_DEBUG("Downloaded library %s.", data->lib->url); return 0; } unsigned l2_version_download_libraries(struct l2_version_library *libs) { unsigned res = VERSION_SUCCESS; struct l2_version__library_dl_data *head = NULL, *tail = NULL; l2_dlpool_t *pool = NULL; int dlres; pool = l2_dlpool_init(&l2_version__dlpool_libs_init, &l2_version__dlpool_libs_complete); if (!pool) { res = VERSION_EALLOC; goto cleanup; } for (struct l2_version_library *cur = libs; cur; cur = cur->next) { dlres = l2_launcher_should_download(cur->fullpath, cur->has_digest ? &cur->digest : NULL, cur->size); if (dlres < 0) { res = VERSION_EDOWNLOAD; goto cleanup; } if (!dlres) continue; struct l2_version__library_dl_data *curdata = calloc(1, sizeof(struct l2_version__library_dl_data)); if (!curdata) { res = VERSION_EALLOC; goto cleanup; } curdata->lib = cur; if ((dlres = l2_dlpool_add(pool, curdata)) < 0) { free(curdata); res = VERSION_EALLOC; goto cleanup; } else if (dlres == 0) { free(curdata); res = VERSION_EDOWNLOAD; goto cleanup; } if (tail) { tail->next = curdata; tail = curdata; } else { head = tail = curdata; } } if (head != NULL) { if (l2_dlpool_perform(pool) < 0) { CMD_WARN0("Error downloading libraries."); res = VERSION_EDOWNLOAD; } else { CMD_INFO0("Successfully downloaded libraries."); res = VERSION_SUCCESS; } } else { CMD_INFO0("No libraries to download."); res = VERSION_SUCCESS; } cleanup: for (struct l2_version__library_dl_data *cur = head, *temp; cur; cur = temp) { temp = cur->next; if (cur->ofile) { fclose(cur->ofile); if (unlink(cur->lib->fullpath) < 0) { CMD_WARN("Error deleting partially downloaded file %s: %s", cur->lib->fullpath, strerror(errno)); } } free(cur); } if (pool) l2_dlpool_cleanup(pool); return res; } int l2_version__should_download(const char *path, const l2_sha1_digest_t *expectdigest, size_t expectsize) { FILE *lfile = fopen(path, "rb"); if (!lfile) { if (errno == ENOENT) { return 1; } else { CMD_DEBUG("Failed to open %s for reading: %s", path, strerror(errno)); return -1; } } int res = l2_launcher_check_integrity(lfile, expectdigest, expectsize); fclose(lfile); switch (res) { case 0: CMD_DEBUG("SHOULD redownload %s, the SHA1 or size doesn't match.", path); return 1; case 1: CMD_DEBUG("SHOULDN'T redownload %s.", path); return 0; default: return res; } } struct l2_version__integrity_data { char *baseurl; struct l2_version_library *lib; /* here for convenience */ char digest_hex[L2_SHA1_HEX_STRLEN + 1]; size_t len; }; size_t l2_version__integ_writecb(char *ptr, size_t size, size_t nmemb, void *user) { /* NOTE: this function does not check what it's reading (or anything after the 40th character). * It could be argued that this is a problem (the file may have stuff after the hash), but idc LOL. */ struct l2_version__integrity_data *integ = user; size_t realsz = size * nmemb; size_t rem = L2_SHA1_HEX_STRLEN - integ->len; if (rem <= 0) return realsz; size_t copylen = L2_BADMIN(rem, realsz); memcpy(integ->digest_hex, ptr, copylen); integ->len += copylen; return realsz; } int l2_version__integ_dl_init(CURL *curl, void *user) { struct l2_version_library *lib = user; char *fullurl; size_t sz; L2_ASPRINTF(fullurl, sz, "%s.sha1", lib->url); curl_easy_setopt(curl, CURLOPT_URL, fullurl); curl_easy_setopt(curl, CURLOPT_WRITEDATA, user); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &l2_version__integ_writecb); CMD_DEBUG("Downloading signature for library: %s", fullurl); return 0; } int l2_version__integ_dl_finish(CURL *curl, CURLcode code, void *user) { struct l2_version__integrity_data *integ = user; long res; if (code != CURLE_OK) { CMD_WARN("Error downloading integrity (%s): %s", integ->baseurl, curl_easy_strerror(code)); return -1; /* vanilla considers this non-fatal */ } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res); if ((res / 100) != 2) { CMD_WARN("Error downloading integrity (%s): server returned HTTP %ld", integ->baseurl, res); CMD_WARN0("Proceeding under the assumption that this resource is not checksummed. Integrity will not be checked."); return 0; } if (l2_sha1_digest_from_hex(&integ->lib->digest, integ->digest_hex) < 0) { CMD_WARN("Error downloading integrity (%s): %s", integ->baseurl, "invalid sha1 digest returned"); return -1; /* vanilla considers this non-fatal (though the comparison will always* fail) */ } CMD_DEBUG("Retrieved SHA1 information for %s", integ->baseurl); integ->lib->has_digest = true; return 0; } unsigned l2_version__retrieve_library_integ(struct l2_version_library *libs) { unsigned res = VERSION_SUCCESS; struct l2_version__integrity_data *integ = NULL; l2_dlpool_t *pool = NULL; size_t nlibs = 0; for (struct l2_version_library *cur = libs; cur; cur = cur->next) { if (!cur->has_digest) ++nlibs; } if (!nlibs) return VERSION_SUCCESS; integ = calloc(nlibs, sizeof(struct l2_version__integrity_data)); if (!integ) { res = VERSION_EALLOC; goto cleanup; } pool = l2_dlpool_init(&l2_version__integ_dl_init, &l2_version__integ_dl_finish); if (!pool) { res = VERSION_EALLOC; goto cleanup; } nlibs = 0; int addres; for (struct l2_version_library *cur = libs; cur; cur = cur->next) { if (cur->has_digest) continue; integ[nlibs].lib = cur; integ[nlibs].baseurl = cur->url; if ((addres = l2_dlpool_add(pool, integ + nlibs)) < 0) { res = VERSION_EALLOC; goto cleanup; } else if (addres == 0) { integ[nlibs].lib = NULL; /* signal that this one is invalid */ continue; } ++nlibs; } if (l2_dlpool_perform(pool) < 0) { res = VERSION_EDOWNLOAD; goto cleanup; } free(integ); l2_dlpool_cleanup(pool); return VERSION_SUCCESS; cleanup: free(integ); if (pool) l2_dlpool_cleanup(pool); return res; } unsigned l2_version_collect_libraries(json_t *version, struct l2_version_library **libs, l2_version_feature_match_proc_t *feature_matcher) { json_t *jlibs; unsigned res; if (json_unpack(version, "{s:o}", "libraries", &jlibs) < 0) { return VERSION_EJSON; } l2_subst_t *subst = NULL; if (l2_subst_init(&subst) < 0) { res = VERSION_EALLOC; goto cleanup; } if (l2_subst_add(subst, "arch", L2SU_LAUNCHER_ARCH_BITS) < 0) { res = VERSION_EALLOC; goto cleanup; } res = l2_version__collect_libraries(jlibs, subst, libs, feature_matcher); cleanup: if (subst) l2_subst_free(subst); return res; } unsigned l2_version__library_get_classifier(json_t *lib, const char **class) { json_t *natives = json_object_get(lib, "natives"); if (json_is_object(natives)) { json_t *mynative = json_object_get(natives, L2SU_LAUNCHER_OS); if (json_is_string(mynative)) { *class = json_string_value(mynative); } else if (mynative) { return VERSION_EJSON; } else { *class = NULL; } } else if (natives) { return VERSION_EJSON; } else { *class = NULL; } return VERSION_SUCCESS; } unsigned l2_version__gen_one_library(json_t *lib, l2_subst_t *subst, struct l2_version_library **olib) { const char *classifier; unsigned res; int has_class; if ((res = l2_version__library_get_classifier(lib, &classifier)) != VERSION_SUCCESS) { return res; } struct l2_version_library *retlib = calloc(1, sizeof(struct l2_version_library)); if (!retlib) return VERSION_EALLOC; if ((has_class = l2_version__get_library_artifact_path(lib, subst, classifier, &retlib->artifactpath)) < 0) { res = VERSION_EJSON; goto cleanup; } if (!(retlib->fullpath = l2_launcher_sprintf_alloc("%s/libraries/%s", l2_state.paths.data, retlib->artifactpath))) { res = VERSION_EALLOC; goto cleanup; } if (classifier) { retlib->extract.extracttype = EXTRACT_OLD; } else if (has_class) { retlib->extract.extracttype = EXTRACT_NEW; } else { retlib->extract.extracttype = EXTRACT_NONE; } /* first, find the URL of the library */ json_t *jurl = json_object_get(lib, "url"); if (json_is_string(jurl)) { retlib->url = l2_launcher_sprintf_alloc("%s%s", json_string_value(jurl), retlib->artifactpath); retlib->has_digest = false; if (!retlib->url) { res = VERSION_EALLOC; goto cleanup; } } else if (jurl) { res = VERSION_EJSON; goto cleanup; } json_t *jdownloads = json_object_get(lib, "downloads"); if (json_is_object(jdownloads)) { json_t *dljson = NULL; if (classifier) { if (json_unpack(jdownloads, "{s:{s:o}}", "classifiers", classifier, &dljson) < 0 || !dljson) { res = VERSION_EJSON; goto cleanup; } } else { dljson = json_object_get(jdownloads, "artifact"); } if (!json_is_object(dljson)) { res = VERSION_EJSON; goto cleanup; } const char *courl, *cosha1; json_int_t asize; if (json_unpack(dljson, "{s:s, s:s, s:I}", "url", &courl, "sha1", &cosha1, "size", &asize) < 0) { res = VERSION_EJSON; goto cleanup; } retlib->url = strdup(courl); if (!retlib->url) { res = VERSION_EJSON; goto cleanup; } retlib->size = (size_t)asize; retlib->has_digest = true; if (l2_sha1_digest_from_hex(&retlib->digest, cosha1) < 0) { res = VERSION_EJSON; goto cleanup; } } else if (jdownloads) { res = VERSION_EJSON; goto cleanup; } else { /* guess from name */ retlib->url = l2_launcher_sprintf_alloc("https://libraries.minecraft.net/%s", retlib->artifactpath); retlib->has_digest = false; if (!retlib->url) { res = VERSION_EALLOC; goto cleanup; } res = VERSION_EJSON; goto cleanup; } json_t *jextract = json_object_get(lib, "extract"); if (json_is_object(jextract)) { json_t *jexclude = json_object_get(jextract, "exclude"); if (json_is_array(jexclude)) { size_t idx; json_t *val; retlib->extract.exclude = calloc(json_array_size(jexclude) + 1, sizeof(char *)); if (!retlib->extract.exclude) { res = VERSION_EALLOC; goto cleanup; } json_array_foreach(jexclude, idx, val) { if (!json_is_string(val)) { res = VERSION_EJSON; goto cleanup; } retlib->extract.exclude[idx] = strdup(json_string_value(val)); if (!retlib->extract.exclude[idx]) { res = VERSION_EALLOC; goto cleanup; } } } else if (jexclude) { res = VERSION_EJSON; goto cleanup; } } else if (jextract) { res = VERSION_EJSON; goto cleanup; } *olib = retlib; return VERSION_SUCCESS; cleanup: l2_version_free_libraries(retlib); return res; } unsigned l2_version__collect_libraries(json_t *jlibs, l2_subst_t *subst, struct l2_version_library **libs, l2_version_feature_match_proc_t *feature_matcher) { struct l2_version_library *libraries = NULL, *tail = NULL; struct l2_version_library *curlib = NULL; unsigned ret = VERSION_EJSON; enum l2_version_check_result applies; size_t idx; json_t *val; if (!json_is_array(jlibs)) return VERSION_EJSON; json_array_foreach(jlibs, idx, val) { if (!json_is_object(val)) { ret = VERSION_EJSON; goto cleanup; } json_t *rules = json_object_get(val, "rules"); if (rules) { applies = l2_version_check_rules(rules, feature_matcher); if (applies == RULE_CHECK_ERROR) { ret = VERSION_EJSON; goto cleanup; } else if (applies != RULE_CHECK_ALLOW) { continue; } } /* this library applies, now generate the download for it */ const char *name = json_string_value(json_object_get(val, "name")); CMD_DEBUG("Library: %s", name); curlib = NULL; ret = l2_version__gen_one_library(val, subst, &curlib); if (ret == VERSION_EJSON && !curlib) { CMD_WARN("Library %p was not decoded properly. This is likely a launcher deficiency.", (void *)curlib); continue; } else if (ret != VERSION_SUCCESS) { goto cleanup; } if (tail) { tail->next = curlib; tail = curlib; } else { libraries = tail = curlib; } } *libs = libraries; return VERSION_SUCCESS; cleanup: /* free libraries */ l2_version_free_libraries(libraries); return ret; } void l2_version_free_libraries(struct l2_version_library *libs) { if (!libs) return; char **excludecur; for (struct l2_version_library *next = libs->next; libs; libs = next) { next = libs->next; free(libs->artifactpath); free(libs->fullpath); free(libs->url); excludecur = libs->extract.exclude; if (excludecur) { for (; *excludecur; ++excludecur) { free(*excludecur); } free(libs->extract.exclude); } free(libs); } } int l2_version__get_library_artifact_path(json_t *lib, l2_subst_t *subst, const char *classifier, char **path) { json_t *jname = json_object_get(lib, "name"); if (!json_is_string(jname)) return -1; const char *cname = json_string_value(jname); char *name; size_t size; L2_ASTRDUP(name, size, cname); char *nameparts[] = { name, NULL, NULL, NULL }; size_t curpart = 1; for (char *cur = name; *cur; ++cur) { if (*cur == ':') { if (curpart == 4) return -1; /* invalid name: too many colons! */ *cur = '\0'; nameparts[curpart++] = cur + 1; } else if (curpart == 1 && *cur == '.') { *cur = '/'; } } if (!nameparts[1] || !nameparts[2]) return -1; char *buf; size_t bufsz; int has_class = 0; if (nameparts[3] || classifier) { const char *class = nameparts[3] ? nameparts[3] : classifier; L2_ASPRINTF(buf, bufsz, "%s/%s/%s/%s-%s-%s.jar", nameparts[0], nameparts[1], nameparts[2], nameparts[1], nameparts[2], class); has_class = 1; } else { L2_ASPRINTF(buf, bufsz, "%s/%s/%s/%s-%s.jar", nameparts[0], nameparts[1], nameparts[2], nameparts[1], nameparts[2]); } return l2_subst_apply(subst, buf, path) < 0 ? -1 : has_class; } int l2_version__whole_string_matches(const char *pattern, const char *subject) { PCRE2_SIZE erroffset; int errcode; pcre2_code *cd = pcre2_compile((PCRE2_SPTR8)pattern, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED | PCRE2_ENDANCHORED, &errcode, &erroffset, NULL); pcre2_match_data *md; if (!cd) { return -1; } md = pcre2_match_data_create(1, NULL); errcode = pcre2_match(cd, (PCRE2_SPTR8)subject, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); /* we care about neither the code nor the match data */ pcre2_code_free(cd); pcre2_match_data_free(md); if (errcode == PCRE2_ERROR_NOMATCH) return 0; return errcode >= 0 ? 1 : -1; } enum l2_version_check_result l2_version__check_one_rule(json_t *rule, l2_version_feature_match_proc_t *feature_matcher) { if (!json_is_object(rule)) return RULE_CHECK_ERROR; enum l2_version_check_result myaction; json_t *action = json_object_get(rule, "action"); if (!json_is_string(action)) return RULE_CHECK_ERROR; const char *actionstr = json_string_value(action); if (!strcmp(actionstr, "allow")) { myaction = RULE_CHECK_ALLOW; } else if (!strcmp(actionstr, "disallow")) { myaction = RULE_CHECK_DISALLOW; } else { return RULE_CHECK_ERROR; } json_t *val = json_object_get(rule, "os"); if (json_is_object(val)) { json_t *checkval = json_object_get(val, "name"); if (json_is_string(checkval)) { if (strcmp(json_string_value(checkval), L2SU_LAUNCHER_OS)) { /* wrong OS, rule doesn't apply */ return RULE_CHECK_DEFAULT; } } else if (checkval) { return RULE_CHECK_ERROR; } checkval = json_object_get(val, "arch"); if (json_is_string(checkval)) { int res = l2_version__whole_string_matches(json_string_value(checkval), L2SU_LAUNCHER_ARCH); if (res < 0) return RULE_CHECK_ERROR; if (!res) return RULE_CHECK_DEFAULT; } else if (checkval) { return RULE_CHECK_ERROR; } /* FIXME: don't check OS version because idk how to get that portably yet */ return myaction; } else if (val) { return RULE_CHECK_ERROR; } val = json_object_get(rule, "features"); if (json_is_object(val)) { json_t *feat; const char *fname; json_object_foreach(val, fname, feat) { if (!(*feature_matcher)(fname, feat)) { return RULE_CHECK_DEFAULT; } } } else if (val) { return RULE_CHECK_ERROR; } return myaction; } enum l2_version_check_result l2_version_check_rules(json_t *rules, l2_version_feature_match_proc_t *feature_matcher) { size_t idx; json_t *val; enum l2_version_check_result res = RULE_CHECK_DEFAULT; if (!json_is_array(rules)) { return RULE_CHECK_ERROR; } json_array_foreach(rules, idx, val) { /* first, check if the rule actually matches us */ switch (l2_version__check_one_rule(val, feature_matcher)) { case RULE_CHECK_ERROR: return RULE_CHECK_ERROR; case RULE_CHECK_ALLOW: res = RULE_CHECK_ALLOW; break; case RULE_CHECK_DISALLOW: res = RULE_CHECK_DISALLOW; break; default: break; } } return res; } unsigned l2_version_download_jar(json_t *version, const char *specifier, char **path) { const char *id = NULL; const char *digeststr = NULL; const char *url = NULL; json_int_t jexpectsize = 0; size_t expect_size; l2_sha1_digest_t expect_digest; if (json_unpack(version, "{s:{s:{s?:s, s?:s, s?:I}}, s:s}", "downloads", specifier, "sha1", &digeststr, "url", &url, "size", &jexpectsize, "id", &id) < 0) { return VERSION_EJSON; } /* make sure values in version file make sense */ if (jexpectsize < 0) return VERSION_EJSON; if (digeststr && l2_sha1_digest_from_hex(&expect_digest, digeststr) < 0) return VERSION_EJSON; expect_size = (size_t)jexpectsize; /* create the final path where the jar is placed */ char *pathstr = l2_launcher_sprintf_alloc("%s/versions/%s/%s.jar", l2_state.paths.data, id, id); if (!pathstr) return VERSION_EALLOC; int dlres = l2_launcher_download_checksummed(url, pathstr, digeststr ? &expect_digest : NULL, expect_size); if (dlres < 0) { free(pathstr); return VERSION_EDOWNLOAD; } *path = pathstr; return VERSION_SUCCESS; }