diff options
| author | 2023-12-28 06:40:07 -0600 | |
|---|---|---|
| committer | 2023-12-28 06:40:07 -0600 | |
| commit | cfe01c9f8b7ac0d82e694323b60fc172e0c35a48 (patch) | |
| tree | 6f02144b38fcb2a4c5116e412f9ed4b0cc711728 /src | |
| parent | add cheap SHA1 implementation (diff) | |
[wip] version stuff
Diffstat (limited to 'src')
| -rw-r--r-- | src/cmd-version.c | 2 | ||||
| -rw-r--r-- | src/command.h | 8 | ||||
| -rw-r--r-- | src/digest/sha1.c | 2 | ||||
| -rw-r--r-- | src/l2su.h | 22 | ||||
| -rw-r--r-- | src/launcherutil.c | 77 | ||||
| -rw-r--r-- | src/macros.h | 14 | ||||
| -rw-r--r-- | src/meson.build | 2 | ||||
| -rw-r--r-- | src/version.c | 327 | ||||
| -rw-r--r-- | src/version.h | 30 |
9 files changed, 481 insertions, 3 deletions
diff --git a/src/cmd-version.c b/src/cmd-version.c index 873108a..62baa3f 100644 --- a/src/cmd-version.c +++ b/src/cmd-version.c @@ -1,11 +1,13 @@ #include "command.h" #include "commands.h" #include "digest/digest.h" +#include "version.h" #include <stdio.h> unsigned cmd_version_list_remote(struct l2_context_node *ctx, char **args) { + printf("%s\n", l2_version_strerror(l2_version_load_remote())); return CMD_RESULT_SUCCESS; } diff --git a/src/command.h b/src/command.h index 7709292..76a9166 100644 --- a/src/command.h +++ b/src/command.h @@ -54,6 +54,14 @@ enum { #define CMD_WARN(_s, ...) CMD_MSG("warn", _s, __VA_ARGS__) #define CMD_WARN0(_s) CMD_MSG0("warn", _s) +#ifdef NDEBUG +#define CMD_DEBUG(_s, ...) +#define CMD_DEBUG0(_s) +#else +#define CMD_DEBUG(_s, ...) CMD_MSG("debug", _s, __VA_ARGS__) +#define CMD_DEBUG0(_s) CMD_MSG0("debug", _s) +#endif + #define CMD_MSG_UNKNOWN_ARGUMENT "Unknown argument '%s'." #define CMD_MSG_REQ_ARG_UNSPECIFIED "Required argument '%s' not specified." #define CMD_MSG_INVALID_UUID "Invalid UUID '%s'." diff --git a/src/digest/sha1.c b/src/digest/sha1.c index 6601961..e563abb 100644 --- a/src/digest/sha1.c +++ b/src/digest/sha1.c @@ -215,7 +215,7 @@ int l2_sha1__hex_to_uint32(const char *in, uint32_t *out) return 0; } -#if 0 +#if L2_SHA1_STANDALONE_TEST int main(int argc, char **argv) { if (argc < 2) return 1; @@ -3,8 +3,11 @@ #include "command.h" #include "instance.h" +#include "version.h" #include <fcntl.h> +#include <time.h> +#include <curl/curl.h> struct l2_user_paths { const char *config; @@ -18,6 +21,9 @@ struct tag_l2_state_t { * but for now a doubly-linked list accomplishes our goals */ struct l2_instance *instance_head; struct l2_instance *instance_tail; + + struct l2_version_remote *ver_remote_head; + struct l2_version_remote *ver_remote_tail; }; extern struct tag_l2_state_t l2_state; @@ -33,4 +39,20 @@ int l2_launcher_open_config(const char *path, int flags, mode_t mode); int l2_launcher_mkdir_parents(const char *path); +char *l2_launcher_parse_iso_time(const char *str, struct tm *ts); + +struct l2_dlbuf { + void *data; + size_t size; + size_t capacity; +}; + +void l2_launcher_download_init(struct l2_dlbuf *buf); +void *l2_launcher_download_finalize(struct l2_dlbuf *buf, size_t *psz); +void l2_launcher_download_cleanup(struct l2_dlbuf *buf); +//extern size_t (*const l2_dlcb)(char *, size_t, size_t, void *); +extern const curl_write_callback l2_dlcb; + +int l2_launcher_download(CURL *cd, const char *url, void **odata, size_t *osize); + #endif /* include guard */ diff --git a/src/launcherutil.c b/src/launcherutil.c index 8df4a71..f73428a 100644 --- a/src/launcherutil.c +++ b/src/launcherutil.c @@ -1,6 +1,7 @@ #include "macros.h" #include "l2su.h" +#include <curl/easy.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> @@ -155,3 +156,79 @@ mdcleanup: 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; + +int l2_launcher_download(CURL *cd, const char *url, void **odata, size_t *osize) +{ + struct l2_dlbuf db; + + 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); + + if (curl_easy_perform(cd) != CURLE_OK) { + l2_launcher_download_cleanup(&db); + return -1; + } + + *odata = l2_launcher_download_finalize(&db, osize); + l2_launcher_download_cleanup(&db); /* not strictly necessary but ok */ + + return 0; +} diff --git a/src/macros.h b/src/macros.h index 4cf6e7b..5487b12 100644 --- a/src/macros.h +++ b/src/macros.h @@ -4,6 +4,20 @@ #include "config.h" #define L2_ARRLEN(_a) (sizeof(_a) / sizeof(*(_a))) +#define L2_CSTRLEN(_s) (L2_ARRLEN(_s) - 1) #define L2_UNUSED(_v) ((void)(_v)) +#include <alloca.h> +#include <string.h> + +#define L2_ASTRCAT2(_var, _s1, _len1, _s2, _len2) do { \ + _var = alloca(_len1 + _len2 + 1); \ + memcpy(_var, (_s1), (_len1)); \ + memcpy(_var + (_len1), (_s2), (_len2)); \ + (_var)[((_len1) + (_len2))] = '\0'; \ +} while (0) + +#define L2_URL_META_BASE "https://piston-meta.mojang.com" +#define L2_URL_META_VERSION_MANIFEST L2_URL_META_BASE "/mc/game/version_manifest_v2.json" + #endif /* include guard */ diff --git a/src/meson.build b/src/meson.build index 261148a..0b90ba4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,4 +1,4 @@ -launcher_srcs = files('l2su.c', 'command.c', 'cmd-instance.c', 'uuid/uuid.c', 'launcherutil.c', 'instance.c', 'cmd-version.c', 'digest/sha1.c') +launcher_srcs = files('l2su.c', 'command.c', 'cmd-instance.c', 'uuid/uuid.c', 'launcherutil.c', 'instance.c', 'cmd-version.c', 'digest/sha1.c', 'version.c') configure_file(input : 'config.h.in', output : 'config.h', configuration : config_data) config_include_dir = include_directories('.') diff --git a/src/version.c b/src/version.c new file mode 100644 index 0000000..b442f85 --- /dev/null +++ b/src/version.c @@ -0,0 +1,327 @@ +#include "version.h" +#include "digest/digest.h" +#include "l2su.h" +#include "macros.h" +#include "endian.h" +#include "command.h" + +#include <curl/curl.h> +#include <curl/easy.h> +#include <curl/header.h> +#include <jansson.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <alloca.h> + +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; +} diff --git a/src/version.h b/src/version.h index b3f640f..246e346 100644 --- a/src/version.h +++ b/src/version.h @@ -1,10 +1,38 @@ #ifndef L2SU_VERSION_H_INCLUDED #define L2SU_VERSION_H_INCLUDED +#include "digest/digest.h" +#include <time.h> +#include <jansson.h> + +enum { + VERSION_SUCCESS, + VERSION_EFORMAT, + VERSION_EJSON, + VERSION_EALLOC, + VERSION_ERRNO, + VERSION_EUNSPEC +}; + struct l2_version_remote { - + struct l2_version_remote *next; + struct l2_version_remote *prev; + + char *id; + char *type; + char *url; + + l2_sha1_digest_t sha1; + json_int_t compliance_level; + + time_t update_time; + time_t release_time; }; +extern const char *const l2_version__messages[]; + +#define l2_version_strerror(_en) l2_version__messages[(_en)] + unsigned l2_version_load_remote(void); unsigned l2_version_load_local(void); |
