[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