GameLib is a collection of libraries for creating applications in Cakelisp.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
12 KiB

;; AutoUpdate.cake: A system for creating self-updating executables, i.e. ones where the user can
;; request the new version be downloaded and used without having to leave the app.
;; Components:
;; * Checking for updates
;; - A server is running to say what the latest version is.
;; - The client sends a request and gets a response with data on what the latest version is.
;; - The client compares its version with latest and decides what to do (ideally, presenting
;; the user the option to update).
;; * Getting the updated file
;; - The client talks to the server to get the URL it can get the latest version from.
;; - The client downloads the file from that URL, via CURL etc.
;; - The file is downloaded to a location where the user isn't likely to move it, e.g. their local
;; user data directory.
;; * Verifying the file
;; - The client checks the file's cryptographic signature against a hard-coded public key. The file
;; is signed by the server with a matching public key. If they match, no man-in-the-middle attack
;; should be possible.
;; * Using the file
;; - Remove the extra bytes used for the signature.
;; - Depending on the application, extract the file, load the extracted library, close and
;; run the update as a subprocess, etc.
;; - On subsequent executions, check the user data directory for any update files, and run those
;; as a subprocess instead. (In the case where the update is an entire executable).
;; Cakelisp
"FileUtilities.cake" "CHelpers.cake"
;; GameLib
"Introspection.cake" "DynamicArray.cake" "OpenSSL.cake" "Curl.cake" "Cryptography.cake"
"Compression.cake" "FileSystem.cake")
(c-import &with-decls "<stddef.h>")
(def-introspect-struct auto-update-download
operating-system ([] 32 char)
architecture ([] 32 char)
url ([] 1024 char)
;; After the update, this file should be used for the latest version (e.g. executable or DLL)
file-to-use ([] 1024 char))
(def-introspect-struct auto-update-metadata
name ([] 64 char)
latest-version int
changelog (* char)
downloads (* auto-update-download) (override 'dynarray))
(def-type-alias-global CURL void)
(defun auto-update-get-current-platform-download (update-data (* auto-update-metadata)
&return (* auto-update-download))
(each-item-addr-in-dynarray (path update-data > downloads) i download (* auto-update-download)
(when (and
(= 0 (strcmp "Windows" (path download > operating-system)))
(= 0 (strcmp "x64" (path download > architecture))))
(return download)))
(when (and
(= 0 (strcmp "Linux" (path download > operating-system)))
(= 0 (strcmp "x64" (path download > architecture))))
(return download)))))
(return null))
;; TODO: Add version header
(defun auto-update-get-latest-version-metadata (curl (* CURL) update-cakedata-url (* (const char))
update-data-out (* auto-update-metadata)
&return bool)
(var result-buffer dynstring null)
(dynarray-set-capacity result-buffer (* 1024 4)) ;; 4 kib
(unless (curl-download-into-dynarray curl update-cakedata-url (addr result-buffer))
(dynarray-free result-buffer)
(return false))
;; Null-terminate
(dynarray-push result-buffer 0)
;; (fprintf stderr "%s" result-buffer)
(unless (read-introspect-struct-s-expr auto-update-metadata--metadata
update-data-out result-buffer malloc null)
(dynarray-free result-buffer)
(return false))
(dynarray-free result-buffer)
(return true))
(defun auto-update-download-and-verify-signature (curl (* CURL)
url (* (const char))
public-key (* (unsigned char))
verified-payload-out (* (* (unsigned char)))
verified-payload-out-size (* (unsigned (long long)))
&return bool)
(set (deref verified-payload-out-size) 0)
(var result-buffer (* char) null)
(unless (curl-download-into-dynarray curl url (addr result-buffer))
(dynarray-free result-buffer)
(return false))
;; This will contain the extra bytes from the signature, which is wasted, but minimal
(set (deref verified-payload-out)
(type-cast (malloc (dynarray-length result-buffer)) (* (unsigned char))))
(unless (= 0 (crypto_sign_open (deref verified-payload-out) verified-payload-out-size
(type-cast result-buffer (* (const (unsigned char))))
(dynarray-length result-buffer)
(fprintf stderr "warning: the downloaded file's signature does NOT appear to be signed
appropriately. It will not be used. Either someone messed up, your public key is out of date, or an
attempt at compromising security occurred and was thwarted by this protection.\n")
(dynarray-free result-buffer)
(free (deref verified-payload-out))
(return false))
(dynarray-free result-buffer)
(return true))
;; Use CryptographyCLI.cake utility to generate your own keys and signed files. You can set up
;; the .cakedata serving however you want, so long as Curl can download it.
;; Creating an auto-update file:
;; zip machsearch
;; ./cryptography-cli create-signed-file ~/website/updates/Machsearch/
;; The download will only occur if the latest version is greater than current-application-version.
;; new-file-to-use-out-buffer will only be set if there was actually a newer file
;; changelog-out may be null if not provided
;; Note: Any more args and this should take an args struct, or return the metadata!
(defun auto-update-download (curl (* CURL) public-key (* (unsigned char))
update-cakedata-url (* (const char))
current-application-version int
output-directory (* (const char))
new-file-to-use-out-buffer (* char)
new-file-to-use-out-buffer-size size_t
new-version-out (* int)
changelog-out (* (* char))
&return bool)
(var update-metadata auto-update-metadata (array 0))
(unless (auto-update-get-latest-version-metadata
(addr update-metadata))
(fprintf stderr "error: expected server to be running before doing this test\n")
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return false))
(var platform-download (* auto-update-download)
(auto-update-get-current-platform-download (addr update-metadata)))
(unless platform-download
(fprintf stderr "Could not find update for this platform\n")
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return false))
(var platform-update-url (* (const char)) (path platform-download > url))
(scope ;; Print results
(fprintf stderr "Latest version of %s is %d.\nDownloads:\n"
(field update-metadata name) (field update-metadata latest-version))
(each-item-addr-in-dynarray (field update-metadata downloads) i download (* auto-update-download)
(fprintf stderr "\t%s %s at %s\n" (path download > operating-system)
(path download > architecture)
(path download > url)))
(fprintf stderr "The current platform should download %s\n"
(? platform-update-url platform-update-url "unknown platform")))
(when (>= current-application-version (field update-metadata latest-version))
(fprintf stderr "The application is already up-to-date.\n")
(set (at 0 new-file-to-use-out-buffer) 0)
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return true))
(var verified-payload (* (unsigned char)) null)
(var verified-payload-size (unsigned (long long)) 0)
(unless (auto-update-download-and-verify-signature curl platform-update-url public-key
(addr verified-payload)
(addr verified-payload-size))
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return false))
(var version-output-directory ([] 1024 char) (array 0))
(sprintf version-output-directory "%s/v%d" output-directory (field update-metadata latest-version))
(unless (make-directory version-output-directory)
(free verified-payload)
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return false))
(unless (decompress-zip-from-memory-to-files
verified-payload (type-cast verified-payload-size size_t)
(free verified-payload)
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return false))
(free verified-payload)
(snprintf new-file-to-use-out-buffer new-file-to-use-out-buffer-size "%s/%s"
version-output-directory (path platform-download > file-to-use))
(set (deref new-version-out) (field update-metadata latest-version))
(when changelog-out
(set (deref changelog-out) (field update-metadata changelog))
;; Caller takes ownership
(set (field update-metadata changelog) null))
(free-introspect-struct-fields auto-update-metadata--metadata (addr update-metadata) free)
(return true))
;; Retain the ability to use another allocator within this module
(defun auto-update-changelog-free (changelog (* char))
(when changelog
(free changelog)))
(c-import "<stdio.h>")
(defun test--auto-update (&return int)
;; These will need to be changed if you want this to work for you!
;; Use CryptographyCLI.cake utility to generate your own keys and signed files. You can set up
;; the .cakedata serving however you want, so long as Curl can download it.
;; Creating an auto-update file:
;; zip TestSerialize.cakedata TestDictionarySerialize.cakedata
;; ./cryptography-cli create-signed-file ~/website/updates/Product/
(var macoy-public-key ([] crypto_sign_PUBLICKEYBYTES (unsigned char))
(array 0x8a 0xd0 0x2a 0x05 0x0a 0x57 0xe8 0x4c 0x7c 0x73 0xcf 0xdb 0x26 0xdd 0xb9 0xf7 0x6f 0x92
0x05 0xe6 0x5f 0xa5 0xf7 0xf4 0x50 0x87 0x33 0xec 0x5f 0xb0 0x66 0x84))
(var update-cakedata-url (* (const char))
(var current-version int 0)
(var output-directory (* (const char)) ".")
(var new-file-to-use ([] 2048 char) (array 0))
(when (!= (curl_global_init CURL_GLOBAL_DEFAULT) 0)
(fprintf stderr "error: Failed to initialize curl\n")
(return 1))
(var curl (* CURL) (curl_easy_init))
(unless curl
(fprintf stderr "error: Failed to get curl\n")
(return 1))
;; For testing only! Disable SSL certificate validation
(curl_easy_setopt curl CURLOPT_SSL_VERIFYPEER 0)
(curl_easy_setopt curl CURLOPT_SSL_VERIFYHOST 0)
(var new-version int 0)
(var changelog (* char) null)
(unless (auto-update-download curl macoy-public-key update-cakedata-url current-version
output-directory new-file-to-use (sizeof new-file-to-use)
(addr new-version) (addr changelog))
(curl_easy_cleanup curl)
(return 1))
(fprintf stderr "The update says I should use %s for version %d\n" new-file-to-use new-version)
(if changelog
(fprintf stderr "Changelog:\n%s\n" changelog)
(auto-update-changelog-free changelog))
(curl_easy_cleanup curl)
(return 0))))