[ioquake3] 53/136: Initial shot at writing an ioquake3 autoupdater.
Simon McVittie
smcv at debian.org
Thu Jun 15 09:09:09 UTC 2017
This is an automated email from the git hooks/post-receive script.
smcv pushed a commit to branch debian/master
in repository ioquake3.
commit 4729c683fd0cca6223955da36eff8b7285e6ad31
Author: Ryan C. Gordon <icculus at icculus.org>
Date: Thu May 25 14:13:18 2017 -0400
Initial shot at writing an ioquake3 autoupdater.
---
Makefile | 85 +++--
autoupdater-readme.txt | 125 +++++++
code/autoupdater/autoupdater.c | 721 +++++++++++++++++++++++++++++++++++++++++
code/autoupdater/sha256.c | 158 +++++++++
code/autoupdater/sha256.h | 34 ++
code/sys/sys_main.c | 46 +++
6 files changed, 1147 insertions(+), 22 deletions(-)
diff --git a/Makefile b/Makefile
index cd98122..0650759 100644
--- a/Makefile
+++ b/Makefile
@@ -35,6 +35,9 @@ endif
ifndef BUILD_RENDERER_OPENGL2
BUILD_RENDERER_OPENGL2=
endif
+ifndef BUILD_AUTOUPDATER
+ BUILD_AUTOUPDATER=
+endif
#############################################################################
#
@@ -228,6 +231,10 @@ ifndef USE_YACC
USE_YACC=0
endif
+ifndef USE_AUTOUPDATER
+USE_AUTOUPDATER=1
+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
@@ -975,6 +984,11 @@ ifneq ($(BUILD_GAME_QVM),0)
endif
endif
+ifneq ($(BUILD_AUTOUPDATER),0)
+ AUTOUPDATER_BIN := autoupdater$(FULLBINEXT)
+ TARGETS += $(B)/$(AUTOUPDATER_BIN)
+endif
+
ifeq ($(USE_OPENAL),1)
CLIENT_CFLAGS += -DUSE_OPENAL
ifeq ($(USE_OPENAL_DLOPEN),1)
@@ -1075,6 +1089,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
@@ -1331,6 +1350,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
@@ -1551,6 +1571,27 @@ $(Q3ASM): $(Q3ASMOBJ)
#############################################################################
+# AUTOUPDATER
+#############################################################################
+
+define DO_AUTOUPDATER_CC
+$(echo_cmd) "AUTOUPDATER_CC $<"
+$(Q)$(TOOLS_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) $(CURL_LIBS) -o $@ $(Q3AUTOUPDATEROBJ)
+
+
+#############################################################################
# CLIENT/SERVER
#############################################################################
diff --git a/autoupdater-readme.txt b/autoupdater-readme.txt
new file mode 100644
index 0000000..1353f4f
--- /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 at icculus.org
+
+--ryan.
+
diff --git a/code/autoupdater/autoupdater.c b/code/autoupdater/autoupdater.c
new file mode 100644
index 0000000..e8b8d40
--- /dev/null
+++ b/code/autoupdater/autoupdater.c
@@ -0,0 +1,721 @@
+/*
+The code in this file is in the public domain. The rest of ioquake3
+is licensed until the GPLv2. Do not mingle code, please!
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdarg.h>
+
+#include <unistd.h>
+#include <sys/stat.h>
+#include <signal.h>
+
+#include <curl/curl.h>
+#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"
+#else
+#error Please define your platform.
+#endif
+
+#ifdef __i386__
+#define AUTOUPDATE_ARCH "i386"
+#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;
+
+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 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;
+static void die(const char *why)
+{
+ infof("FAILURE: %s", why);
+ restoreRollbacks();
+ freeManifest();
+ exit(1);
+}
+
+static void outOfMemory() NEVER_RETURNS;
+static void outOfMemory()
+{
+ die("Out of memory");
+}
+
+static void makeDir(const char *dirname)
+{
+ /* !!! FIXME: we don't care if this fails right now. */
+ mkdir(dirname, 0777);
+}
+
+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 %lld if necessary", (long long) options.waitforprocess);
+ } else if (strcmp(argv[i], "--updateself") == 0) {
+ options.updateself = argv[i + 1];
+ infof("We are updating ourself ('%s')", options.updateself);
+ }
+ }
+}
+
+static CURL *prepCurl(const char *url, FILE *outfile)
+{
+ char *fullurl;
+ const size_t len = strlen(AUTOUPDATE_URL) + strlen(url) + 1;
+ CURL *curl = curl_easy_init();
+ if (!curl) {
+ die("curl_easy_init() failed");
+ }
+
+ fullurl = (char *) alloca(len);
+ if (!fullurl) {
+ outOfMemory();
+ }
+
+ snprintf(fullurl, len, "%s%s", AUTOUPDATE_URL, url);
+
+ infof("Downloading from '%s'", fullurl);
+
+ #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, outfile);
+
+ curl_easy_setopt(curl, CURLOPT_URL, fullurl);
+
+ 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. */
+
+ return curl;
+}
+
+static void downloadURL(const char *from, const char *to)
+{
+ FILE *io;
+ CURL *curl;
+
+ infof("Preparing to download to '%s'", to);
+
+ buildParentDirs(to);
+ io = fopen(to, "wb");
+ if (!io) {
+ die("Failed to open output file");
+ }
+
+ curl = prepCurl(from, io);
+ if (curl_easy_perform(curl) != CURLE_OK) {
+ remove(to);
+ die("Download failed");
+ }
+ curl_easy_cleanup(curl);
+
+ 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, BYTE *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 *) malloc(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;
+
+ 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), "%lld", (long long) options.waitforprocess);
+ execl(options.updateself, options.updateself, "--waitpid", pidstr, NULL);
+ } else {
+ execl(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;
+ BYTE 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), "%lld", (long long) options.waitforprocess);
+ execl(to, to, "--updateself", argv0, "--waitpid", pidstr, NULL);
+ } else {
+ execl(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");
+ fclose(io);
+ if (io != NULL) {
+ static int rollbackIndex = 0;
+ char rollbackPath[64];
+ 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) {
+ /* ioquake3 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;
+ infof("Waiting for pid %lld to die...", (long long) options.waitforprocess);
+ 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);
+ }
+ 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 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 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)
+{
+ signal(SIGPIPE, SIG_IGN); /* don't trigger signal when fd3 closes */
+
+ 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]);
+ }
+
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
+ die("curl_global_init() failed!");
+ }
+
+ 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();
+ curl_global_cleanup();
+
+ infof("Updater ending, %s", timestamp());
+
+ return 0;
+}
+
diff --git a/code/autoupdater/sha256.c b/code/autoupdater/sha256.c
new file mode 100644
index 0000000..eb9c5c0
--- /dev/null
+++ b/code/autoupdater/sha256.c
@@ -0,0 +1,158 @@
+/*********************************************************************
+* 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 <stdlib.h>
+#include <memory.h>
+#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 WORD 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 BYTE data[])
+{
+ WORD 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 BYTE data[], size_t len)
+{
+ WORD 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, BYTE hash[])
+{
+ WORD i;
+
+ 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 0000000..7123a30
--- /dev/null
+++ b/code/autoupdater/sha256.h
@@ -0,0 +1,34 @@
+/*********************************************************************
+* 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 <stddef.h>
+
+/****************************** MACROS ******************************/
+#define SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest
+
+/**************************** DATA TYPES ****************************/
+typedef unsigned char BYTE; // 8-bit byte
+typedef unsigned int WORD; // 32-bit word, change to "long" for 16-bit machines
+
+typedef struct {
+ BYTE data[64];
+ WORD datalen;
+ unsigned long long bitlen;
+ WORD state[8];
+} SHA256_CTX;
+
+/*********************** FUNCTION DECLARATIONS **********************/
+void sha256_init(SHA256_CTX *ctx);
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len);
+void sha256_final(SHA256_CTX *ctx, BYTE hash[]);
+
+#endif // SHA256_H
diff --git a/code/sys/sys_main.c b/code/sys/sys_main.c
index 6d7fe7b..e754304 100644
--- a/code/sys/sys_main.c
+++ b/code/sys/sys_main.c
@@ -31,6 +31,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#include <ctype.h>
#include <errno.h>
+#ifndef _WIN32
+#include <unistd.h>
+#endif
+
#ifndef DEDICATED
#ifdef USE_LOCAL_HEADERS
# include "SDL.h"
@@ -659,6 +663,48 @@ int main( int argc, char **argv )
int i;
char commandLine[ MAX_STRING_CHARS ] = { 0 };
+#ifdef USE_AUTOUPDATER
+{
+ #ifndef AUTOUPDATER_BIN
+ #error The build system should have defined AUTOUPDATER_BIN
+ #endif
+
+ 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';
+ chdir(argv[0]);
+ #ifdef __APPLE__
+ chdir("../.."); /* put this at base of app bundle so paths make sense later. */
+ #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
+
#ifndef DEDICATED
// SDL version check
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/ioquake3.git
More information about the Pkg-games-commits
mailing list