commit 054a9c41f3bd637b1f182c3fc8b2f4103c36f0e2 parent 780a7f3497fffc37974575145bf71d629e0e419f Author: Michael Savage <mikejsavage@gmail.com> Date: Sun Nov 20 00:00:38 +0200 Add intial launcher/updater code Diffstat:
launcher/main.cc | | | 277 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
diff --git a/launcher/main.cc b/launcher/main.cc @@ -0,0 +1,277 @@ +#include <stdlib.h> + +#include <string> +#include <vector> +#include <map> + +#include "../intrinsics.h" +#include "../log.h" +#include "../http.h" +#include "../sha2.h" +#include "../strlcpy.h" +#include "../platform_io.h" + +#define HASH_SEED 1234 + +struct SHA256 { + SHA256() { } + + explicit SHA256( const void * data, size_t len ) { + SHA2_CTX ctx; + SHA256Init( &ctx ); + SHA256Update( &ctx, ( const u8 * ) data, len ); + + u8 digest[ SHA256_DIGEST_LENGTH ]; + SHA256Final( digest, &ctx ); + + static const char hex[] = "0123456789abcdef"; + for( size_t i = 0; i < sizeof( digest ); i++ ) { + e[ i * 2 + 0 ] = hex[ digest[ i ] >> 4 ]; + e[ i * 2 + 1 ] = hex[ digest[ i ] & 0x0f ]; + } + e[ sizeof( e ) - 1 ] = '\0'; + } + + explicit SHA256( const std::string & str ) : SHA256( str.c_str(), str.size() ) { } + + char e[ SHA256_DIGEST_STRING_LENGTH ]; +}; + +static bool operator==( const SHA256 & a, const SHA256 & b ) { + return memcmp( a.e, b.e, sizeof( a.e ) ) == 0; +} + +static bool operator!=( const SHA256 & a, const SHA256 & b ) { + return !( a == b ); +} + +struct ManifestEntry { + const char * file_name; + SHA256 checksum; + u64 file_size; +}; + +struct Version { + u32 a, b, c, d; +}; + +static bool operator==( const Version & a, const Version & b ) { + return a.a == b.a && a.b == b.b && a.c == b.c && a.d == b.d; +} + +static bool operator!=( const Version & a, const Version & b ) { + return !( a == b ); +} + +static bool parse_version( Version * v, const char * str ) { + int fields = sscanf( str, "%u.%u.%u.%u", &v->a, &v->b, &v->c, &v->d ); + if( fields != 4 ) { + *v = { }; + return false; + } + return true; +} + +static bool parse_manifest( std::map< std::string, ManifestEntry > & manifest, const char * data ) { + size_t len = strlen( data ); + size_t i = 0; + while( true ) { + char file_name[ 128 ]; + char sha256[ 65 ]; + unsigned long long file_size_ull; + int bytes_read; + + int fields = sscanf( data + i, "%127s %64[0-9a-f] %llu%n", file_name, sha256, &file_size_ull, &bytes_read ); + if( fields != 3 ) return false; + + ManifestEntry entry; + strlcpy( entry.checksum.e, sha256, sizeof( entry.checksum.e ) ); + entry.file_size = checked_cast< u64 >( file_size_ull ); + + manifest[ file_name ] = entry; + + i += bytes_read; + if( i >= len - 1 ) break; + } + + return true; +} + +static bool already_downloaded( const std::string & path, const ManifestEntry & entry ) { + FILE * file = fopen( path.c_str(), "rb" ); + if( file == NULL ) return false; + + SCOPE_EXIT( fclose( file ) ); + + fseek( file, 0, SEEK_END ); + u64 file_size = checked_cast< u64 >( ftell( file ) ); + fseek( file, 0, SEEK_SET ); + + if( file_size != entry.file_size ) return false; + + void * contents = malloc( file_size ); + size_t bytes_read = fread( contents, 1, file_size, file ); + ASSERT( bytes_read == file_size ); + + SHA256 sha256( contents, file_size ); + free( contents ); + + return sha256 == entry.checksum; +} + +static const char * file_get_contents_or_empty( const char * path, size_t * out_len = NULL ) { + FILE * file = fopen( path, "rb" ); + if( file == NULL ) return ""; + + fseek( file, 0, SEEK_END ); + size_t len = checked_cast< size_t >( ftell( file ) ); + ASSERT( len < SIZE_MAX ); + fseek( file, 0, SEEK_SET ); + + char * contents = ( char * ) malloc( len + 1 ); + if( contents == NULL ) FATAL( "malloc" ); + size_t bytes_read = fread( contents, 1, len, file ); + contents[ len ] = '\0'; + ASSERT( bytes_read == len ); + + if( out_len ) *out_len = len; + + fclose( file ); + + return contents; +} + +#define HOST "f.mikejsavage.co.uk" +int main() { +#if PLATFORM_WINDOWS + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 2, 2 ), &wsa_data ) != 0 ) { + FATAL( "couldn't init Winsock" ); + } +#endif + + // TODO: if local verison doesn't exist treat it as 0.0.0.0 + const char * local_version_str = file_get_contents_or_empty( "version.txt" ); + std::string remote_version_str; + // TODO: if getting remote version fails wait 30s and try again + struct sockaddr_storage address; + if( !dns_first( HOST, &address ) ) { + FATAL( "couldn't resolve " HOST ); + } + if( http_get( address, HOST, "/version.txt", &remote_version_str ) != GET_OK ) { + FATAL( "couldn't download version.txt" ); + } + + Version local_version, remote_version; + bool has_local_version = parse_version( &local_version, local_version_str ); + // TODO: if getting remote version fails wait 30s and try again + if( !parse_version( &remote_version, remote_version_str.c_str() ) ) { + FATAL( "can't parse version: %s", remote_version_str ); + } + + printf( "local %u.%u.%u.%u remote %u.%u.%u.%u\n", + local_version.a, local_version.b, local_version.c, local_version.d, + remote_version.a, remote_version.b, remote_version.c, remote_version.d ); + + if( has_local_version && local_version == remote_version ) { + printf( "no need to update\n" ); + return 0; + } + + const char * local_manifest_str = file_get_contents_or_empty( "manifest.txt" ); + char remote_manifest_path[ 256 ]; + sprintf( remote_manifest_path, "/%u.%u.%u.%u.txt", remote_version.a, remote_version.b, remote_version.c, remote_version.d ); + std::string remote_manifest_str; + http_get( address, HOST, remote_manifest_path, &remote_manifest_str ); + + std::map< std::string, ManifestEntry > local_manifest, remote_manifest; + parse_manifest( local_manifest, local_manifest_str ); + parse_manifest( remote_manifest, remote_manifest_str.c_str() ); + + std::vector< std::string > files_to_update, files_to_remove; + u64 update_size = 0; + + // look for files that have changed and files that should be removed + for( const auto & kv : local_manifest ) { + const auto & it = remote_manifest.find( kv.first ); + if( it == remote_manifest.end() ) { + files_to_remove.push_back( kv.first ); + } + else if( it->second.checksum != kv.second.checksum ) { + files_to_update.push_back( kv.first ); + update_size += it->second.file_size; + } + } + + // look for new files + for( const auto & kv : remote_manifest ) { + const auto & it = local_manifest.find( kv.first ); + if( it == local_manifest.end() ) { + files_to_update.push_back( kv.first ); + update_size += kv.second.file_size; + } + } + + mkdir( "update", 0755 ); + for( size_t i = 0; i < files_to_update.size(); i++ ) { + const ManifestEntry & entry = remote_manifest[ files_to_update[ i ] ]; + const std::string update_path = "update/" + files_to_update[ i ]; + + // check if we already downloaded it + if( already_downloaded( update_path, entry ) ) { + printf( "already downloaded %s\n", files_to_update[ i ].c_str() ); + continue; + } + + const std::string remote_path = std::string( "/" ) + entry.checksum.e; + printf( "downloading %s (" HOST "%s)\n", + files_to_update[ i ].c_str(), + remote_path.c_str() ); + + // TODO: http_get_to_file. can keep track of file size and SHA256 state as we download + std::string file_contents; + GetResult res = http_get( address, HOST, remote_path.c_str(), &file_contents ); + if( res != GET_OK ) { + FATAL( "couldn't download %s", files_to_update[ i ].c_str() ); + } + + if( file_contents.size() != entry.file_size ) { + FATAL( "sizes don't match" ); + } + + SHA256 checksum( file_contents ); + if( checksum != entry.checksum ) { + FATAL( "checksums don't match" ); + } + + // TODO: create more directories if we need to + FILE * f = fopen( ( "update/" + files_to_update[ i ] ).c_str(), "wb" ); + fwrite( file_contents.c_str(), 1, file_contents.size(), f ); + fclose( f ); + } + + for( size_t i = 0; i < files_to_update.size(); i++ ) { + const std::string update_path = "update/" + files_to_update[ i ]; + rename( update_path.c_str(), files_to_update[ i ].c_str() ); + } + + rmdir( "update" ); + + for( size_t i = 0; i < files_to_remove.size(); i++ ) { + printf( "removing: %s\n", files_to_remove[ i ].c_str() ); + // TODO + // unlink( files_to_remove[ i ].c_str() ); + } + + { + FILE * f = fopen( "version.txt", "w" ); + fwrite( remote_version_str.c_str(), 1, remote_version_str.size(), f ); + fclose( f ); + + FILE * f2 = fopen( "manifest.txt", "w" ); + fwrite( remote_manifest_str.c_str(), 1, remote_manifest_str.size(), f2 ); + fclose( f2 ); + } + + return 0; +}