From 7bd2c17f9d59e58fc9e769cb01f4aa47a164b321 Mon Sep 17 00:00:00 2001 From: Tim Angus Date: Sun, 2 Nov 2025 17:19:40 +0000 Subject: [PATCH] Move autoupdater to its own repo (https://github.com/ioquake/autoupdater) --- code/autoupdater/autoupdater.c | 1046 ----------------- code/autoupdater/rsa_tools/.gitignore | 8 - .../rsa_tools/build-libtom-unix.sh | 79 -- code/autoupdater/rsa_tools/build-rsa-tools.sh | 33 - code/autoupdater/rsa_tools/rsa_common.c | 61 - code/autoupdater/rsa_tools/rsa_common.h | 30 - code/autoupdater/rsa_tools/rsa_make_keys.c | 45 - code/autoupdater/rsa_tools/rsa_sign.c | 75 -- code/autoupdater/rsa_tools/rsa_verify.c | 60 - code/autoupdater/rsa_tools/test-rsa-tools.sh | 17 - docs/autoupdater-readme.txt | 165 --- 11 files changed, 1619 deletions(-) delete mode 100644 code/autoupdater/autoupdater.c delete mode 100644 code/autoupdater/rsa_tools/.gitignore delete mode 100755 code/autoupdater/rsa_tools/build-libtom-unix.sh delete mode 100755 code/autoupdater/rsa_tools/build-rsa-tools.sh delete mode 100644 code/autoupdater/rsa_tools/rsa_common.c delete mode 100644 code/autoupdater/rsa_tools/rsa_common.h delete mode 100644 code/autoupdater/rsa_tools/rsa_make_keys.c delete mode 100644 code/autoupdater/rsa_tools/rsa_sign.c delete mode 100644 code/autoupdater/rsa_tools/rsa_verify.c delete mode 100755 code/autoupdater/rsa_tools/test-rsa-tools.sh delete mode 100644 docs/autoupdater-readme.txt diff --git a/code/autoupdater/autoupdater.c b/code/autoupdater/autoupdater.c deleted file mode 100644 index e013a1f4..00000000 --- a/code/autoupdater/autoupdater.c +++ /dev/null @@ -1,1046 +0,0 @@ -/* -The code in this file is in the public domain. The rest of ioquake3 -is licensed under the GPLv2. Do not mingle code, please! -*/ - -#include -#include -#include -#include -#include - -#if defined(_MSC_VER) && (_MSC_VER < 1600) -typedef __int64 int64_t; -#else -#include -#endif - -#include -#include -#include - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN 1 -#include -#include -#include -#define PIDFMT "%u" -#define PIDFMTCAST unsigned int -typedef DWORD PID; -#else -#include -#include -typedef pid_t PID; -#define PIDFMT "%llu" -#define PIDFMTCAST unsigned long long -#endif - -/* If your build fails here with tomcrypt.h missing, you probably need to - run the build-libtom script in the rsa_tools subdirectory. */ -#define TFM_DESC -#define LTC_NO_ROLC -#include "tomcrypt.h" - -#define PUBLICKEY_FNAME "updater-publickey.bin" -#define SALT_LEN 8 -static int sha256_hash_index = 0; - - -#ifndef AUTOUPDATE_USER_AGENT -#define AUTOUPDATE_USER_AGENT "ioq3autoupdater/0.1" -#endif - - -#ifndef AUTOUPDATE_URL - -#ifndef AUTOUPDATE_BASEURL -#define AUTOUPDATE_BASEURL "https://upd.ioquake3.org/updates/v1" -#endif - -#ifndef AUTOUPDATE_PACKAGE -#define AUTOUPDATE_PACKAGE "ioquake3" -#endif - -#ifdef __APPLE__ -#define AUTOUPDATE_PLATFORM "mac" -#elif defined(__linux__) -#define AUTOUPDATE_PLATFORM "linux" -#elif defined(_WIN32) -#define AUTOUPDATE_PLATFORM "windows" -#else -#error Please define your platform. -#endif - -#ifdef __i386__ -#define AUTOUPDATE_ARCH "x86" -#elif defined(__x86_64__) -#define AUTOUPDATE_ARCH "x86_64" -#else -#error Please define your platform. -#endif - -#define AUTOUPDATE_URL AUTOUPDATE_BASEURL "/" AUTOUPDATE_PACKAGE "/" AUTOUPDATE_PLATFORM "/" AUTOUPDATE_ARCH "/" -#endif - -#if defined(__GNUC__) || defined(__clang__) -#define NEVER_RETURNS __attribute__((noreturn)) -#define PRINTF_FUNC(fmtargnum, dotargnum) __attribute__ (( format( __printf__, fmtargnum, dotargnum ))) -#else -#define NEVER_RETURNS -#define PRINTF_FUNC(fmtargnum, dotargnum) -#endif - - -typedef struct -{ - pid_t waitforprocess; - const char *updateself; -} Options; - -static Options options; - - -typedef struct ManifestItem -{ - char *fname; - unsigned char sha256[32]; - int64_t len; - int update; - int rollback; - struct ManifestItem *next; -} ManifestItem; - -static ManifestItem *manifest = NULL; - -static void freeManifest(void) -{ - ManifestItem *item = manifest; - manifest = NULL; - - while (item != NULL) { - ManifestItem *next = item->next; - free(item->fname); - free(item); - item = next; - } - manifest = NULL; -} - -static const char *timestamp(void) -{ - time_t t = time(NULL); - char *retval = asctime(localtime(&t)); - if (retval) { - char *ptr; - for (ptr = retval; *ptr; ptr++) { - if ((*ptr == '\r') || (*ptr == '\n')) { - *ptr = '\0'; - break; - } - } - } - return retval ? retval : "[date unknown]"; -} - - -static FILE *logfile = NULL; - -static void info(const char *str) -{ - fputs(str, logfile); - fputs("\n", logfile); - fflush(logfile); -} - -static void infof(const char *fmt, ...) PRINTF_FUNC(1, 2); -static void infof(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - vfprintf(logfile, fmt, ap); - va_end(ap); - fputs("\n", logfile); - fflush(logfile); -} - -static void restoreRollbacks(void) -{ - /* you can't call die() in this function! If this fails, you're doomed. */ - ManifestItem *item; - for (item = manifest; item != NULL; item = item->next) { - if (item->rollback) { - char rollbackPath[64]; - snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", item->rollback); - infof("restore rollback: '%s' -> '%s'", rollbackPath, item->fname); - remove(item->fname); - rename(rollbackPath, item->fname); - } - } -} - -static void die(const char *why) NEVER_RETURNS; - - - -#ifdef _WIN32 - -#define chmod(a,b) do {} while (0) -#define makeDir(path) mkdir(path) - -static void windowsWaitForProcessToDie(const DWORD pid) -{ - HANDLE h; - infof("Waiting on process ID #%u", (unsigned int) pid); - h = OpenProcess(SYNCHRONIZE, FALSE, pid); - if (!h) { - const DWORD err = GetLastError(); - if (err == ERROR_INVALID_PARAMETER) { - info("No such process; probably already dead. Carry on."); - return; /* process is (probably) already gone. */ - } - infof("OpenProcess failed. err=%d", (unsigned int) err); - die("OpenProcess failed"); - } - if (WaitForSingleObject(h, INFINITE) != WAIT_OBJECT_0) { - die("WaitForSingleObject failed"); - } - CloseHandle(h); -} - -static void launchProcess(const char *exe, ...) -{ - PROCESS_INFORMATION procinfo; - STARTUPINFO startinfo; - va_list ap; - char cmdline[1024]; - char *ptr = cmdline; - size_t totallen = 0; - const char *arg = NULL; - - #define APPENDCMDLINE(str) { \ - const size_t len = strlen(str); \ - totallen += len; \ - if ((totallen + 1) < sizeof (cmdline)) { \ - strcpy(ptr, str); \ - ptr += len; \ - } \ - } - - va_start(ap, exe); - APPENDCMDLINE(exe); - while ((arg = va_arg(ap, const char *)) != NULL) { - APPENDCMDLINE(arg); - } - va_end(ap); - - if (totallen >= sizeof (cmdline)) { - die("command line too long to launch."); - } - - cmdline[totallen] = 0; - - infof("launching process '%s' with cmdline '%s'", exe, cmdline); - - memset(&procinfo, '\0', sizeof (procinfo)); - memset(&startinfo, '\0', sizeof (startinfo)); - startinfo.cb = sizeof (startinfo); - if (CreateProcessA(exe, cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startinfo, &procinfo)) - { - CloseHandle(procinfo.hProcess); - CloseHandle(procinfo.hThread); - exit(0); /* we're done, it's launched. */ - } - - infof("CreateProcess failed: err=%d", (int) GetLastError()); -} - -static HINTERNET hInternet; -static void prepHttpLib(void) -{ - hInternet = InternetOpenA(AUTOUPDATE_USER_AGENT, - INTERNET_OPEN_TYPE_PRECONFIG, - NULL, NULL, 0); - if (!hInternet) { - die("InternetOpen failed"); - } -} - -static void shutdownHttpLib(void) -{ - if (hInternet) { - InternetCloseHandle(hInternet); - hInternet = NULL; - } -} - -static int runHttpDownload(const char *from, FILE *to) -{ - /* !!! FIXME: some of this could benefit from GetLastError+FormatMessage. */ - int retval = 0; - DWORD httpcode = 0; - DWORD dwordlen = sizeof (DWORD); - DWORD zero = 0; - HINTERNET hUrl = InternetOpenUrlA(hInternet, from, NULL, 0, - INTERNET_FLAG_HYPERLINK | - INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | - INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS | - INTERNET_FLAG_NO_CACHE_WRITE | - INTERNET_FLAG_NO_COOKIES | - INTERNET_FLAG_NO_UI | - INTERNET_FLAG_RESYNCHRONIZE | - INTERNET_FLAG_RELOAD | - INTERNET_FLAG_SECURE, 0); - - if (!hUrl) { - infof("InternetOpenUrl failed. err=%d", (int) GetLastError()); - } else if (!HttpQueryInfo(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &httpcode, &dwordlen, &zero)) { - infof("HttpQueryInfo failed. err=%d", (int) GetLastError()); - } else if (httpcode != 200) { - infof("HTTP request failed with response code %d", (int) httpcode); - } else { - while (1) { - DWORD br = 0; - BYTE buf[1024 * 64]; - if (!InternetReadFile(hUrl, buf, sizeof (buf), &br)) { - infof("InternetReadFile failed. err=%d", (int) GetLastError()); - break; - } else if (br == 0) { - retval = 1; - break; /* done! */ - } else { - if (fwrite(buf, br, 1, to) != 1) { - info("fwrite failed"); - break; - } - } - } - } - - InternetCloseHandle(hUrl); - return retval; -} - -#else /* Everything that isn't Windows. */ - -#define launchProcess execl -#define makeDir(path) mkdir(path, 0777) - -/* hooray for Unix linker hostility! */ -#undef curl_easy_setopt -#include -typedef void (*CURLFN_curl_easy_cleanup)(CURL *curl); -typedef CURL *(*CURLFN_curl_easy_init)(void); -typedef CURLcode (*CURLFN_curl_easy_setopt)(CURL *curl, CURLoption option, ...); -typedef CURLcode (*CURLFN_curl_easy_perform)(CURL *curl); -typedef CURLcode (*CURLFN_curl_global_init)(long flags); -typedef void (*CURLFN_curl_global_cleanup)(void); - -static CURLFN_curl_easy_cleanup CURL_curl_easy_cleanup; -static CURLFN_curl_easy_init CURL_curl_easy_init; -static CURLFN_curl_easy_setopt CURL_curl_easy_setopt; -static CURLFN_curl_easy_perform CURL_curl_easy_perform; -static CURLFN_curl_global_init CURL_curl_global_init; -static CURLFN_curl_global_cleanup CURL_curl_global_cleanup; - -static void prepHttpLib(void) -{ - #ifdef __APPLE__ - const char *libname = "libcurl.4.dylib"; - #else - const char *libname = "libcurl.so.4"; - #endif - - void *handle = dlopen(libname, RTLD_NOW | RTLD_GLOBAL); - if (!handle) { - infof("dlopen(\"%s\") failed: %s", libname, dlerror()); - die("Failed to load libcurl library"); - } - #define LOADCURLSYM(fn) \ - if ((CURL_##fn = (CURLFN_##fn) dlsym(handle, #fn)) == NULL) { \ - die("Failed to load libcurl symbol '" #fn "'"); \ - } - - LOADCURLSYM(curl_easy_cleanup); - LOADCURLSYM(curl_easy_init); - LOADCURLSYM(curl_easy_setopt); - LOADCURLSYM(curl_easy_perform); - LOADCURLSYM(curl_global_init); - LOADCURLSYM(curl_global_cleanup); - - #define curl_easy_cleanup CURL_curl_easy_cleanup - #define curl_easy_init CURL_curl_easy_init - #define curl_easy_setopt CURL_curl_easy_setopt - #define curl_easy_perform CURL_curl_easy_perform - #define curl_global_init CURL_curl_global_init - #define curl_global_cleanup CURL_curl_global_cleanup - - if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { - die("curl_global_init() failed!"); - } -} - -static void shutdownHttpLib(void) -{ - if (curl_global_cleanup) { - curl_global_cleanup(); - } -} - -static int runHttpDownload(const char *from, FILE *to) -{ - int retval; - CURL *curl = curl_easy_init(); - if (!curl) { - info("curl_easy_init() failed"); - return 0; - } - - #if 0 - /* !!! FIXME: enable compression? */ - curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); /* enable compression */ - - /* !!! FIXME; hook up proxy support to libcurl */ - curl_easy_setopt(curl, CURLOPT_PROXY, proxyURL); - #endif - - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_STDERR, logfile); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, to); - - curl_easy_setopt(curl, CURLOPT_URL, from); - - curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); /* allow redirects. */ - curl_easy_setopt(curl, CURLOPT_USERAGENT, AUTOUPDATE_USER_AGENT); - - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); /* require valid SSL cert. */ - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); /* require SSL cert with same hostname as we connected to. */ - - retval = (curl_easy_perform(curl) == CURLE_OK); - curl_easy_cleanup(curl); - return retval; -} -#endif - - -static void die(const char *why) -{ - infof("FAILURE: %s", why); - restoreRollbacks(); - freeManifest(); - infof("Updater ending (in failure), %s", timestamp()); - exit(1); -} - -static void outOfMemory(void) NEVER_RETURNS; -static void outOfMemory(void) -{ - die("Out of memory"); -} - -static void buildParentDirs(const char *_path) -{ - char *ptr; - char *path = (char *) alloca(strlen(_path) + 1); - if (!path) { - outOfMemory(); - } - strcpy(path, _path); - - for (ptr = path; *ptr; ptr++) { - if (*ptr == '/') { - *ptr = '\0'; - makeDir(path); - *ptr = '/'; - } - } -} - -static int64_t fileLength(const char *fname) -{ - struct stat statbuf; - if (stat(fname, &statbuf) == -1) { - return -1; - } - return (int64_t) statbuf.st_size; -} - -static void parseArgv(int argc, char **argv) -{ - int i; - - infof("command line (argc=%d)...", argc); - for (i = 0; i < argc; i++) { - infof(" argv[%d]: %s",i, argv[i]); - } - - for (i = 1; i < argc; i += 2) { - if (strcmp(argv[i], "--waitpid") == 0) { - options.waitforprocess = atoll(argv[i + 1]); - infof("We will wait for process " PIDFMT " if necessary", (PIDFMTCAST) options.waitforprocess); - } else if (strcmp(argv[i], "--updateself") == 0) { - options.updateself = argv[i + 1]; - infof("We are updating ourself ('%s')", options.updateself); - } - } -} - -static void downloadURL(const char *from, const char *to) -{ - FILE *io = NULL; - const size_t len = strlen(AUTOUPDATE_URL) + strlen(from) + 1; - char *fullurl = (char *) alloca(len); - if (!fullurl) { - outOfMemory(); - } - snprintf(fullurl, len, "%s%s", AUTOUPDATE_URL, from); - - infof("Downloading from '%s' to '%s'", fullurl, to); - - buildParentDirs(to); - io = fopen(to, "wb"); - if (!io) { - die("Failed to open output file"); - } - - if (!runHttpDownload(fullurl, io)) { - fclose(io); - remove(to); - die("Download failed"); - } - - if (fclose(io) == EOF) { - die("Can't flush file on close. i/o error? Disk full?"); - } - - chmod(to, 0777); /* !!! FIXME */ -} - -static int hexcvt(const int ch) -{ - if ((ch >= 'a') && (ch <= 'f')) { - return (ch - 'a') + 10; - } else if ((ch >= 'A') && (ch <= 'F')) { - return (ch - 'A') + 10; - } else if ((ch >= '0') && (ch <= '9')) { - return ch - '0'; - } else { - die("Invalid hex character"); - } - return 0; -} - -static void convertSha256(char *str, unsigned char *sha256) -{ - int i; - for (i = 0; i < 32; i++) { - const int a = hexcvt(*(str++)); - const int b = hexcvt(*(str++)); - *sha256 = (a << 4) | b; - sha256++; - } -} - -static void parseManifest(const char *fname) -{ - ManifestItem *item = NULL; - FILE *io = fopen(fname, "r"); - char buf[512]; - if (!io) { - die("Failed to open manifest for reading"); - } - - /* !!! FIXME: this code sucks. */ - while (fgets(buf, sizeof (buf), io)) { - char *ptr = (buf + strlen(buf)) - 1; - while (ptr >= buf) { - if ((*ptr != '\n') && (*ptr != '\r')) { - break; - } - *ptr = '\0'; - ptr--; - } - - if (!item && !buf[0]) { - continue; /* blank line between items or blank at EOF */ - } - - if (!item) { - infof("Next manifest item: %s", buf); - - item = (ManifestItem *) calloc(1, sizeof (ManifestItem)); - if (!item) { - outOfMemory(); - } - item->fname = strdup(buf); - if (!item->fname) { - outOfMemory(); - } - item->len = -1; - item->next = NULL; - } else if (item->len == -1) { - infof("Item size: %s", buf); - item->len = atoll(buf); - } else { - infof("Item sha256: %s", buf); - convertSha256(buf, item->sha256); - item->next = manifest; - manifest = item; - item = NULL; - } - } - - if (ferror(io)) { - die("Error reading manifest"); - } else if (item) { - die("Incomplete manifest"); - } - - fclose(io); -} - -static void read_file(const char *fname, void *buf, unsigned long *len) -{ - ssize_t br; - FILE *io = fopen(fname, "rb"); - if (!io) { - infof("Can't open '%s' for reading: %s", fname, strerror(errno)); - die("Failed to read file"); - } - - br = fread(buf, 1, *len, io); - if (ferror(io)) { - infof("Couldn't read '%s': %s", fname, strerror(errno)); - die("Failed to read file"); - } else if (!feof(io)) { - infof("Buffer too small to read '%s'", fname); - die("Failed to read file"); - } - fclose(io); - - *len = (unsigned long) br; -} - -static void read_rsakey(rsa_key *key, const char *fname) -{ - unsigned char buf[4096]; - unsigned long len = sizeof (buf); - int rc; - - read_file(fname, buf, &len); - - if ((rc = rsa_import(buf, len, key)) != CRYPT_OK) { - infof("rsa_import for '%s' failed: %s", fname, error_to_string(rc)); - die("Couldn't import public key"); - } -} - -static void verifySignature(const char *fname, const char *sigfname, const char *keyfname) -{ - rsa_key key; - unsigned char hash[256]; - unsigned long hashlen = sizeof (hash); - unsigned char sig[1024]; - unsigned long siglen = sizeof (sig); - int status = 0; - int rc = 0; - - read_rsakey(&key, keyfname); - read_file(sigfname, sig, &siglen); - - if ((rc = hash_file(sha256_hash_index, fname, hash, &hashlen)) != CRYPT_OK) { - infof("hash_file for '%s' failed: %s", fname, error_to_string(rc)); - die("Couldn't verify manifest signature"); - } - - if ((rc = rsa_verify_hash(sig, siglen, hash, hashlen, sha256_hash_index, SALT_LEN, &status, &key)) != CRYPT_OK) { - infof("rsa_verify_hash for '%s' failed: %s", fname, error_to_string(rc)); - die("Couldn't verify manifest signature"); - } - - if (!status) { - infof("Invalid signature for '%s'! Don't trust this file!", fname); - die("Manifest is incomplete, corrupt, or compromised"); - } - - info("Manifest signature appears to be valid"); - rsa_free(&key); -} - -static void downloadManifest(void) -{ - const char *manifestfname = "updates/manifest.txt"; - const char *manifestsigfname = "updates/manifest.txt.sig"; - downloadURL("manifest.txt", manifestfname); - downloadURL("manifest.txt.sig", manifestsigfname); - verifySignature(manifestfname, manifestsigfname, PUBLICKEY_FNAME); - parseManifest(manifestfname); -} - -static void upgradeSelfAndRestart(const char *argv0) NEVER_RETURNS; -static void upgradeSelfAndRestart(const char *argv0) -{ - const char *tempfname = "origUpdater"; - const char *why = NULL; - FILE *in = NULL; - FILE *out = NULL; - - /* unix replaces the process with execl(), but Windows needs to wait for the parent to terminate. */ - #ifdef _WIN32 - DWORD ppid = 0; - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (h) { - const DWORD myPid = GetCurrentProcessId(); - PROCESSENTRY32 pe; - memset(&pe, '\0', sizeof (pe)); - pe.dwSize = sizeof(PROCESSENTRY32); - - if (Process32First(h, &pe)) { - do { - if (pe.th32ProcessID == myPid) { - ppid = pe.th32ParentProcessID; - break; - } - } while (Process32Next(h, &pe)); - } - CloseHandle(h); - } - if (!ppid) { - die("Can't determine parent process id"); - } - windowsWaitForProcessToDie(ppid); - #endif - - in = fopen(argv0, "rb"); - if (!in) { - die("Can't open self for input while upgrading updater"); - } - - remove(tempfname); - if (rename(options.updateself, tempfname) == -1) { - die("Can't rename original while upgrading updater"); - } - - out = fopen(options.updateself, "wb"); - if (!out) { - die("Can't open file for output while upgrading updater"); - } - - while (!feof(in) && !why) { - char buf[512]; - const size_t br = fread(buf, 1, sizeof (buf), in); - if (br > 0) { - if (fwrite(buf, br, 1, out) != 1) { - why = "write failure while upgrading updater"; - } - } else if (ferror(in)) { - why = "read failure while upgrading updater"; - } - } - - fclose(in); - - if ((fclose(out) == EOF) && (!why)) { - why = "close failure while upgrading updater"; - } - - if (why) { - remove(options.updateself); - rename(tempfname, options.updateself); - die(why); - } - - remove(tempfname); - - chmod(options.updateself, 0777); - - if (options.waitforprocess) { - char pidstr[64]; - snprintf(pidstr, sizeof (pidstr), PIDFMT, (PIDFMTCAST) options.waitforprocess); - launchProcess(options.updateself, options.updateself, "--waitpid", pidstr, NULL); - } else { - launchProcess(options.updateself, options.updateself, NULL); - } - die("Failed to relaunch upgraded updater"); -} - -static const char *justFilename(const char *path) -{ - const char *fname = strrchr(path, '/'); - return fname ? fname + 1 : path; -} - -static void hashFile(const char *fname, unsigned char *sha256) -{ - int rc = 0; - unsigned long hashlen = 32; - if ((rc = hash_file(sha256_hash_index, fname, sha256, &hashlen)) != CRYPT_OK) { - infof("hash_file failed for '%s': %s", fname, error_to_string(rc)); - die("Can't hash file"); - } -} - -static int fileHashMatches(const char *fname, const unsigned char *wanted) -{ - unsigned char sha256[32]; - hashFile(fname, sha256); - return (memcmp(sha256, wanted, 32) == 0); -} - -static int fileNeedsUpdate(const ManifestItem *item) -{ - if (item->len != fileLength(item->fname)) { - infof("Update '%s', file size is different", item->fname); - return 1; /* obviously different. */ - } else if (!fileHashMatches(item->fname, item->sha256)) { - infof("Update '%s', file sha256 is different", item->fname); - return 1; - } - - infof("Don't update '%s', the file is already up to date", item->fname); - return 0; -} - -static void downloadFile(const ManifestItem *item) -{ - const char *outpath = "updates/downloads/"; - const size_t len = strlen(outpath) + strlen(item->fname) + 1; - char *to = (char *) alloca(len); - if (!to) { - outOfMemory(); - } - - snprintf(to, len, "%s%s", outpath, item->fname); - - if ((item->len == fileLength(to)) && fileHashMatches(to, item->sha256)) { - infof("Already downloaded '%s', not getting again", item->fname); - } else { - downloadURL(item->fname, to); - if ((item->len != fileLength(to)) || !fileHashMatches(to, item->sha256)) { - die("Download is incorrect or corrupted"); - } - } -} - -static int downloadUpdates(void) -{ - int updatesAvailable = 0; - ManifestItem *item; - for (item = manifest; item != NULL; item = item->next) { - item->update = fileNeedsUpdate(item); - if (item->update) { - updatesAvailable = 1; - downloadFile(item); - } - } - return updatesAvailable; -} - -static void maybeUpdateSelf(const char *argv0) -{ - ManifestItem *item; - - /* !!! FIXME: this needs to be a different string on macOS. */ - const char *fname = justFilename(argv0); - - for (item = manifest; item != NULL; item = item->next) { - if (strcasecmp(item->fname, fname) == 0) { - if (fileNeedsUpdate(item)) { - const char *outpath = "updates/downloads/"; - const size_t len = strlen(outpath) + strlen(item->fname) + 1; - char *to = (char *) alloca(len); - if (!to) { - outOfMemory(); - } - snprintf(to, len, "%s%s", outpath, item->fname); - info("Have to upgrade the updater"); - downloadFile(item); - chmod(to, 0777); - - if (options.waitforprocess) { - char pidstr[64]; - snprintf(pidstr, sizeof (pidstr), PIDFMT, (PIDFMTCAST) options.waitforprocess); - launchProcess(to, to, "--updateself", argv0, "--waitpid", pidstr, NULL); - } else { - launchProcess(to, to, "--updateself", argv0, NULL); - } - die("Failed to initially launch upgraded updater"); - } - break; /* done in any case. */ - } - } -} - -static void installUpdatedFile(const ManifestItem *item) -{ - const char *basepath = "updates/downloads/"; - const size_t len = strlen(basepath) + strlen(item->fname) + 1; - char *downloadPath = (char *) alloca(len); - if (!downloadPath) { - outOfMemory(); - } - - snprintf(downloadPath, len, "%s%s", basepath, item->fname); - - infof("Moving file for update: '%s' -> '%s'", downloadPath, item->fname); - buildParentDirs(item->fname); - if (rename(downloadPath, item->fname) == -1) { - die("Failed to move updated file to final position"); - } -} - -static void applyUpdates(void) -{ - FILE *io; - ManifestItem *item; - for (item = manifest; item != NULL; item = item->next) { - if (!item->update) { - continue; - } - - io = fopen(item->fname, "rb"); - if (io != NULL) { - static int rollbackIndex = 0; - char rollbackPath[64]; - fclose(io); - item->rollback = ++rollbackIndex; - snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", rollbackIndex); - infof("Moving file for rollback: '%s' -> '%s'", item->fname, rollbackPath); - remove(rollbackPath); - if (rename(item->fname, rollbackPath) == -1) { - die("failed to move to rollback dir"); - } - } - - installUpdatedFile(item); - } -} - - -static void waitToApplyUpdates(void) -{ - if (options.waitforprocess) { - infof("Waiting for pid " PIDFMT " to die...", (PIDFMTCAST) options.waitforprocess); - { - #ifdef _WIN32 - windowsWaitForProcessToDie(options.waitforprocess); - #else - /* The parent opens a pipe on fd 3, and then forgets about it. We block - on a read to that pipe here. When the game process quits (and the - OS forcibly closes the pipe), we will unblock. Then we can loop on - kill() until the process is truly gone. */ - int x = 0; - struct timespec req; - req.tv_sec = 0; - req.tv_nsec = 100000000; - read(3, &x, sizeof (x)); - info("Pipe has closed, waiting for process to fully go away now."); - while (kill(options.waitforprocess, 0) == 0) { - nanosleep(&req, NULL); - } - #endif - } - info("pid is gone, continuing"); - } -} - -static void deleteRollbacks(void) -{ - ManifestItem *item; - for (item = manifest; item != NULL; item = item->next) { - if (item->rollback) { - char rollbackPath[64]; - snprintf(rollbackPath, sizeof (rollbackPath), "updates/rollbacks/%d", item->rollback); - infof("delete rollback: %s", rollbackPath); - remove(rollbackPath); - } - } -} - -static void chdirToBasePath(const char *argv0) -{ - const char *fname = justFilename(argv0); - size_t len; - char *buf; - - if (fname == argv0) { /* no path? Assume we're already there. */ - return; - } - - len = ((size_t) (fname - argv0)) - 1; - buf = (char *) alloca(len); - if (!buf) { - outOfMemory(); - } - - memcpy(buf, argv0, len); - buf[len] = '\0'; - if (chdir(buf) == -1) { - infof("base path is '%s'", buf); - die("chdir to base path failed"); - } -} - -int main(int argc, char **argv) -{ - #ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); /* don't trigger signal when fd3 closes */ - #endif - - logfile = stdout; - chdirToBasePath(argv[0]); - - makeDir("updates"); - makeDir("updates/downloads"); - makeDir("updates/rollbacks"); - - logfile = fopen("updates/updater-log.txt", "a"); - if (!logfile) { - logfile = stdout; - } - - infof("Updater starting, %s", timestamp()); - - parseArgv(argc, argv); - - /* set up crypto */ - ltc_mp = tfm_desc; - sha256_hash_index = register_hash(&sha256_desc); - if (sha256_hash_index == -1) { - die("Failed to register sha256 hasher"); - } - - /* if we have downloaded a new updater and restarted with that binary, - replace the original updater and restart again in the right place. */ - if (options.updateself) { - upgradeSelfAndRestart(argv[0]); - } - - prepHttpLib(); - - downloadManifest(); /* see if we need an update at all. */ - - maybeUpdateSelf(argv[0]); /* might relaunch if there's an updater upgrade. */ - - if (!downloadUpdates()) { - info("Nothing needs updating, so we're done here!"); - } else { - waitToApplyUpdates(); - applyUpdates(); - deleteRollbacks(); - info("You are now up to date!"); - } - - freeManifest(); - shutdownHttpLib(); - - unregister_hash(&sha256_desc); - - infof("Updater ending, %s", timestamp()); - - return 0; -} - diff --git a/code/autoupdater/rsa_tools/.gitignore b/code/autoupdater/rsa_tools/.gitignore deleted file mode 100644 index 8182fafc..00000000 --- a/code/autoupdater/rsa_tools/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -crypt-*.tar.bz2 -tfm-*.tar.xz -libtomcrypt-* -tomsfastmath-* -rsa_make_keys -rsa_sign -rsa_verify -*.exe diff --git a/code/autoupdater/rsa_tools/build-libtom-unix.sh b/code/autoupdater/rsa_tools/build-libtom-unix.sh deleted file mode 100755 index bbd86312..00000000 --- a/code/autoupdater/rsa_tools/build-libtom-unix.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -TFMVER=0.13.1 -LTCVER=1.17 -set -e - -OSTYPE=`uname -s` -if [ "$OSTYPE" = "Linux" ]; then - NCPU=`cat /proc/cpuinfo |grep vendor_id |wc -l` - let NCPU=$NCPU+1 -elif [ "$OSTYPE" = "Darwin" ]; then - NCPU=`sysctl -n hw.ncpu` - export CFLAGS="$CFLAGS -mmacosx-version-min=10.7 -DMAC_OS_X_VERSION_MIN_REQUIRED=1070" - export LDFLAGS="$LDFLAGS -mmacosx-version-min=10.7" -elif [ "$OSTYPE" = "SunOS" ]; then - NCPU=`/usr/sbin/psrinfo |wc -l |sed -e 's/^ *//g;s/ *$//g'` -else - NCPU=1 -fi - -if [ -z "$NCPU" ]; then - NCPU=1 -elif [ "$NCPU" = "0" ]; then - NCPU=1 -fi - -if [ ! -f tfm-$TFMVER.tar.xz ]; then - echo "Downloading TomsFastMath $TFMVER sources..." - curl -L -o tfm-$TFMVER.tar.xz https://github.com/libtom/tomsfastmath/releases/download/v$TFMVER/tfm-$TFMVER.tar.xz || exit 1 -fi - -if [ ! -f ./crypt-$LTCVER.tar.bz2 ]; then - echo "Downloading LibTomCrypt $LTCVER sources..." - curl -L -o crypt-$LTCVER.tar.bz2 https://github.com/libtom/libtomcrypt/releases/download/$LTCVER/crypt-$LTCVER.tar.bz2 || exit 1 -fi - -if [ ! -d tomsfastmath-$TFMVER ]; then - echo "Checking TomsFastMath archive hash..." - if [ "`shasum -a 256 tfm-$TFMVER.tar.xz |awk '{print $1;}'`" != "47c97a1ada3ccc9fcbd2a8a922d5859a84b4ba53778c84c1d509c1a955ac1738" ]; then - echo "Uhoh, tfm-$TFMVER.tar.xz does not have the sha256sum we expected!" - exit 1 - fi - echo "Unpacking TomsFastMath $TFMVER sources..." - tar -xJvvf ./tfm-$TFMVER.tar.xz -fi - -if [ ! -d libtomcrypt-$LTCVER ]; then - if [ "`shasum -a 256 crypt-$LTCVER.tar.bz2 |awk '{print $1;}'`" != "e33b47d77a495091c8703175a25c8228aff043140b2554c08a3c3cd71f79d116" ]; then - echo "Uhoh, crypt-$LTCVER.tar.bz2 does not have the sha256sum we expected!" - exit 1 - fi - echo "Unpacking LibTomCrypt $LTCVER sources..." - tar -xjvvf ./crypt-$LTCVER.tar.bz2 -fi - -echo -echo -echo "Will use make -j$NCPU. If this is wrong, check NCPU at top of script." -echo -echo - -set -e -set -x - -# Some compilers can't handle the ROLC inline asm; just turn it off. -cd tomsfastmath-$TFMVER -make -j$NCPU -cd .. - -export CFLAGS="$CFLAGS -DTFM_DESC -DLTC_NO_ROLC -I ../tomsfastmath-$TFMVER/src/headers" -cd libtomcrypt-$LTCVER -make -j$NCPU -cd .. - -set +x -echo "All done." - -# end of build-libtom-unix.sh ... - diff --git a/code/autoupdater/rsa_tools/build-rsa-tools.sh b/code/autoupdater/rsa_tools/build-rsa-tools.sh deleted file mode 100755 index dda9396c..00000000 --- a/code/autoupdater/rsa_tools/build-rsa-tools.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -export TFMDIR="tomsfastmath-0.13.1" -export LTCDIR="libtomcrypt-1.17" - -OSTYPE=`uname -s` -if [ -z "$CC" ]; then - if [ "`uname -o`" = "Cygwin" ]; then - export CC=/usr/bin/i686-w64-mingw32-gcc - else - export CC=cc - fi -fi - -function build { - if [ "$OSTYPE" = "Darwin" ]; then - $CC -mmacosx-version-min=10.7 -DMAC_OS_X_VERSION_MIN_REQUIRED=1070 -I $TFMDIR/src/headers -I $LTCDIR/src/headers -o "$1" -Wall -O3 "$1.c" rsa_common.c $LTCDIR/libtomcrypt.a $TFMDIR/libtfm.a - else - $CC -I $TFMDIR/src/headers -I $LTCDIR/src/headers -o "$1" -Wall -O3 "$1.c" rsa_common.c $LTCDIR/libtomcrypt.a $TFMDIR/libtfm.a - fi -} - -set -e -set -x - -./build-libtom-unix.sh -build rsa_make_keys -build rsa_sign -build rsa_verify - -set +x -echo "rsa_tools are compiled!" - diff --git a/code/autoupdater/rsa_tools/rsa_common.c b/code/autoupdater/rsa_tools/rsa_common.c deleted file mode 100644 index d0a10a0a..00000000 --- a/code/autoupdater/rsa_tools/rsa_common.c +++ /dev/null @@ -1,61 +0,0 @@ -#include "rsa_common.h" - -void fail(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fputs("\n", stderr); - fflush(stderr); - exit(1); -} - -void write_file(const char *fname, const void *buf, const unsigned long len) -{ - FILE *io = fopen(fname, "wb"); - if (!io) { - fail("Can't open '%s' for writing: %s", fname, strerror(errno)); - } - - if (fwrite(buf, len, 1, io) != 1) { - fail("Couldn't write '%s': %s", fname, strerror(errno)); - } - - if (fclose(io) != 0) { - fail("Couldn't flush '%s' to disk: %s", fname, strerror(errno)); - } -} - -void read_file(const char *fname, void *buf, unsigned long *len) -{ - ssize_t br; - FILE *io = fopen(fname, "rb"); - if (!io) { - fail("Can't open '%s' for reading: %s", fname, strerror(errno)); - } - - br = fread(buf, 1, *len, io); - if (ferror(io)) { - fail("Couldn't read '%s': %s", fname, strerror(errno)); - } else if (!feof(io)) { - fail("Buffer too small to read '%s'", fname); - } - fclose(io); - - *len = (unsigned long) br; -} - -void read_rsakey(rsa_key *key, const char *fname) -{ - unsigned char buf[4096]; - unsigned long len = sizeof (buf); - int rc; - - read_file(fname, buf, &len); - - if ((rc = rsa_import(buf, len, key)) != CRYPT_OK) { - fail("rsa_import for '%s' failed: %s", fname, error_to_string(rc)); - } -} - diff --git a/code/autoupdater/rsa_tools/rsa_common.h b/code/autoupdater/rsa_tools/rsa_common.h deleted file mode 100644 index 48695522..00000000 --- a/code/autoupdater/rsa_tools/rsa_common.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef _INCL_RSA_COMMON_H_ -#define _INCL_RSA_COMMON_H_ 1 - -#include -#include -#include - -#define TFM_DESC -#define LTC_NO_ROLC -#include "tomcrypt.h" - -#define SALT_LEN 8 - -#if defined(__GNUC__) || defined(__clang__) -#define NEVER_RETURNS __attribute__((noreturn)) -#define PRINTF_FUNC(fmtargnum, dotargnum) __attribute__ (( format( __printf__, fmtargnum, dotargnum ))) -#else -#define NEVER_RETURNS -#define PRINTF_FUNC(fmtargnum, dotargnum) -#endif - -void fail(const char *fmt, ...) NEVER_RETURNS PRINTF_FUNC(1, 2); -void write_file(const char *fname, const void *buf, const unsigned long len); -void read_file(const char *fname, void *buf, unsigned long *len); -void read_rsakey(rsa_key *key, const char *fname); - -#endif - -/* end of rsa_common.h ... */ - diff --git a/code/autoupdater/rsa_tools/rsa_make_keys.c b/code/autoupdater/rsa_tools/rsa_make_keys.c deleted file mode 100644 index a7f801c0..00000000 --- a/code/autoupdater/rsa_tools/rsa_make_keys.c +++ /dev/null @@ -1,45 +0,0 @@ -#include "rsa_common.h" - -static void write_rsakey(rsa_key *key, const int type, const char *fname) -{ - unsigned char buf[4096]; - unsigned long len = sizeof (buf); - int rc; - - if ((rc = rsa_export(buf, &len, type, key)) != CRYPT_OK) { - fail("rsa_export for '%s' failed: %s", fname, error_to_string(rc)); - } - write_file(fname, buf, len); -} - -int main(int argc, char **argv) -{ - int rc = 0; - prng_state prng; - int prng_index; - rsa_key key; - - ltc_mp = tfm_desc; - prng_index = register_prng(&sprng_desc); /* (fortuna_desc is a good choice if your platform's PRNG sucks.) */ - - if (prng_index == -1) { - fail("Failed to register a RNG"); - } - - if ((rc = rng_make_prng(128, prng_index, &prng, NULL)) != CRYPT_OK) { - fail("rng_make_prng failed: %s", error_to_string(rc)); - } - - if ((rc = rsa_make_key(&prng, prng_index, 256, 65537, &key)) != CRYPT_OK) { - fail("rng_make_key failed: %s", error_to_string(rc)); - } - - write_rsakey(&key, PK_PRIVATE, "privatekey.bin"); - write_rsakey(&key, PK_PUBLIC, "publickey.bin"); - - rsa_free(&key); - - return 0; -} - -/* end of rsa_make_keys.c ... */ diff --git a/code/autoupdater/rsa_tools/rsa_sign.c b/code/autoupdater/rsa_tools/rsa_sign.c deleted file mode 100644 index 5eec24dd..00000000 --- a/code/autoupdater/rsa_tools/rsa_sign.c +++ /dev/null @@ -1,75 +0,0 @@ -#include "rsa_common.h" - -static void sign_file(const char *fname, rsa_key *key, prng_state *prng, const int prng_index, const int hash_index) -{ - const size_t sigfnamelen = strlen(fname) + 5; - char *sigfname = (char *) malloc(sigfnamelen); - unsigned char hash[256]; - unsigned long hashlen = sizeof (hash); - unsigned char sig[1024]; - unsigned long siglen = sizeof (sig); - int rc = 0; - int status = 0; - - if (!sigfname) { - fail("out of memory"); - } - - if ((rc = hash_file(hash_index, fname, hash, &hashlen)) != CRYPT_OK) { - fail("hash_file for '%s' failed: %s", fname, error_to_string(rc)); - } - - if ((rc = rsa_sign_hash(hash, hashlen, sig, &siglen, prng, prng_index, hash_index, SALT_LEN, key)) != CRYPT_OK) { - fail("rsa_sign_hash for '%s' failed: %s", fname, error_to_string(rc)); - } - - if ((rc = rsa_verify_hash(sig, siglen, hash, hashlen, hash_index, SALT_LEN, &status, key)) != CRYPT_OK) { - fail("rsa_verify_hash for '%s' failed: %s", fname, error_to_string(rc)); - } - - if (!status) { - fail("Generated signature isn't valid! Bug in the program!"); - } - - snprintf(sigfname, sigfnamelen, "%s.sig", fname); - write_file(sigfname, sig, siglen); - free(sigfname); -} - -int main(int argc, char **argv) -{ - int rc = 0; - prng_state prng; - int prng_index, hash_index; - rsa_key key; - int i; - - ltc_mp = tfm_desc; - - prng_index = register_prng(&sprng_desc); /* (fortuna_desc is a good choice if your platform's PRNG sucks.) */ - if (prng_index == -1) { - fail("Failed to register a RNG"); - } - - hash_index = register_hash(&sha256_desc); - if (hash_index == -1) { - fail("Failed to register sha256 hasher"); - } - - if ((rc = rng_make_prng(128, prng_index, &prng, NULL)) != CRYPT_OK) { - fail("rng_make_prng failed: %s", error_to_string(rc)); - } - - read_rsakey(&key, "privatekey.bin"); - - for (i = 1; i < argc; i++) { - sign_file(argv[i], &key, &prng, prng_index, hash_index); - } - - rsa_free(&key); - - return 0; -} - -/* end of rsa_sign.c ... */ - diff --git a/code/autoupdater/rsa_tools/rsa_verify.c b/code/autoupdater/rsa_tools/rsa_verify.c deleted file mode 100644 index 09abfb24..00000000 --- a/code/autoupdater/rsa_tools/rsa_verify.c +++ /dev/null @@ -1,60 +0,0 @@ -#include "rsa_common.h" - -static void verify_file(const char *fname, rsa_key *key, const int hash_index) -{ - const size_t sigfnamelen = strlen(fname) + 5; - char *sigfname = (char *) malloc(sigfnamelen); - unsigned char hash[256]; - unsigned long hashlen = sizeof (hash); - unsigned char sig[1024]; - unsigned long siglen = sizeof (sig); - int status = 0; - int rc = 0; - - if (!sigfname) { - fail("out of memory"); - } - - snprintf(sigfname, sigfnamelen, "%s.sig", fname); - read_file(sigfname, sig, &siglen); - free(sigfname); - - if ((rc = hash_file(hash_index, fname, hash, &hashlen)) != CRYPT_OK) { - fail("hash_file for '%s' failed: %s", fname, error_to_string(rc)); - } - - if ((rc = rsa_verify_hash(sig, siglen, hash, hashlen, hash_index, SALT_LEN, &status, key)) != CRYPT_OK) { - fail("rsa_verify_hash for '%s' failed: %s", fname, error_to_string(rc)); - } - - if (!status) { - fail("Invalid signature for '%s'! Don't trust this file!", fname); - } -} - -int main(int argc, char **argv) -{ - int hash_index; - rsa_key key; - int i; - - ltc_mp = tfm_desc; - - hash_index = register_hash(&sha256_desc); - if (hash_index == -1) { - fail("Failed to register sha256 hasher"); - } - - read_rsakey(&key, "publickey.bin"); - - for (i = 1; i < argc; i++) { - verify_file(argv[i], &key, hash_index); - } - - rsa_free(&key); - - return 0; -} - -/* end of rsa_verify.c ... */ - diff --git a/code/autoupdater/rsa_tools/test-rsa-tools.sh b/code/autoupdater/rsa_tools/test-rsa-tools.sh deleted file mode 100755 index f4b4bdb0..00000000 --- a/code/autoupdater/rsa_tools/test-rsa-tools.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -if [ -f privatekey.bin ]; then - echo "move your existing keys out of the way." - exit 1 -fi - -( ./rsa_make_keys && echo "key making okay") || echo "key making NOT okay" -echo "The quick brown fox jumped over the lazy dog." >testmsg.txt -( ./rsa_sign testmsg.txt && echo "signing okay" ) || echo "signing NOT okay" -( ./rsa_verify testmsg.txt && echo "basic verifying okay" ) || echo "basic verifying NOT okay" -echo "The quick brown fox jumped over the lazy dog!" >testmsg.txt -( ./rsa_verify testmsg.txt 2>/dev/null && echo "tamper test NOT okay" ) || echo "tamper test okay" -echo "The quick brown fox jumped over the lazy dog." >testmsg.txt -( ./rsa_verify testmsg.txt && echo "reverify okay" ) || echo "reverify NOT okay" -rm -f testmsg.txt testmsg.txt.sig publickey.bin privatekey.bin - diff --git a/docs/autoupdater-readme.txt b/docs/autoupdater-readme.txt deleted file mode 100644 index 34561b7c..00000000 --- a/docs/autoupdater-readme.txt +++ /dev/null @@ -1,165 +0,0 @@ -The updater program's code is public domain. The rest of ioquake3 is not. - -The source code to the autoupdater is in the code/autoupdater directory. -There is a small piece of code in ioquake3 itself at startup, too; this is -in code/sys/sys_autoupdater.c ... - -(This is all Unix terminology, but similar approaches on Windows apply.) - -The updater is a separate program, written in C, with no dependencies on -the game. It (statically) links to libcurl and uses the C runtime, but -otherwise has no external dependencies. It has to be a single binary file -with no shared libraries. - -The basic flow looks like this: - -- The game launches as usual. -- Right after main() starts, the game creates a pipe, forks off a new process, - and execs the updater in that process. The game won't ever touch the pipe - again. It's just there to block the child app until the game terminates. -- The updater has no UI. It writes a log file. -- The updater downloads a manifest from a known URL over https://, using - libCurl. The base URL is platform-specific (it might be - https://example.com/mac/, or https://example.com/linux-x86/, whatever). - The url might have other features, like a updater version or a specific - product name, etc. - The manifest is at $BASEURL/manifest.txt -- The updater also downloads $BASEURL/manifest.txt.sig, which is a digital - signature for the manifest. It checks the manifest against this signature - and a known public RSA key; if the manifest doesn't match the signature, - the updater refuses to continue. -- The manifest looks like this: three lines per item... - -Contents/MacOS/baseq3/uix86_64.dylib -332428 -a49bbe77f8eb6c195265ea136f881f7830db58e4d8a883b27f59e1e23e396a20 - -- That's the file's path, its size in bytes, and an sha256 hash of the data. -- The file will be at this path under the base url on the webserver. -- The manifest only lists files that ever needed updating; it's not necessary - to list every file in the game's installation (unless you want to allow the - entire game to download). -- The updater will check each item in the manifest: - - Does the file not exist in the install? Needs downloading. - - Does the file have a different size? Needs downloading. - - Does the file have a different sha256sum? Needs downloading. - - Otherwise, file is up to date, leave it alone. -- If an item needs downloading, do these same checks against the file in the - download directory (if it's already there and matches, don't download again.) -- Download necessary files with libcurl, put it in a download directory. -- The downloaded file is also checked for size and sha256 vs the manifest, to - make sure there was no corruption or confusion. If a downloaded file doesn't - match what was expected, the updater aborts and will try again next time. - This could fail checksum due to i/o errors and compromised security, but - it might just be that a new version was being published and bad luck - happened, and a retry later could correct everything. -- If the updater itself needs upgrading, we deal with that first. It's - downloaded, then the updater relaunches from the downloaded binary with - a special command line. That relaunched process copies itself to the proper - location, and then relaunches _again_ to restart the normal updating - process with the new updater in its correct position. -- Once the downloads are complete and the updater itself doesn't need - upgrading, we are ready to start the normal upgrade. Since we can't replace - executables on some platforms while they are running, and swapping out a - game's data files at runtime isn't wise in general, the updater will now - block until the game terminates. It does this by reading on the pipe that - the game created when forking the updater; since the game never writes - anything to this pipe, it causes the updater to block until the pipe closes. - Since the game never deliberately closes the pipe either, it remains open - until the OS forcibly closes it as the game process terminates. Being an - unnamed pipe, it just vaporizes at this point, leaving no state that might - accidentally hang us up later, like a global semaphore or whatnot. This - technique also lets us localize the game's code changes to one small block - of C code, with no need to manage these resources elsewhere. -- As a sanity check, the updater will also kill(game_process_id, 0) until it - fails, sleeping for 100 milliseconds between each attempt, in case the - process is still being cleaned up by the OS after closing the pipe. -- Once the updater is confident the game process is gone, it will start - upgrading the appropriate files. It does this in two steps: it moves - the old file to a "rollback" directory so it's out of the way but still - available, then it moves the newly-downloaded file into place. Since these - are all simple renames and not copies, this can move fast. Any missing - parent directories are created, in case the update is adding a new file - in a directory that didn't previously exist. -- If something goes wrong at this point (file i/o error, etc), the updater - will roll back the changes by deleting the updated files, and moving the - files in the "rollback" directory back to their original locations. Then - the updater aborts. -- If nothing went wrong, the rollback files are deleted. And we are officially - up to date! The updater terminates. - - -The updater is designed to fail at any point. If a download fails, it'll -pick up and try again next time, etc. Completed downloads will remain, so it -will just need to download any missing/incomplete files. - -The server side just needs to be able to serve static files over HTTPS from -any standard Apache/nginx/whatever process. - -Failure points: -- If the updater fails when still downloading data, it just picks up on next - restart. -- If the updater fails when replacing files, it rolls back any changes it has - made. -- If the updater fails when rolling back, then running the updater again after - fixing the specific problem (disk error, etc?) will redownload and replace - any files that were left in an uncertain state. The only true point of - risk is crashing during a rollback and then having the updater bricked for - some reason, but that's an extremely small surface area, knock on wood. -- If the updater crashes or totally bricks, ioquake3 should just keep being - ioquake3. It will still launch and play, even if the updater is quietly - segfaulting in the background on startup. -- If an update bricks ioquake3 to the point where it can't run the updater, - running the updater directly should let it recover (assuming a future update - fixes the problem). -- If the download server is compromised, they would need the private key - (not stored on the download server) to alter the manifest to serve - compromised files to players. If they try to change a file or the manifest, - the updater will know to abort without updating anything. -- If the private key is compromised, we generate a new one, ship new - installers with an updated public key, and re-sign the manifest with the - new private key. Existing installations will never update again until they - do a fresh install, or at least update their copy of the public key. - - -How manifest signing works: - -Some admin will generate a public/private key with the rsa_make_keys program, -keeping the private key secret. Using the private key and the rsa_sign -program, the admin will sign the manifest, generating manifest.txt.sig. - -The public key ships with the game (adding 270 bytes to the download), the -.sig is downloaded with the manifest by the autoupdater (256 bytes extra -download), then the autoupdater checks the manifest against the signature -with the public key. if the signature isn't valid (the manifest was tampered -with or corrupt), the autoupdater refuses to continue. - -If the manifest is to be trusted, it lists sha256 checksums for every file to -download, so there's no need to sign every file; if they can't tamper with the -manifest, they can't tamper with any other file to be updated since the file's -listed sha256 won't match. - -If the private key is compromised, we generate new keys and ship new -installers, so new installations will be able to update but existing ones -will need to do a new install to keep getting updates. Don't let the private -key get compromised. The private key doesn't go on a public server. Maybe it -doesn't even live on the admin's laptop hard drive. - -If the download server is compromised and serving malware, the autoupdater -will reject it outright if they haven't compromised the private key, generated -a new manifest, and signed it with the private key. - - - -Items to consider for future revisions: -- Maybe put a limit on the number manifest downloads, so we only check once - every hour? Every day? -- Channels? Stable (what everyone gets by default), Nightly (once a day), - Experimental (some other work-in-progress branch), Bloody (literally the - latest commit). -- Let mods update, separate from the main game? - -Questions? Ask Ryan: icculus@icculus.org - ---ryan. -