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
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).
|
|
|
|
(import
|
|
;; 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)
|
|
(comptime-cond
|
|
('Windows
|
|
(when (and
|
|
(= 0 (strcmp "Windows" (path download > operating-system)))
|
|
(= 0 (strcmp "x64" (path download > architecture))))
|
|
(return download)))
|
|
('Unix
|
|
(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)
|
|
public-key))
|
|
(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_Linux-x64.zip machsearch
|
|
;; ./cryptography-cli create-signed-file Machsearch_Linux-x64.zip ~/website/updates/Machsearch/Machsearch_Linux-x64.auto-update
|
|
;; 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
|
|
curl
|
|
update-cakedata-url
|
|
(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)
|
|
version-output-directory)
|
|
(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)))
|
|
|
|
(comptime-cond
|
|
('auto-test
|
|
(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 test.zip TestSerialize.cakedata TestDictionarySerialize.cakedata
|
|
;; ./cryptography-cli create-signed-file test.zip ~/website/updates/Product/Product_Linux-x64.auto-update
|
|
(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))
|
|
"https://localhost:8888/updates/Machsearch/machsearch.cakedata")
|
|
(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")
|
|
(curl_global_cleanup)
|
|
(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)
|
|
(curl_global_cleanup)
|
|
(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)
|
|
(curl_global_cleanup)
|
|
(return 0))))
|
|
|