diff --git a/Makefile b/Makefile index f0d6251e..4714655c 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ endif ifndef BUILD_RENDERER_OPENGL2 BUILD_RENDERER_OPENGL2= endif +ifndef BUILD_AUTOUPDATER # DON'T build unless you mean to! + BUILD_AUTOUPDATER=0 +endif ############################################################################# # @@ -228,6 +231,10 @@ ifndef USE_YACC USE_YACC=0 endif +ifndef USE_AUTOUPDATER # DON'T include unless you mean to! +USE_AUTOUPDATER=0 +endif + ifndef DEBUG_CFLAGS DEBUG_CFLAGS=-ggdb -O0 endif @@ -262,6 +269,7 @@ LBURGDIR=$(MOUNT_DIR)/tools/lcc/lburg Q3CPPDIR=$(MOUNT_DIR)/tools/lcc/cpp Q3LCCETCDIR=$(MOUNT_DIR)/tools/lcc/etc Q3LCCSRCDIR=$(MOUNT_DIR)/tools/lcc/src +AUTOUPDATERSRCDIR=$(MOUNT_DIR)/autoupdater LOKISETUPDIR=misc/setup NSISDIR=misc/nsis SDLHDIR=$(MOUNT_DIR)/SDL2 @@ -269,29 +277,30 @@ LIBSDIR=$(MOUNT_DIR)/libs bin_path=$(shell which $(1) 2> /dev/null) +# The autoupdater uses curl, so figure out its flags no matter what. # We won't need this if we only build the server -ifneq ($(BUILD_CLIENT),0) - # set PKG_CONFIG_PATH to influence this, e.g. - # PKG_CONFIG_PATH=/opt/cross/i386-mingw32msvc/lib/pkgconfig - ifneq ($(call bin_path, pkg-config),) - CURL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags libcurl) - CURL_LIBS ?= $(shell pkg-config --silence-errors --libs libcurl) - OPENAL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags openal) - OPENAL_LIBS ?= $(shell pkg-config --silence-errors --libs openal) - SDL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags sdl2|sed 's/-Dmain=SDL_main//') - SDL_LIBS ?= $(shell pkg-config --silence-errors --libs sdl2) - FREETYPE_CFLAGS ?= $(shell pkg-config --silence-errors --cflags freetype2) - else - # assume they're in the system default paths (no -I or -L needed) - CURL_LIBS ?= -lcurl - OPENAL_LIBS ?= -lopenal - endif - # Use sdl2-config if all else fails - ifeq ($(SDL_CFLAGS),) - ifneq ($(call bin_path, sdl2-config),) - SDL_CFLAGS ?= $(shell sdl2-config --cflags) - SDL_LIBS ?= $(shell sdl2-config --libs) - endif + +# set PKG_CONFIG_PATH to influence this, e.g. +# PKG_CONFIG_PATH=/opt/cross/i386-mingw32msvc/lib/pkgconfig +ifneq ($(call bin_path, pkg-config),) + CURL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags libcurl) + CURL_LIBS ?= $(shell pkg-config --silence-errors --libs libcurl) + OPENAL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags openal) + OPENAL_LIBS ?= $(shell pkg-config --silence-errors --libs openal) + SDL_CFLAGS ?= $(shell pkg-config --silence-errors --cflags sdl2|sed 's/-Dmain=SDL_main//') + SDL_LIBS ?= $(shell pkg-config --silence-errors --libs sdl2) + FREETYPE_CFLAGS ?= $(shell pkg-config --silence-errors --cflags freetype2) +else + # assume they're in the system default paths (no -I or -L needed) + CURL_LIBS ?= -lcurl + OPENAL_LIBS ?= -lopenal +endif + +# Use sdl2-config if all else fails +ifeq ($(SDL_CFLAGS),) + ifneq ($(call bin_path, sdl2-config),) + SDL_CFLAGS ?= $(shell sdl2-config --cflags) + SDL_LIBS ?= $(shell sdl2-config --libs) endif endif @@ -367,6 +376,7 @@ ifneq (,$(findstring "$(PLATFORM)", "linux" "gnu_kfreebsd" "kfreebsd-gnu" "gnu") THREAD_LIBS=-lpthread LIBS=-ldl -lm + AUTOUPDATER_LIBS += -ldl CLIENT_LIBS=$(SDL_LIBS) RENDERER_LIBS = $(SDL_LIBS) -lGL @@ -594,6 +604,8 @@ ifdef MINGW endif LIBS= -lws2_32 -lwinmm -lpsapi + AUTOUPDATER_LIBS += -lwininet + # clang 3.4 doesn't support this ifneq ("$(CC)", $(findstring "$(CC)", "clang" "clang++")) CLIENT_LDFLAGS += -mwindows @@ -834,6 +846,8 @@ ifeq ($(PLATFORM),irix64) SHLIBLDFLAGS=-shared LIBS=-ldl -lm -lgen + AUTOUPDATER_LIBS += -ldl + # FIXME: The X libraries probably aren't necessary? CLIENT_LIBS=-L/usr/X11/$(LIB) $(SDL_LIBS) \ -lX11 -lXext -lm @@ -888,6 +902,7 @@ ifeq ($(PLATFORM),sunos) THREAD_LIBS=-lpthread LIBS=-lsocket -lnsl -ldl -lm + AUTOUPDATER_LIBS += -ldl BOTCFLAGS=-O0 @@ -986,6 +1001,16 @@ ifneq ($(BUILD_GAME_QVM),0) endif endif +ifneq ($(BUILD_AUTOUPDATER),0) + # PLEASE NOTE that if you run an exe on Windows Vista or later + # with "setup", "install", "update" or other related terms, it + # will unconditionally trigger a UAC prompt, and in the case of + # ioq3 calling CreateProcess() on it, it'll just fail immediately. + # So don't call this thing "autoupdater" here! + AUTOUPDATER_BIN := autosyncerator$(FULLBINEXT) + TARGETS += $(B)/$(AUTOUPDATER_BIN) +endif + ifeq ($(USE_OPENAL),1) CLIENT_CFLAGS += -DUSE_OPENAL ifeq ($(USE_OPENAL_DLOPEN),1) @@ -1086,6 +1111,11 @@ ifeq ($(USE_FREETYPE),1) RENDERER_LIBS += $(FREETYPE_LIBS) endif +ifeq ($(USE_AUTOUPDATER),1) + CLIENT_CFLAGS += -DUSE_AUTOUPDATER -DAUTOUPDATER_BIN=\\\"$(AUTOUPDATER_BIN)\\\" + SERVER_CFLAGS += -DUSE_AUTOUPDATER -DAUTOUPDATER_BIN=\\\"$(AUTOUPDATER_BIN)\\\" +endif + ifeq ("$(CC)", $(findstring "$(CC)", "clang" "clang++")) BASE_CFLAGS += -Qunused-arguments endif @@ -1317,6 +1347,9 @@ endif @echo " CLIENT_LIBS:" $(call print_wrapped, $(CLIENT_LIBS)) @echo "" + @echo " AUTOUPDATER_LIBS:" + $(call print_wrapped, $(AUTOUPDATER_LIBS)) + @echo "" @echo " Output:" $(call print_list, $(NAKED_TARGETS)) @echo "" @@ -1342,6 +1375,7 @@ endif makedirs: @if [ ! -d $(BUILD_DIR) ];then $(MKDIR) $(BUILD_DIR);fi @if [ ! -d $(B) ];then $(MKDIR) $(B);fi + @if [ ! -d $(B)/autoupdater ];then $(MKDIR) $(B)/autoupdater;fi @if [ ! -d $(B)/client ];then $(MKDIR) $(B)/client;fi @if [ ! -d $(B)/client/opus ];then $(MKDIR) $(B)/client/opus;fi @if [ ! -d $(B)/client/vorbis ];then $(MKDIR) $(B)/client/vorbis;fi @@ -1561,6 +1595,27 @@ $(Q3ASM): $(Q3ASMOBJ) $(Q)$(TOOLS_CC) $(TOOLS_CFLAGS) $(TOOLS_LDFLAGS) -o $@ $^ $(TOOLS_LIBS) +############################################################################# +# AUTOUPDATER +############################################################################# + +define DO_AUTOUPDATER_CC +$(echo_cmd) "AUTOUPDATER_CC $<" +$(Q)$(CC) $(CFLAGS) $(CURL_CFLAGS) -o $@ -c $< +endef + +Q3AUTOUPDATEROBJ = \ + $(B)/autoupdater/autoupdater.o \ + $(B)/autoupdater/sha256.o + +$(B)/autoupdater/%.o: $(AUTOUPDATERSRCDIR)/%.c + $(DO_AUTOUPDATER_CC) + +$(B)/$(AUTOUPDATER_BIN): $(Q3AUTOUPDATEROBJ) + $(echo_cmd) "AUTOUPDATER_LD $@" + $(Q)$(CC) $(LDFLAGS) -o $@ $(Q3AUTOUPDATEROBJ) $(AUTOUPDATER_LIBS) + + ############################################################################# # CLIENT/SERVER ############################################################################# @@ -1664,6 +1719,7 @@ Q3OBJ = \ $(B)/client/sdl_snd.o \ \ $(B)/client/con_log.o \ + $(B)/client/sys_autoupdater.o \ $(B)/client/sys_main.o ifdef MINGW @@ -2200,6 +2256,7 @@ Q3DOBJ = \ $(B)/ded/null_snddma.o \ \ $(B)/ded/con_log.o \ + $(B)/ded/sys_autoupdater.o \ $(B)/ded/sys_main.o ifeq ($(ARCH),x86) diff --git a/autoupdater-readme.txt b/autoupdater-readme.txt new file mode 100644 index 00000000..1353f4f0 --- /dev/null +++ b/autoupdater-readme.txt @@ -0,0 +1,125 @@ +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 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 manifest is at $BASEURL/manifest.txt +- The manifest looks like this: three lines per file... + +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). + + +Items to consider for future revisions: +- GPG sign the manifest; if we can be confident that the manifest isn't + compromised, then the sha256 hashes of each file it contains should protect + the rest of the process. As it currently stands, we trust the download + server isn't compromised. +- 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. + diff --git a/code/autoupdater/autoupdater.c b/code/autoupdater/autoupdater.c new file mode 100644 index 00000000..eb7937ee --- /dev/null +++ b/code/autoupdater/autoupdater.c @@ -0,0 +1,977 @@ +/* +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 + +#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 + +#include "sha256.h" + + +#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)) +#else +#define NEVER_RETURNS +#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; + +#define SDL_PRINTF_VARARG_FUNC( fmtargnumber ) __attribute__ (( format( __printf__, fmtargnumber, fmtargnumber+1 ))) + +static void info(const char *str) +{ + fputs(str, logfile); + fputs("\n", logfile); + fflush(logfile); +} + +static void infof(const char *fmt, ...) +#if defined(__GNUC__) || defined(__clang__) +__attribute__ (( format( __printf__, 1, 2 ))) +#endif +; + +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, uint8 *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 downloadManifest(void) +{ + const char *manifestfname = "updates/manifest.txt"; + downloadURL("manifest.txt", manifestfname); + /* !!! FIXME: verify manifest download is complete... */ + 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) +{ + SHA256_CTX sha256ctx; + uint8 buf[512]; + FILE *io; + + io = fopen(fname, "rb"); + if (!io) { + die("Failed to open file for hashing"); + } + + sha256_init(&sha256ctx); + do { + size_t br = fread(buf, 1, sizeof (buf), io); + if (br > 0) { + sha256_update(&sha256ctx, buf, br); + } + if (ferror(io)) { + die("Error reading file for hashing"); + } + } while (!feof(io)); + + fclose(io); + + sha256_final(&sha256ctx, sha256); +} + +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; + read(3, &x, sizeof (x)); + info("Pipe has closed, waiting for process to fully go away now."); + while (kill(options.waitforprocess, 0) == 0) { + usleep(100000); + } + #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); + + /* 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(); + + infof("Updater ending, %s", timestamp()); + + return 0; +} + diff --git a/code/autoupdater/sha256.c b/code/autoupdater/sha256.c new file mode 100644 index 00000000..0861860f --- /dev/null +++ b/code/autoupdater/sha256.c @@ -0,0 +1,156 @@ +/********************************************************************* +* Filename: sha256.c +* Author: Brad Conte (brad AT bradconte.com) +* Copyright: +* Disclaimer: This code is presented "as is" without any guarantees. +* Details: Implementation of the SHA-256 hashing algorithm. + SHA-256 is one of the three algorithms in the SHA2 + specification. The others, SHA-384 and SHA-512, are not + offered in this implementation. + Algorithm specification can be found here: + * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf + This implementation uses little endian byte order. +*********************************************************************/ + +/*************************** HEADER FILES ***************************/ +#include +#include +#include "sha256.h" + +/****************************** MACROS ******************************/ +#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b)))) +#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b)))) + +#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22)) +#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25)) +#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3)) +#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10)) + +/**************************** VARIABLES *****************************/ +static const uint32 k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +/*********************** FUNCTION DEFINITIONS ***********************/ +void sha256_transform(SHA256_CTX *ctx, const uint8 data[]) +{ + uint32 a, b, c, d, e, f, g, h, i, j, t1, t2, m[64]; + + for (i = 0, j = 0; i < 16; ++i, j += 4) + m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]); + for ( ; i < 64; ++i) + m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16]; + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + f = ctx->state[5]; + g = ctx->state[6]; + h = ctx->state[7]; + + for (i = 0; i < 64; ++i) { + t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i]; + t2 = EP0(a) + MAJ(a,b,c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +void sha256_init(SHA256_CTX *ctx) +{ + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; +} + +void sha256_update(SHA256_CTX *ctx, const uint8 data[], size_t len) +{ + size_t i; + + for (i = 0; i < len; ++i) { + ctx->data[ctx->datalen] = data[i]; + ctx->datalen++; + if (ctx->datalen == 64) { + sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +void sha256_final(SHA256_CTX *ctx, uint8 hash[]) +{ + uint32 i = ctx->datalen; + + // Pad whatever data is left in the buffer. + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) + ctx->data[i++] = 0x00; + } + else { + ctx->data[i++] = 0x80; + while (i < 64) + ctx->data[i++] = 0x00; + sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + // Append to the padding the total message's length in bits and transform. + ctx->bitlen += ctx->datalen * 8; + ctx->data[63] = ctx->bitlen; + ctx->data[62] = ctx->bitlen >> 8; + ctx->data[61] = ctx->bitlen >> 16; + ctx->data[60] = ctx->bitlen >> 24; + ctx->data[59] = ctx->bitlen >> 32; + ctx->data[58] = ctx->bitlen >> 40; + ctx->data[57] = ctx->bitlen >> 48; + ctx->data[56] = ctx->bitlen >> 56; + sha256_transform(ctx, ctx->data); + + // Since this implementation uses little endian byte ordering and SHA uses big endian, + // reverse all the bytes when copying the final state to the output hash. + for (i = 0; i < 4; ++i) { + hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff; + hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff; + hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff; + hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff; + hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff; + hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff; + hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff; + hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff; + } +} diff --git a/code/autoupdater/sha256.h b/code/autoupdater/sha256.h new file mode 100644 index 00000000..fc9a09a9 --- /dev/null +++ b/code/autoupdater/sha256.h @@ -0,0 +1,42 @@ +/********************************************************************* +* Filename: sha256.h +* Author: Brad Conte (brad AT bradconte.com) +* Copyright: +* Disclaimer: This code is presented "as is" without any guarantees. +* Details: Defines the API for the corresponding SHA1 implementation. +*********************************************************************/ + +#ifndef SHA256_H +#define SHA256_H + +/*************************** HEADER FILES ***************************/ +#include + +/****************************** MACROS ******************************/ +#define SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest + +/**************************** DATA TYPES ****************************/ +#ifdef _MSC_VER +typedef unsigned __int8 uint8; +typedef unsigned __int32 uint32; +typedef unsigned __int64 uint64; +#else +#include +typedef uint8_t uint8; +typedef uint32_t uint32; +typedef uint64_t uint64; +#endif + +typedef struct { + uint8 data[64]; + uint32 datalen; + uint64 bitlen; + uint32 state[8]; +} SHA256_CTX; + +/*********************** FUNCTION DECLARATIONS **********************/ +void sha256_init(SHA256_CTX *ctx); +void sha256_update(SHA256_CTX *ctx, const uint8 data[], size_t len); +void sha256_final(SHA256_CTX *ctx, uint8 hash[]); + +#endif // SHA256_H diff --git a/code/sys/sys_autoupdater.c b/code/sys/sys_autoupdater.c new file mode 100644 index 00000000..2f59e071 --- /dev/null +++ b/code/sys/sys_autoupdater.c @@ -0,0 +1,86 @@ +/* +The code in this file is in the public domain. The rest of ioquake3 +is licensed under the GPLv2. Do not mingle code, please! +*/ + +#ifdef USE_AUTOUPDATER +# ifndef AUTOUPDATER_BIN +# error The build system should have defined AUTOUPDATER_BIN +# endif + +# ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN 1 +# include +# else +# include +# endif + +# include +# include +#endif + +void Sys_LaunchAutoupdater(int argc, char **argv) +{ +#ifdef USE_AUTOUPDATER + #ifdef _WIN32 + { + /* We don't need the Unix pipe() tapdance here because Windows lets children wait on parent processes. */ + PROCESS_INFORMATION procinfo; + STARTUPINFO startinfo; + char cmdline[128]; + memset(&procinfo, '\0', sizeof (procinfo)); + memset(&startinfo, '\0', sizeof (startinfo)); + startinfo.cb = sizeof (startinfo); + sprintf(cmdline, "" AUTOUPDATER_BIN " --waitpid %u", (unsigned int) GetCurrentProcessId()); + + if (CreateProcessA(AUTOUPDATER_BIN, cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startinfo, &procinfo)) + { + /* close handles now so child cleans up immediately if nothing to do */ + CloseHandle(procinfo.hProcess); + CloseHandle(procinfo.hThread); + } + } + #else + int updater_pipes[2]; + if (pipe(updater_pipes) == 0) + { + pid_t pid = fork(); + if (pid == -1) /* failure, oh well. */ + { + close(updater_pipes[0]); + close(updater_pipes[1]); + } + else if (pid == 0) /* child process */ + { + close(updater_pipes[1]); /* don't need write end. */ + if (dup2(updater_pipes[0], 3) != -1) + { + char pidstr[64]; + char *ptr = strrchr(argv[0], '/'); + if (ptr) + *ptr = '\0'; + if (chdir(argv[0]) == -1) { + _exit(1); /* oh well. */ + } + #ifdef __APPLE__ + if (chdir("../..") == -1) { /* put this at base of app bundle so paths make sense later. */ + _exit(1); /* oh well. */ + } + #endif + snprintf(pidstr, sizeof (pidstr), "%lld", (long long) getppid()); + execl(AUTOUPDATER_BIN, AUTOUPDATER_BIN, "--waitpid", pidstr, NULL); + } + _exit(0); /* oh well. */ + } + else /* parent process */ + { + /* leave the write end open until we terminate so updater can block on it. */ + close(updater_pipes[0]); + } + } + #endif +#endif + + (void) argc; (void) argv; /* possibly unused. Pacify compilers. */ +} + diff --git a/code/sys/sys_main.c b/code/sys/sys_main.c index b559861b..fc1ff4ad 100644 --- a/code/sys/sys_main.c +++ b/code/sys/sys_main.c @@ -683,6 +683,9 @@ int main( int argc, char **argv ) int i; char commandLine[ MAX_STRING_CHARS ] = { 0 }; + extern void Sys_LaunchAutoupdater(int argc, char **argv); + Sys_LaunchAutoupdater(argc, argv); + #ifndef DEDICATED // SDL version check