diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/l2su.h | 1 | ||||
| -rw-r--r-- | src/launcherutil.c | 21 | ||||
| -rw-r--r-- | src/runtime.c | 509 |
3 files changed, 528 insertions, 3 deletions
@@ -37,6 +37,7 @@ char *l2_launcher_strapp(char *target, const char *src); bool l2_launcher_strpre(const char *pre, const char *text); char *l2_launcher_sprintf_alloc(const char *fmt, ...) L2_FORMAT(printf, 1, 2); +char *l2_launcher_sprintf_resize(char **buf, size_t *len, const char *fmt, ...) L2_FORMAT(printf, 3, 4); /* launcher utilities */ char *l2_launcher_find_config_path(void); diff --git a/src/launcherutil.c b/src/launcherutil.c index b292df9..070290d 100644 --- a/src/launcherutil.c +++ b/src/launcherutil.c @@ -66,6 +66,27 @@ char *l2_launcher_sprintf_alloc(const char *fmt, ...) return ret; } +char *l2_launcher_sprintf_resize(char **buf, size_t *len, const char *fmt, ...) +{ + va_list pva; + + va_start(pva, fmt); + size_t needlen = vsnprintf(NULL, 0, fmt, pva); + va_end(pva); + + if (needlen + 1 > *len) { + char *temp = realloc(*buf, needlen + 1); + if (!temp) return NULL; + *buf = temp; + *len = needlen + 1; + } + + va_start(pva, fmt); + vsnprintf(*buf, *len, fmt, pva); + va_end(pva); + return *buf; +} + char *l2_launcher_find_config_path(void) { /* check for $L2SU_CONFIG */ diff --git a/src/runtime.c b/src/runtime.c index b20353c..b811519 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -4,6 +4,7 @@ #include "macros.h" #include "l2su.h" #include "endian.h" +#include "downloadpool.h" #include <stdlib.h> #include <string.h> @@ -12,10 +13,12 @@ #include <stdio.h> #include <stdint.h> #include <curl/curl.h> +#include <sys/stat.h> #include <unistd.h> #include <time.h> #include <inttypes.h> #include <ftw.h> +#include <lzma.h> #define L2_RUNTIME__MANIFEST_EXP_TIME ((time_t)120) @@ -361,7 +364,7 @@ int l2_runtime__save_component_manifest_info(const char *compname, const char *v int l2_runtime__check_runtime_ftw(const char *fpath, const struct stat *sb, int typeflag, struct l2_ftw_data *ftwbuf); -int l2_runtime__ensure_runtime_files(json_t *jfiles); +int l2_runtime__ensure_runtime_files(const char *basepath, json_t *jfiles); int l2_runtime_install_component(json_t *manifest, const char *component) { @@ -428,7 +431,7 @@ int l2_runtime_install_component(json_t *manifest, const char *component) goto cleanup; } - if (l2_runtime__ensure_runtime_files(jfiles) < 0) { + if (l2_runtime__ensure_runtime_files(jrepath, jfiles) < 0) { goto cleanup; } @@ -440,6 +443,500 @@ cleanup: return -1; } +struct l2_runtime__file_info { + /* set at init */ + char *lzmaurl, *rawurl; + char *path; + l2_sha1_digest_t expect_digest; + size_t expect_size; + + l2_sha1_state_t digest_state; + size_t nread_total; + + FILE *ofile; + + bool lzma_active; + lzma_stream lz; + + struct l2_runtime__file_info *next; +}; + +void l2_runtime__file_info_cleanup(struct l2_runtime__file_info *info) +{ + free(info->lzmaurl); + free(info->rawurl); + + if (info->ofile) { + fclose(info->ofile); + if (info->path && unlink(info->path) < 0 && errno != ENOENT) { + CMD_WARN("Failed to delete %s: %s", info->path, strerror(errno)); + } + } + + free(info->path); + + if (info->lzma_active) { + lzma_end(&info->lz); + } +} + +int l2_runtime__ensure_one_file(const char *filename, json_t *file, struct l2_runtime__file_info **ptail) +{ + l2_sha1_digest_t expect_digest; + const char *expect_digest_str; + json_int_t expect_size; + struct l2_runtime__file_info *info = NULL; + + const char *raw_url = NULL; + + json_t *lzma_download = NULL; + if (json_unpack(file, "{s:{s:{s:s, s:I, s?:s}, s?:o}}", "downloads", "raw", "sha1", &expect_digest_str, "size", &expect_size, "url", &raw_url, "lzma", &lzma_download) < 0) { + CMD_DEBUG0("invalid format for runtime manifest: couldn't get downloads"); + return -1; + } + + if (l2_sha1_digest_from_hex(&expect_digest, expect_digest_str) < 0) { + CMD_DEBUG("invalid format for runtime manifest: sha1 hash is nonsensical %s", expect_digest_str); + return -1; + } + + FILE *fp = fopen(filename, "rb"); + if (!fp) { + if (errno == ENOENT) { + goto ok_we_have_to_download_the_file; + } else { + CMD_WARN("Cannot open %s for reading: %s", filename, strerror(errno)); + return -1; + } + } + + int integres = l2_launcher_check_integrity(fp, &expect_digest, (size_t)expect_size); + fclose(fp); + fp = NULL; + + if (integres < 0) { + CMD_WARN("Failed to check file integrity for %s.", filename); + return -1; + } else if (integres) { + CMD_DEBUG("Not downloading %s, integrity info matches :)", filename); + return 0; + } + +ok_we_have_to_download_the_file: + info = calloc(1, sizeof(struct l2_runtime__file_info)); + if (!info) { + CMD_DEBUG("allocation failed! %s %s", filename, strerror(errno)); + return -1; + } + + const char *lzma_url = NULL; + if (lzma_download && json_unpack(lzma_download, "{s:s}", "url", &lzma_url) < 0) { + CMD_DEBUG0("invalid format for lzma download: no url"); + goto cleanup; + } + + if (lzma_url) { + info->lzmaurl = strdup(lzma_url); + if (!info->lzmaurl) goto cleanup; + } else { + if (!raw_url) { + CMD_WARN("No download for %s!", filename); + goto cleanup; + } + + info->rawurl = strdup(raw_url); + } + + info->path = strdup(filename); + if (!info->path) { + goto cleanup; + } + + info->expect_size = (size_t)expect_size; + l2_sha1_digest_copy(&info->expect_digest, &expect_digest); + + if (*ptail) { + (*ptail)->next = info; + } + *ptail = info; + return 0; + +cleanup: + l2_runtime__file_info_cleanup(info); + free(info); + return -1; +} + +int l2_runtime__ensure_one_link(const char *filename, json_t *file) +{ + /* TODO */ + const char *target; + size_t targetlen; + if (json_unpack(file, "{s:s%}", "target", &target, &targetlen) < 0) { + CMD_DEBUG0("bad link"); + return -1; + } + + char *buf = alloca(targetlen + 1); + ssize_t nread; + + if ((nread = readlink(filename, buf, targetlen + 1)) < 0) { + if (errno == ENOENT) { + goto set_link; + } + CMD_WARN("Could not read link at %s: %s", filename, strerror(errno)); + return -1; + } + + buf[targetlen] = '\0'; + if (targetlen == (size_t)nread && !strcmp(buf, target)) { + CMD_DEBUG("link %s is good", filename); + return 0; + } + + if (unlink(filename) < 0) { + CMD_WARN("Failed to delete symlink %s: %s", filename, strerror(errno)); + return -1; + } + +set_link: + if (symlink(target, filename) < 0) { + CMD_WARN("Failed to create symlink %s to %s: %s", filename, target, strerror(errno)); + return -1; + } + + return 0; +} + +int l2_runtime__component_update_file(const char *fname, json_t *file, const char *basepath, struct l2_runtime__file_info **ptail) +{ + char *path; + size_t pathsz; + L2_ASPRINTF(path, pathsz, "%s/%s", basepath, fname); + + const char *typestr; + if (json_unpack(file, "{s:s}", "type", &typestr) < 0) { + CMD_DEBUG0("bad type info or something idk"); + return -1; + } + + mode_t exptype = 0; + if (!strcmp(typestr, "file")) { + exptype = S_IFREG; + } else if (!strcmp(typestr, "directory")) { + exptype = S_IFDIR; + } else if (!strcmp(typestr, "link")) { + exptype = S_IFLNK; + } else { + CMD_DEBUG("unknown file type %s, %s", path, typestr); + return -1; + } + + struct stat sp; + bool exists = true; + if (lstat(path, &sp) < 0) { + if (errno == ENOENT) { + exists = false; + } else { + CMD_WARN("could not stat %s: %s", path, strerror(errno)); + return -1; + } + } + + if (exists && (sp.st_mode & S_IFMT) != exptype) { /* should never happen besides by a race condition */ + CMD_WARN("File %s has wrong type! (should be %s (%o), got %o)", path, typestr, exptype, sp.st_mode & S_IFMT); + return -1; + } + + switch (exptype) { + case S_IFDIR: + if (!exists && mkdir(path, 0755) < 0) { + CMD_WARN("Failed to make directory %s: %s", path, strerror(errno)); + return -1; + } + return 0; + case S_IFREG: + return l2_runtime__ensure_one_file(path, file, ptail); + case S_IFLNK: + return l2_runtime__ensure_one_link(path, file); + } + + CMD_MSG0("bug", "expected file type was not file, link, or directory!!!!"); + abort(); +} + +const char *l2_runtime__get_lzma_error(lzma_ret lzret) +{ + switch (lzret) { + case LZMA_OK: + case LZMA_STREAM_END: + return NULL; + case LZMA_MEM_ERROR: + return "out of memory"; + case LZMA_MEMLIMIT_ERROR: + return "memory limit reached"; + case LZMA_FORMAT_ERROR: + return "LZMA/XZ format error"; + case LZMA_OPTIONS_ERROR: + return "invalid usage"; + case LZMA_BUF_ERROR: + return "file is truncated or corrupt"; + case LZMA_DATA_ERROR: + return "file is corrupt"; + default: + return "unknown error"; + } +} + +size_t l2_runtime__dl_lzma_callback(char *buf, size_t size, size_t nmemb, void *user) +{ + struct l2_runtime__file_info *info = user; + size_t realsz = size * nmemb; + info->lz.total_in = 0; + + uint8_t outbuf[4096]; + info->lz.next_in = (const uint8_t *)buf; + info->lz.avail_in = realsz; + do { + info->lz.next_out = outbuf; + info->lz.avail_out = sizeof(outbuf); + + lzma_ret lzret = lzma_code(&info->lz, LZMA_RUN); + const char *errstr = l2_runtime__get_lzma_error(lzret); + + if (errstr) { + CMD_ERROR("Error downloading %s: %s (error %u)", info->path, errstr, lzret); + return CURL_WRITEFUNC_ERROR; + } + + size_t ndecoded = sizeof(outbuf) - info->lz.avail_out; + + if (!ndecoded) return realsz; + + info->nread_total += ndecoded; + l2_sha1_update(&info->digest_state, outbuf, ndecoded); + + errno = 0; + if (fwrite(outbuf, 1, ndecoded, info->ofile) < ndecoded) { + CMD_WARN("Partial write %s: %s", info->path, strerror(errno)); + return CURL_WRITEFUNC_ERROR; + } + } while (info->lz.avail_in); + + return realsz; +} + +size_t l2_runtime__dl_raw_callback(char *buf, size_t size, size_t nmemb, void *user) +{ + struct l2_runtime__file_info *info = user; + size_t realsz = size * nmemb; + + info->nread_total += realsz; + l2_sha1_update(&info->digest_state, buf, realsz); + + errno = 0; + if (fwrite(buf, size, nmemb, info->ofile) < nmemb) { + CMD_WARN("Partial write %s: %s", info->path, strerror(errno)); + return CURL_WRITEFUNC_ERROR; + } + + return realsz; +} + +int l2_runtime__dl_init(CURL *curl, void *user) +{ + struct l2_runtime__file_info *info = user; + + info->ofile = fopen(info->path, "wb"); + if (!info->ofile) { + CMD_WARN("Failed to open %s for writing: %s", info->path, strerror(errno)); + return -1; + } + + l2_sha1_init(&info->digest_state); + + if (info->lzmaurl) { + lzma_stream strm = LZMA_STREAM_INIT; + lzma_ret lzret; + memcpy(&info->lz, &strm, sizeof(lzma_stream)); + + switch (lzret = lzma_auto_decoder(&info->lz, UINT64_MAX, 0)) { + case LZMA_OK: + break; + case LZMA_MEM_ERROR: + CMD_WARN("Failed to initialize LZMA stream for %s: out of memory", info->path); + return -1; + case LZMA_OPTIONS_ERROR: + CMD_WARN("Failed to initialize LZMA stream for %s: invalid argument", info->path); + return -1; + case LZMA_PROG_ERROR: + CMD_WARN("Failed to initialize LZMA stream for %s: bug", info->path); + return -1; + default: + CMD_WARN("Failed to initialize LZMA stream for %s: unknown error %u", info->path, lzret); + return -1; + } + + curl_easy_setopt(curl, CURLOPT_URL, info->lzmaurl); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &l2_runtime__dl_lzma_callback); + info->lzma_active = true; + } else { + curl_easy_setopt(curl, CURLOPT_URL, info->rawurl); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &l2_runtime__dl_raw_callback); + info->lzma_active = false; + } + + curl_easy_setopt(curl, CURLOPT_USERAGENT, L2_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, user); + + return 0; +} + +int l2_runtime__dl_complete(CURL *curl, CURLcode code, void *user) +{ + struct l2_runtime__file_info *info = user; + + if (code != CURLE_OK) { + CMD_WARN("Download for %s failed: CURL error: %s", info->path, curl_easy_strerror(code)); + return -1; + } + + long httpres = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres); + if (httpres / 100 != 2) { + CMD_WARN("Download for %s failed: server responded with %lu", info->path, httpres); + return -1; + } + + if (info->lzma_active) { + uint8_t outbuf[4096]; + info->lz.next_in = NULL; + info->lz.avail_in = 0; + + lzma_ret lzret; + while (true) { + info->lz.next_out = outbuf; + info->lz.avail_out = sizeof(outbuf); + + lzret = lzma_code(&info->lz, LZMA_FINISH); + const char *errstr = l2_runtime__get_lzma_error(lzret); + + if (errstr) { + CMD_WARN("Download for %s failed: LZMA error: %s (error %u)", info->path, errstr, lzret); + return -1; + } + + size_t ndecoded = sizeof(outbuf) - info->lz.avail_out; + if (!ndecoded) break; + + l2_sha1_update(&info->digest_state, outbuf, ndecoded); + info->nread_total += ndecoded; + + errno = 0; + if (fwrite(outbuf, 1, ndecoded, info->ofile) < ndecoded) { + CMD_WARN("Download for %s failed: partial write: %s", info->path, strerror(errno)); + return -1; + } + + if (lzret == LZMA_STREAM_END) break; + } + + lzma_end(&info->lz); + info->lzma_active = false; + } + + fclose(info->ofile); + info->ofile = NULL; + + l2_sha1_digest_t rddg; + l2_sha1_finalize(&info->digest_state, &rddg); + if (l2_sha1_digest_compare(&rddg, &info->expect_digest)) { + char expstr[L2_SHA1_HEX_STRLEN + 1], gotstr[L2_SHA1_HEX_STRLEN + 1]; + l2_sha1_digest_to_hex(&info->expect_digest, expstr); + l2_sha1_digest_to_hex(&rddg, gotstr); + CMD_WARN("Download for %s failed: SHA1 mismatch (expected %s, got %s)", info->path, expstr, gotstr); + goto cleanup_unlink; + } + + if (info->nread_total != info->expect_size) { + CMD_WARN("Download for %s failed: size mismatch (expected %zu bytes, got %zu bytes)", info->path, info->expect_size, info->nread_total); + goto cleanup_unlink; + } + + return 0; + +cleanup_unlink: + if (unlink(info->path) < 0) { + CMD_WARN("Failed to delete %s: %s", info->path, strerror(errno)); + } + return -1; +} + +int l2_runtime__ensure_runtime_files(const char *basepath, json_t *jfiles) +{ + int ret = 0; + struct l2_runtime__file_info *info_head, *info_tail; + info_head = info_tail = NULL; + l2_dlpool_t *dlpool = NULL; + + char *fullpath = NULL; + size_t len = 0; + + json_t *file; + const char *fname; + + json_object_foreach(jfiles, fname, file) { + if (!l2_launcher_sprintf_resize(&fullpath, &len, "%s/%s", basepath, fname)) { + ret = -1; + goto cleanup; + } + + if (l2_runtime__component_update_file(fname, file, basepath, &info_tail) < 0) { + ret = -1; + goto cleanup; + } + + if (info_tail && !info_head) info_head = info_tail; + } + + if (!info_head) { + CMD_INFO("No files to download for %s.", basepath); + ret = 0; + goto cleanup; + } + + dlpool = l2_dlpool_init(&l2_runtime__dl_init, &l2_runtime__dl_complete); + if (!dlpool) { + ret = -1; + goto cleanup; + } + + for (struct l2_runtime__file_info *cur = info_head; cur; cur = cur->next) { + if (l2_dlpool_add(dlpool, cur) <= 0) { + ret = -1; + goto cleanup; + } + } + + if (l2_dlpool_perform(dlpool) < 0) { + ret = -1; + } else { + ret = 0; + } + +cleanup: + free(fullpath); + if (dlpool) l2_dlpool_cleanup(dlpool); + + for (struct l2_runtime__file_info *temp; info_head; info_head = temp) { + temp = info_head->next; + + l2_runtime__file_info_cleanup(info_head); + free(info_head); + } + return ret; +} + int l2_runtime__check_runtime_ftw(const char *fpath, const struct stat *sb, int typeflag, struct l2_ftw_data *ftw) { L2_UNUSED(sb); @@ -492,7 +989,13 @@ int l2_runtime__check_runtime_ftw(const char *fpath, const struct stat *sb, int return -1; } - unsigned desired_mode = (sb->st_mode & 07777) | 0111; + unsigned desired_mode; + if (executable) { + desired_mode = (sb->st_mode & 07777) | 0111; + } else { + desired_mode = (sb->st_mode & 07777) & ~0111; + } + if ((sb->st_mode & 07777) != desired_mode && chmod(fpath, desired_mode) < 0) { CMD_WARN("Failed chmod(%s, %#o): %s", fpath, desired_mode, strerror(errno)); return -1; |
