From 9d99318265fdecf817074254ada07b926ab3109f Mon Sep 17 00:00:00 2001 From: teknomunk Date: Thu, 30 Nov 2023 19:53:05 -0600 Subject: [PATCH] Implement server setup wizard --- .gitignore | 1 + README.md | 10 +- src/controller/admin.c | 135 +++++++++++++++++++++- src/controller/admin.h | 1 + src/controller/main.c | 35 +++--- src/model/owner.c | 20 +++- src/model/owner.h | 1 + src/model/server.c | 113 +++++++++++++----- src/model/server.h | 8 +- src/view/admin/owner-setup.html.template | 34 ++++++ src/view/admin/server-setup.html.template | 11 +- 11 files changed, 312 insertions(+), 57 deletions(-) create mode 100644 src/view/admin/owner-setup.html.template diff --git a/.gitignore b/.gitignore index 3887c73..6a71d16 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ apogee apogee.debug /*.log /*.json +obj/ diff --git a/README.md b/README.md index e5bfc8e..a79bac5 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,17 @@ USE AT YOUR OWN RISK. * ActivityPub federation - follow interesting people on the many instances of the fediverse. # Dependencies +## Runtime * libCURL * OpenSSL +* Globaly resolvable domain name +* Tor + +## Source Install/Development * C Compiler (test with GCC) * Ruby (hopefully temporary) -* Globaly resolvable domain name -# Installation Instructions +# Source Installation Instructions Ensure you have system packages for the dependencies installed, then run the following code in a terminal window: @@ -47,6 +51,6 @@ Starting section 5 (fetch) ```` The listening port is chosen at random each time the program is run until setup, then the configured -port is used. Open a browser and navigate to http://${hostname-or-ip}:${port}/, then fill in the +port is used. Open a browser and navigate to http://${hostnameOrIPAddress}:${port}/, then fill in the required information. diff --git a/src/controller/admin.c b/src/controller/admin.c index e8a7f01..e562d63 100644 --- a/src/controller/admin.c +++ b/src/controller/admin.c @@ -2,14 +2,22 @@ // Model #include "src/model/server.h" +#include "src/model/owner.h" +#include "src/model/account.h" // View // Controller #include "src/controller/api/client_apps.h" +// Submodules +#include "form.h" #include "http/server/request.h" +// Platform Headers +#include +#include + const char* view_checkbox( bool value ) { return value ? "checked" : ""; @@ -33,13 +41,132 @@ bool route_admin_request( struct http_request* req ) return false; } -// Route: /admin/server-setup -// Special: / (when server hasn't been configured) -bool handle_admin_server_setup( struct http_request* req ) +// Special: /, step=1 +bool handle_admin_initial_owner_setup( struct http_request* req ) { + if( http_request_route_method( req, "POST" ) ) { + // TODO: handle post + FILE* body = http_request_get_request_data(req); + struct form_parser* fp = form_pull_parser_new(body); + if( !fp ) { goto show_owner_setup; } + + struct owner* o = owner_new(); + + // Create owner account + struct account* owner = account_new(); + owner->id = owner_account_id; + account_save(owner); + + // Create home timeline account + { + struct account* home = account_new(); + home->id = home_timeline_id; + home->handle = strdup("%home-timeline"); + home->server = strdup("localhost"); + account_save(home); + account_free(home); + } + + // Create public timeline account + { + struct account* public = account_new(); + public->id = public_timeline_id; + public->handle = strdup("%public-timeline"); + public->server = strdup("localhost"); + account_save(public); + account_free(public); + } + + bool success = false; + char* password = NULL; + char* confirm = NULL; + + char* key = NULL; + while( (key=form_pull_parser_read_key(fp)) ) { + if( 0 == strcmp(key,"password") ) { + password = strdup(form_pull_parser_read_value(fp)); + } else if( 0 == strcmp(key,"confirm") ) { + confirm = strdup(form_pull_parser_read_value(fp)); + } else if( 0 == strcmp(key,"handle") ) { + owner->handle = strdup(form_pull_parser_read_value(fp)); + account_save(owner); + } + } + + if( owner->handle && *owner->handle && password && confirm && ( 0 == strcmp(password,confirm) ) ) { + owner_set_password( o, password ); + success = true; + } + form_pull_parser_release(fp); + + if( success ) { + owner_save(o); + } + owner_free(o); + account_free(owner); + + if( success ) { + // TODO: generate crypto keys + + // Advance wizard to next step + g_server->configured = true; + app_args_save(); + + http_request_begin_send_headers( req, 302, false ); + http_request_send_header( req, "Location", "/?complete" ); + http_request_end_send_headers( req, false ); + return true; + } + } + +show_owner_setup: http_request_send_headers( req, 200, "text/html", true ); FILE* f = http_request_get_response_body( req ); - #include "view/admin/server-setup.html.inc" + #include "view/admin/owner-setup.html.inc" return true; } +// Route: /admin/server-setup +// Special: /, step=0 (when server hasn't been configured) +bool handle_admin_server_setup( struct http_request* req ) +{ + if( http_request_route_method( req, "POST" ) ) { + // TODO: handle post + FILE* body = http_request_get_request_data(req); + struct form_parser* fp = form_pull_parser_new(body); + if( !fp ) { return false; } + + app_args_load_from_form( g_server, fp ); + form_pull_parser_release(fp); + + // Advance wizard to next step + if( g_server->setup_wizard_step == 0 ) { + g_server->setup_wizard_step = 1; + } + + app_args_save(); + app_args_load(); + + // Redirect + http_request_begin_send_headers( req, 302, false ); + http_request_send_header( req, "Location", "/?account" ); + http_request_end_send_headers( req, false ); + return true; + } else { + http_request_send_headers( req, 200, "text/html", true ); + FILE* f = http_request_get_response_body( req ); + #include "view/admin/server-setup.html.inc" + return true; + } +} + +bool handle_admin_server_setup_wizard( struct http_request* req ) +{ + switch(g_server->setup_wizard_step) { + case 0: return handle_admin_server_setup(req); + case 1: return handle_admin_initial_owner_setup(req); + } + + return false; +} + diff --git a/src/controller/admin.h b/src/controller/admin.h index 3e46bc5..9f8ce11 100644 --- a/src/controller/admin.h +++ b/src/controller/admin.h @@ -6,4 +6,5 @@ struct http_request; bool route_admin_request( struct http_request* req ); bool handle_admin_server_setup( struct http_request* req ); +bool handle_admin_server_setup_wizard( struct http_request* req ); diff --git a/src/controller/main.c b/src/controller/main.c index a39a8cb..7394cf6 100644 --- a/src/controller/main.c +++ b/src/controller/main.c @@ -170,6 +170,9 @@ bool route_request( struct http_request* req ) char onion_location[512]; snprintf( onion_location,512, "http://%s%s", g_server->tor_hidden_service, http_request_get_full_path(req) ); http_request_add_header( req, "Onion-Location", onion_location ); + #define SOURCE_URL "https://gitea.polaris-1.work/teknomunk/apogee" + #define LICENSE_URL "https://www.gnu.org/licenses/agpl-3.0.html" + http_request_add_header( req, "Link", "<" LICENSE_URL ">; rel=\"license\", <" SOURCE_URL ">; rel=\"source\"" ); bool inner( struct http_request* req ) { if( http_request_route( req, "/oauth" ) ) { @@ -211,20 +214,26 @@ bool route_request( struct http_request* req ) return false; } - if( http_request_route( req, "/api/v1/" ) ) { - return route_mastodon_api( req ); - } else if( http_request_route( req, "/api/pleroma" ) ) { - return route_pleroma_api2( req ); - } else if( http_request_route( req, "/admin" ) ) { - return route_admin_request( req ); - } else if( inner( req ) ) { - return true; - } else if( http_request_route_term( req, "/" ) && !g_server->configured ) { - return handle_admin_server_setup( req ); + if( g_server->configured ) { + if( http_request_route( req, "/api/v1/" ) ) { + return route_mastodon_api( req ); + } else if( http_request_route( req, "/api/pleroma" ) ) { + return route_pleroma_api2( req ); + } else if( http_request_route( req, "/admin" ) ) { + return route_admin_request( req ); + } else if( inner( req ) ) { + return true; + } else { + return send_asset( req, "assets/soapbox/index.html" ); + } + + return false; } else { - return send_asset( req, "assets/soapbox/index.html" ); - } + if( http_request_route_term( req, "/" ) || http_request_route( req, "/?") ) { + return handle_admin_server_setup_wizard( req ); + } - return false; + return false; + } } diff --git a/src/model/owner.c b/src/model/owner.c index 2622cdd..0d576e2 100644 --- a/src/model/owner.c +++ b/src/model/owner.c @@ -3,6 +3,7 @@ #include "json/json.h" #include "json/layout.h" #include "sha256/sha256.h" +#include "form.h" #include #include @@ -32,10 +33,7 @@ struct owner* owner_new() { struct owner* o = allocate(sizeof(struct owner)); - if( !json_read_object_layout_from_file( "data/owner.json", owner_layout, o ) ) { - owner_free(o); - return NULL; - } + json_read_object_layout_from_file( "data/owner.json", owner_layout, o ); return o; } @@ -46,15 +44,24 @@ void owner_free( struct owner* o ) free(o->password_hash); free(o); } +void owner_save( struct owner* o ) +{ + json_write_object_layout_to_file( "data/owner.json", "\t", owner_layout, o ); +} bool owner_check_password( struct owner* o, const char* passwd ) { char buffer[512]; snprintf( buffer, 512, "%s:%s", o->password_salt, passwd ); + printf( "passwd='%s'\n", passwd ); + char hash[65] = ""; sha256_easy_hash_hex( buffer, strlen(buffer), hash ); + printf( "hash=%s\n", hash ); + printf( "expt=%s\n", o->password_hash ); + return 0 == strcmp(hash,o->password_hash); } @@ -64,6 +71,7 @@ void owner_set_password( struct owner* o, const char* passwd ) free(o->password_hash); char* new_salt = o->password_salt = malloc(65); + memset(new_salt,0,65); for( int i = 0; i < 64; ++i ) { new_salt[i] = 'a' + (rand()%26); } @@ -73,6 +81,10 @@ void owner_set_password( struct owner* o, const char* passwd ) snprintf( buffer, 512, "%s:%s", new_salt, passwd ); char* new_hash = o->password_hash = malloc(65); + memset(new_hash,0,65); sha256_easy_hash_hex( buffer, strlen(buffer), new_hash ); + + o->password_hash = new_hash; + o->password_salt = new_salt; } diff --git a/src/model/owner.h b/src/model/owner.h index 1dfe20d..320a52b 100644 --- a/src/model/owner.h +++ b/src/model/owner.h @@ -15,4 +15,5 @@ void owner_free( struct owner* ); bool owner_check_password( struct owner* o, const char* passwd ); void owner_set_password( struct owner* o, const char* passwd ); +struct form_pull_parser; diff --git a/src/model/server.c b/src/model/server.c index 7c8b274..40fb316 100644 --- a/src/model/server.c +++ b/src/model/server.c @@ -1,17 +1,20 @@ #include "server.h" -#include "json/layout.h" - #include "controller/cli.h" #include "process.h" +// Submodules +#include "json/layout.h" +#include "form.h" + +// Platform libraries #include #include #include #define OBJ_TYPE struct app_args static struct json_object_field app_args_layout[] = { - JSON_FIELD_STRING( domain, true ), + JSON_FIELD_STRING( icann_domain, false ), JSON_FIELD_STRING( user_agent, false ), { .key = "addr", @@ -29,8 +32,10 @@ static struct json_object_field app_args_layout[] = { JSON_FIELD_INTEGER( outbox_discard_limit, false ), JSON_FIELD_BOOL( develop, false ), - JSON_FIELD_BOOL( enabled, false ), + JSON_FIELD_BOOL( disabled, false ), JSON_FIELD_BOOL( configured, false ), + + JSON_FIELD_INTEGER( setup_wizard_step, false ), JSON_FIELD_END, }; #undef OBJ_TYPE @@ -55,28 +60,40 @@ void app_args_refresh_tor_hidden_service() } fclose(f); } -struct app_args* app_args_new( int argc, char** argv ) +void app_args_load() { - struct app_args* args = (struct app_args*)malloc( sizeof(struct app_args) ); - memset(args,0,sizeof(*args)); - if( !args ) { return NULL; } + g_server->http_settings.bind_port = 9000 + ( rand() % 41000 ); + g_server->http_settings.bind_address = strdup("0.0.0.0"); + g_server->tor_socks_port = ( rand() % 41000 ) + 9000; + g_server->section = -1; + g_server->outbox_discard_limit = 5; + g_server->user_agent = strdup( "Apogee/0.1" ); + g_server->disabled = true; + + json_read_object_layout_from_file( "data/server.json", app_args_layout, g_server ); - g_server = args; + app_args_refresh_tor_hidden_service(); - args->http_settings.bind_port = 9000 + ( rand() % 41000 ); - args->http_settings.bind_address = strdup("0.0.0.0"); - args->tor_socks_port = ( rand() % 41000 ) + 9000; - args->section = -1; - args->outbox_discard_limit = 5; - args->user_agent = strdup( "Apogee/0.1" ); + if( g_server->icann_domain ) { + g_server->domain = strdup(g_server->icann_domain); + } else if( g_server->tor_hidden_service ) { + g_server->domain = strdup(g_server->tor_hidden_service); + } +} +struct app_args* app_args_new( int argc, char** argv ) +{ + if( !g_server ) { + struct app_args* args = (struct app_args*)malloc( sizeof(struct app_args) ); + memset(args,0,sizeof(*args)); + if( !args ) { return NULL; } - json_read_object_layout_from_file( "data/server.json", app_args_layout, args ); + g_server = args; + } - app_args_refresh_tor_hidden_service(); + app_args_load(); if( ( argc > 1 ) && ( 0 != strncmp(argv[1],"--",2) ) ) { handle_command( argv, argc ); - free(args); return NULL; } @@ -84,14 +101,14 @@ struct app_args* app_args_new( int argc, char** argv ) const char* arg = argv[i]; // Debug flag - if( 0 == strcmp(argv[i],"--debug") ) { args->debug = true; goto next_arg; } + if( 0 == strcmp(argv[i],"--debug") ) { g_server->debug = true; goto next_arg; } // Sections by number - if( sscanf(arg,"--section=%d",&args->section) ) { goto next_arg; } + if( sscanf(arg,"--section=%d",&g_server->section) ) { goto next_arg; } // Override server port - if( sscanf(arg,"--listen=%d",&args->http_settings.bind_port) ) { - printf( "Listening on port %d\n", args->http_settings.bind_port ); + if( sscanf(arg,"--listen=%d",&g_server->http_settings.bind_port) ) { + printf( "Listening on port %d\n", g_server->http_settings.bind_port ); goto next_arg; } @@ -99,7 +116,7 @@ struct app_args* app_args_new( int argc, char** argv ) if( ( argv[i][0] == '-' ) && ( argv[i][1] == '-' ) ) { for( int i = 0; i <= process_get_max_section(); ++i ) { if( strcmp( &argv[i][2], process_get_section_name(i) ) ) { - args->section = i; + g_server->section = i; goto next_arg; } } @@ -107,32 +124,74 @@ struct app_args* app_args_new( int argc, char** argv ) // Development if( 0 == strcmp(argv[i],"--devel") ) { - args->section = 100; + g_server->section = 100; goto next_arg; } // Unknown argument printf( "Unknown argument: %s\n", argv[i] ); - free(args); return NULL; next_arg:; } - return args; + return g_server; } void app_args_release( struct app_args* args ) { free(args->http_settings.bind_address); free(args->domain); + free(args->icann_domain); free(args->tor_hidden_service); free(args->user_agent); free(args); } void app_args_save() { - // TODO: save server settings to disk + // Save to disk json_write_object_layout_to_file( "data/server.json", "\t", app_args_layout, g_server ); } +void app_args_load_from_form( struct app_args* args, struct form_parser* fp ) +{ + char* key = NULL; + while( (key=form_pull_parser_read_key(fp)) != NULL ) { + for( struct json_object_field* jof = app_args_layout; jof->key; jof += 1 ) { + intptr_t field_offset = (intptr_t)args + jof->offset; + + if( strcmp(jof->key, key) == 0 ) { + if( jof->type == &json_field_string ) { + const char* value = form_pull_parser_read_value(fp); + char** field = (char**)field_offset; + + printf( "%s: %s\n", jof->key, value ); + free( *field ); + *field = NULL; + + if( value ) { + *field = strdup(value); + } + } else if( jof->type == &json_field_integer ) { + const char* value = form_pull_parser_read_value(fp); + printf( "%s: %s\n", jof->key, value ); + + int* field = (int*)field_offset; + sscanf( value, "%d", field ); + } else if( jof->type == &json_field_bool ) { + const char* value = form_pull_parser_read_value(fp); + printf( "%s: %s\n", jof->key, value ); + + bool* field = (bool*)field_offset; + if( strcmp(value,"on") == 0 ) { + *field = true; + } else { + *field = false; + } + } else { + printf( "Unknown type for field %s\n", jof->key ); + } + } + } + } +} diff --git a/src/model/server.h b/src/model/server.h index a62a56f..f3d26f6 100644 --- a/src/model/server.h +++ b/src/model/server.h @@ -11,6 +11,7 @@ struct app_args struct http_server_args http_settings; //char* addr; char* domain; + char* icann_domain; char* tor_hidden_service; char* user_agent; bool debug; @@ -21,12 +22,17 @@ struct app_args int outbox_discard_limit; bool develop; - bool enabled; + bool disabled; bool configured; + int setup_wizard_step; }; struct app_args* app_args_new( int argc, char** argv ); +void app_args_load(); void app_args_release( struct app_args* args ); void app_args_save(); + +struct form_parser; +void app_args_load_from_form( struct app_args* args, struct form_parser* fp ); extern struct app_args* g_server; diff --git a/src/view/admin/owner-setup.html.template b/src/view/admin/owner-setup.html.template new file mode 100644 index 0000000..9192198 --- /dev/null +++ b/src/view/admin/owner-setup.html.template @@ -0,0 +1,34 @@ +%(/* + vim: filetype=html +*/) + +

Welcome to Apogee

+

+Apogee is a single-user Activity Pub federated server intended for self-hosting. +

+ +

Account Settings

+
+ + + + + + + + + + + + + + + + +
Username:
Password:
Confirm Password:
+ +
+ +
+ + diff --git a/src/view/admin/server-setup.html.template b/src/view/admin/server-setup.html.template index 6c138e7..44d5825 100644 --- a/src/view/admin/server-setup.html.template +++ b/src/view/admin/server-setup.html.template @@ -8,13 +8,14 @@ Apogee is a single-user Activity Pub federated server intended for self-hosting.

Server Settings

-
+

HTTP

- - + + + @@ -64,8 +65,8 @@ Apogee is a single-user Activity Pub federated server intended for self-hosting. - - + +
Domain Name:ICANN Domain Name:
Listen Address: Change server behavior to suit developers
Server Enabled:enabled ) }/>Server Disabled:disabled ) }/> Turn the entire server off