#include "digest/digest.h" #include "macros.h" #include "l2su.h" #include #include #include #include #include #include #include #include #include #include #include /* handcoded string functions * * NOTE: I am aware that this is inefficient but since these are used only a handful of times * during initialization, I don't think performance is really a huge deal here. * * remember that this is a Minecraft launcher, so the PC has to meet the minimum specs of that, * which are well above the minimum specs of this launcher */ char *l2_launcher_strapp(char *buf, const char *src) { size_t buflen = strlen(buf); size_t srclen = strlen(src); char *ret = realloc(buf, buflen + srclen + 1); if (!ret) return NULL; memcpy(ret + buflen, src, srclen); ret[buflen + srclen] = '\0'; /* realloc does not initialize like calloc does */ return ret; } char *l2_launcher_sprintf_alloc(const char *fmt, ...) { va_list pva; va_start(pva, fmt); size_t len = vsnprintf(NULL, 0, fmt, pva); va_end(pva); char *ret = calloc(len + 1, sizeof(char)); if (!ret) { return ret; } va_start(pva, fmt); vsnprintf(ret, len + 1, fmt, pva); va_end(pva); return ret; } char *l2_launcher_find_config_path(void) { /* check for $L2SU_CONFIG */ char *config = getenv(PROJECT_NAME_UPPER "_CONFIG"); if (config) { return strdup(config); } /* check for $XDG_CONFIG_HOME */ config = getenv("XDG_CONFIG_HOME"); if (config) { /* want to append '"/" PROJECT_NAME' to our string */ char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/" PROJECT_NAME); } /* check for $HOME/.config */ config = getenv("HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/.config/" PROJECT_NAME); } /* fail (do NOT attempt to find home directory from passwd */ return NULL; } char *l2_launcher_find_data_path(void) { /* check for $L2SU_DATA */ char *config = getenv(PROJECT_NAME_UPPER "_DATA"); if (config) { return strdup(config); } /* check for $XDG_DATA_HOME */ config = getenv("XDG_DATA_HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/" PROJECT_NAME); } /* check for $HOME/.local/share */ config = getenv("HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/.local/share/" PROJECT_NAME); } return NULL; } int l2_launcher_open_config(const char *path, int flags, mode_t mode) { int conffd = open(l2_state.paths.config, O_RDONLY | O_DIRECTORY); if (conffd < 0) return INSTANCE_ERRNO; int instfd = openat(conffd, path, flags, mode); int en = errno; /* back up errno because close can fail */ close(conffd); errno = en; return instfd; } int l2_launcher_mkdir_parents(const char *path) { return l2_launcher_mkdir_parents_ex(path, 0); } /* NOTE: There's no portable (or otherwise - see open(2) BUGS) way to do this without race conditions. */ int l2_launcher_mkdir_parents_ex(const char *path, unsigned ignore) { if (*path != '/') return -1; char *pathbuf; size_t pathlen; L2_ASTRDUP(pathbuf, pathlen, path); char *pcurelem = pathbuf; for (char *cur = pathbuf + pathlen; ignore && cur != pathbuf; --cur) { if (*cur == '/') { --ignore; *cur = '\0'; } } struct stat stbuf = { 0 }; do { /* strtok is off-limits because it smells bad */ *pcurelem = '/'; pcurelem = strchr(pcurelem + 1, '/'); if (pcurelem) { *pcurelem = '\0'; } /* now pathbuf contains our truncated path name which may or may not exist */ if (mkdir(pathbuf, 0755) < 0) { if (errno == EEXIST) { /* racy: stat the file and continue if it is a directory */ if (stat(pathbuf, &stbuf) < 0) { return -1; } if (!S_ISDIR(stbuf.st_mode)) { return -1; } } else { return -1; } } } while (pcurelem); return 0; } char *l2_launcher_parse_iso_time(const char *str, struct tm *ts) { return strptime(str, "%FT%T%z", ts); /* TODO: replace with something portable */ } void l2_launcher_download_init(struct l2_dlbuf *buf) { buf->data = NULL; buf->size = 0; buf->capacity = 0; } size_t l2_launcher__download_callback(char *data, size_t size, size_t nmemb, void *user) { struct l2_dlbuf *buf = user; size_t realsz = size * nmemb; if (buf->size + realsz > buf->capacity) { size_t newcap = 16; while (newcap && buf->size + realsz > newcap) newcap <<= 1; if (!newcap) return CURLE_WRITE_ERROR; void *newbuf = realloc(buf->data, newcap); if (!newbuf) return CURLE_WRITE_ERROR; buf->data = newbuf; buf->capacity = newcap; } memcpy((char *)buf->data + buf->size, data, realsz); buf->size += realsz; return realsz; } void *l2_launcher_download_finalize(struct l2_dlbuf *buf, size_t *psz) { void *smaller = realloc(buf->data, buf->size); if (smaller) buf->data = smaller; void *data = buf->data; buf->data = NULL; *psz = buf->size; return data; } void l2_launcher_download_cleanup(struct l2_dlbuf *buf) { free(buf->data); } const curl_write_callback l2_dlcb = &l2_launcher__download_callback; CURLcode l2_launcher_download(CURL *cd, const char *url, void **odata, size_t *osize) { struct l2_dlbuf db; CURLcode code; l2_launcher_download_init(&db); curl_easy_setopt(cd, CURLOPT_URL, url); curl_easy_setopt(cd, CURLOPT_WRITEDATA, &db); curl_easy_setopt(cd, CURLOPT_WRITEFUNCTION, l2_dlcb); curl_easy_setopt(cd, CURLOPT_USERAGENT, L2_USER_AGENT); if ((code = curl_easy_perform(cd)) != CURLE_OK) { l2_launcher_download_cleanup(&db); return code; } *odata = l2_launcher_download_finalize(&db, osize); l2_launcher_download_cleanup(&db); /* not strictly necessary but ok */ return CURLE_OK; } int l2_json_merge_objects(json_t *j1, json_t *j2) { const char *key; size_t keylen; json_t *val; json_t *myval; json_object_keylen_foreach(j2, key, keylen, val) { myval = json_object_getn(j1, key, keylen); if (json_is_object(myval) && json_is_object(val)) { if (l2_json_merge_objects(myval, val) < 0) return -1; } else if (json_is_array(myval) && json_is_array(val)) { if (json_array_extend(myval, val) < 0) return -1; } else if (!myval) { if (json_object_setn_nocheck(j1, key, keylen, val) < 0) return -1; } } return 0; } int l2_launcher_check_integrity(FILE *fp, const 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; if (!digest && sz == 0) return 1; 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; } } int l2_launcher_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_launcher__download_data { l2_sha1_state_t digest_state; size_t recv_size; FILE *fp; }; size_t l2_launcher__download_writecb(char *ptr, size_t size, size_t nmemb, void *user) { struct l2_launcher__download_data *data = user; size_t realsz = size * nmemb; /* size should be 1 but whatever */ if (fwrite(ptr, size, nmemb, data->fp) < nmemb) { return CURL_WRITEFUNC_ERROR; } l2_sha1_update(&data->digest_state, ptr, realsz); data->recv_size += realsz; return realsz; } int l2_launcher_download_checksummed(const char *url, const char *pathstr, l2_sha1_digest_t *expect_digest, size_t expect_size) { int res = -1; CURL *pc = NULL; /* check if we even need to redownload the thing */ int rdres = l2_launcher_should_download(pathstr, expect_digest, expect_size); if (rdres < 0) { return -1; } else if (!rdres) { CMD_DEBUG("Not downloading %s", pathstr); return 0; } if (!url) { CMD_WARN("Cannot redownload %s, even though I need to! (no URL specified)", pathstr); return -1; } /* redownload the file */ struct l2_launcher__download_data dldata; char errbuf[CURL_ERROR_SIZE]; memset(&dldata, 0, sizeof(struct l2_launcher__download_data)); memset(&errbuf, 0, sizeof(errbuf)); l2_sha1_init(&dldata.digest_state); if (l2_launcher_mkdir_parents_ex(pathstr, 1) < 0) { goto cleanup; } dldata.fp = fopen(pathstr, "wb"); if (!dldata.fp) { CMD_WARN("Failed to open %s for writing: %s", pathstr, strerror(errno)); goto cleanup; } pc = curl_easy_init(); if (!pc) { goto cleanup; } curl_easy_setopt(pc, CURLOPT_USERAGENT, L2_USER_AGENT); curl_easy_setopt(pc, CURLOPT_URL, url); curl_easy_setopt(pc, CURLOPT_WRITEDATA, &dldata); curl_easy_setopt(pc, CURLOPT_WRITEFUNCTION, &l2_launcher__download_writecb); curl_easy_setopt(pc, CURLOPT_ERRORBUFFER, errbuf); CURLcode cres = curl_easy_perform(pc); if (cres != CURLE_OK) { CMD_WARN("Failed to download %s: %s: %s", pathstr, curl_easy_strerror(cres), errbuf); goto cleanup; } fclose(dldata.fp); dldata.fp = NULL; curl_easy_cleanup(pc); pc = NULL; l2_sha1_digest_t recvdigest; l2_sha1_finalize(&dldata.digest_state, &recvdigest); if (expect_digest && l2_sha1_digest_compare(&recvdigest, expect_digest)) { char expstr[L2_SHA1_HEX_STRLEN + 1]; char gotstr[L2_SHA1_HEX_STRLEN + 1]; l2_sha1_digest_to_hex(expect_digest, expstr); l2_sha1_digest_to_hex(&recvdigest, gotstr); CMD_WARN("Downloaded %s has wrong digest! (expected: %s, got: %s)", pathstr, expstr, gotstr); if (unlink(pathstr) < 0) { CMD_WARN("Failed to delete %s: %s", pathstr, strerror(errno)); } goto cleanup; } if (expect_size > 0 && expect_size != dldata.recv_size) { CMD_WARN("Downloaded %s has wrong size! (expected: %zu bytes, got: %zu bytes)", pathstr, expect_size, dldata.recv_size); if (unlink(pathstr) < 0) { CMD_WARN("Failed to delete %s: %s", pathstr, strerror(errno)); } goto cleanup; } CMD_INFO("Downloaded %s successfully.", pathstr); curl_easy_cleanup(pc); return 1; cleanup: if (dldata.fp) fclose(dldata.fp); if (pc) curl_easy_cleanup(pc); return res; }