Initial shot at writing an ioquake3 autoupdater.

This commit is contained in:
Ryan C. Gordon 2017-05-25 14:13:18 -04:00
parent c65d2c2657
commit 4729c683fd
6 changed files with 1147 additions and 22 deletions

View File

@ -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
@ -1550,6 +1570,27 @@ $(Q3ASM): $(Q3ASMOBJ)
$(Q)$(TOOLS_CC) $(TOOLS_CFLAGS) $(TOOLS_LDFLAGS) -o $@ $^ $(TOOLS_LIBS)
#############################################################################
# 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
#############################################################################

125
autoupdater-readme.txt Normal file
View File

@ -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.

View File

@ -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;
}

158
code/autoupdater/sha256.c Normal file
View File

@ -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;
}
}

34
code/autoupdater/sha256.h Normal file
View File

@ -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

View File

@ -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