diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | ci/build.yaml | 6 | ||||
| -rw-r--r-- | src/main.c | 10 | ||||
| -rw-r--r-- | src/web.c | 191 | ||||
| -rw-r--r-- | src/web.h | 15 | ||||
| -rw-r--r-- | test/http.c | 94 |
6 files changed, 299 insertions, 19 deletions
@@ -7,7 +7,7 @@ OBJDIR ?= obj PACKAGES = libmicrohttpd libcjson sqlite3 -LIBS = -lm +LIBS = -lm -lcrypt INCS = -Isrc/ -Igen/ -I. CFLAGS ?= -O3 -D_FORTIFY_SOURCE=2 -Wall -g diff --git a/ci/build.yaml b/ci/build.yaml index 4d19a18..7367fe4 100644 --- a/ci/build.yaml +++ b/ci/build.yaml @@ -4,14 +4,14 @@ resources: source: uri: git@github.com:DelusionalLogic/borgflag.git branch: main - private_key: ((vbump.borgflag-key)) + private_key: ((borgflag.borgflag-key)) - name: final-image type: registry-image icon: docker source: repository: registry.jnsn.dev/borgflag - username: ((vbump.username)) - password: ((vbump.password)) + username: ((borgflag.username)) + password: ((borgflag.password)) tag: latest jobs: @@ -9,6 +9,14 @@ int main(int argc, char ** argv) { return 1; } + const char *admin_user = getenv("ADMIN_USER"); + const char *admin_pass = getenv("ADMIN_PASS"); + + if (admin_user == NULL || admin_pass == NULL) { + fprintf(stderr, "Error: ADMIN_USER and ADMIN_PASS environment variables must be set\n"); + return 1; + } + struct App app = { .dbname = "borgflag.db" }; @@ -17,7 +25,7 @@ int main(int argc, char ** argv) { server.app = &app; prepare_database(&app); - web_begin(&server, atoi(argv[1])); + web_begin(&server, atoi(argv[1]), admin_user, admin_pass); getchar(); web_join(&server); } @@ -14,6 +14,7 @@ #include <sqlite3.h> #include <cjson/cJSON.h> #include <fcntl.h> +#include <crypt.h> #define PAGE "<html><head><title>libmicrohttpd demo</title>"\ "</head><body>libmicrohttpd demo</body></html>" @@ -39,6 +40,26 @@ static int generate_secret(char *out) { return 0; } +static char *generate_session_token(struct App *app, const char *username, const char *password) { + char combined[128]; + snprintf(combined, sizeof(combined), "%s:%s", username, password); + return crypt(combined, app->session_secret); +} + +static int validate_credentials(struct App *app, const char *username, const char *password) { + return strcmp(username, app->admin_user) == 0 && strcmp(password, app->admin_pass) == 0; +} + +static int is_authenticated(struct App *app, struct MHD_Connection *connection) { + const char *cookie = MHD_lookup_connection_value(connection, MHD_COOKIE_KIND, "session"); + if (cookie == NULL) return 0; + + char *expected = generate_session_token(app, app->admin_user, app->admin_pass); + if (expected == NULL) return 0; + + return strcmp(cookie, expected) == 0; +} + void prepare_database(struct App *app) { // @CLEANUP: Move this to main? doesn't belong here at least sqlite3_config(SQLITE_CONFIG_LOG, sqliteError, NULL); @@ -614,6 +635,9 @@ static char *render_clients_page(struct Request *request, size_t *out_len) { "</head>\n" "<body>\n" " <h1>Clients</h1>\n" + " <form method=\"POST\" action=\"/logout\" style=\"float:right;margin-top:-40px;\">\n" + " <button type=\"submit\">Logout</button>\n" + " </form>\n" " <table border=\"1\">\n" " <tr><th>Name</th><th>Secret</th><th>Actions</th></tr>\n"); @@ -745,6 +769,39 @@ static char *render_client_detail_page(struct Request *request, int64_t client_i return buf; } +static char *render_login_page(const char *error_msg, size_t *out_len) { + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) return NULL; + + fprintf(f, + "<!DOCTYPE html>\n" + "<html>\n" + "<head>\n" + " <title>Login</title>\n" + "</head>\n" + "<body>\n" + " <h1>Login</h1>\n"); + + if (error_msg) { + fprintf(f, " <p style=\"color: red;\">%s</p>\n", error_msg); + } + + fprintf(f, + " <form method=\"POST\" action=\"/login\">\n" + " <p><label>Username: <input type=\"text\" name=\"username\" required></label></p>\n" + " <p><label>Password: <input type=\"password\" name=\"password\" required></label></p>\n" + " <p><button type=\"submit\">Login</button></p>\n" + " </form>\n" + "</body>\n" + "</html>\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} + static enum MHD_Result create_post_collector(struct PostCollector *c) { c->data = malloc(512); c->size = 0; @@ -799,6 +856,14 @@ form_iterator(void *cls, memcpy(buf, data, copy_size); request->form.client_id = strtoll(buf, NULL, 10); request->form.client_id_set = 1; + } else if (strcmp(key, "username") == 0 && off + size < 32) { + memcpy(request->form.username + off, data, size); + request->form.username[off + size] = '\0'; + request->form.username_set = 1; + } else if (strcmp(key, "password") == 0 && off + size < 64) { + memcpy(request->form.password + off, data, size); + request->form.password[off + size] = '\0'; + request->form.password_set = 1; } return MHD_YES; @@ -831,7 +896,7 @@ static enum MHD_Result handler( request->host = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Host"); if(strcmp(method, "POST") == 0) { - if(strcmp(url, "/clients") == 0 || strcmp(url, "/clients/delete") == 0) { + if(strcmp(url, "/clients") == 0 || strcmp(url, "/clients/delete") == 0 || strcmp(url, "/login") == 0) { request->post_proc = MHD_create_post_processor( connection, 1024, &form_iterator, request); if (request->post_proc == NULL) { @@ -861,10 +926,71 @@ static enum MHD_Result handler( } } - if(strcmp(url, "/clients") == 0) { + if(strcmp(url, "/logout") == 0) { + struct MHD_Response *response = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Set-Cookie", "session=; Path=/; HttpOnly; Max-Age=0"); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_SEE_OTHER, response); + MHD_destroy_response(response); + return ret; + } else if(strcmp(url, "/login") == 0) { MHD_destroy_post_processor(request->post_proc); request->post_proc = NULL; + if (!request->form.username_set || !request->form.password_set) { + size_t html_len; + char *html = render_login_page("Invalid credentials", &html_len); + if (!html) return MHD_NO; + struct MHD_Response *response = MHD_create_response_from_buffer( + html_len, html, MHD_RESPMEM_MUST_FREE); + ret = MHD_add_response_header(response, "Content-Type", "text/html"); + if(ret != MHD_YES) return ret; + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; + } + + if (!validate_credentials(server->app, request->form.username, request->form.password)) { + size_t html_len; + char *html = render_login_page("Invalid credentials", &html_len); + if (!html) return MHD_NO; + struct MHD_Response *response = MHD_create_response_from_buffer( + html_len, html, MHD_RESPMEM_MUST_FREE); + ret = MHD_add_response_header(response, "Content-Type", "text/html"); + if(ret != MHD_YES) return ret; + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; + } + + // Valid credentials - set cookie and redirect + char *token = generate_session_token(server->app, request->form.username, request->form.password); + if (token == NULL) return MHD_NO; + + char cookie_header[128]; + snprintf(cookie_header, sizeof(cookie_header), "session=%s; Path=/; HttpOnly", token); + + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Set-Cookie", cookie_header); + MHD_add_response_header(response, "Location", "/clients"); + ret = MHD_queue_response(connection, MHD_HTTP_SEE_OTHER, response); + MHD_destroy_response(response); + return ret; + } else if(strcmp(url, "/clients") == 0) { + MHD_destroy_post_processor(request->post_proc); + request->post_proc = NULL; + + // Auth check + if (!is_authenticated(server->app, connection)) { + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + return ret; + } + if (!request->form.name_set || request->form.name[0] == '\0') { return MHD_NO; } @@ -885,6 +1011,16 @@ static enum MHD_Result handler( MHD_destroy_post_processor(request->post_proc); request->post_proc = NULL; + // Auth check + if (!is_authenticated(server->app, connection)) { + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + return ret; + } + if (!request->form.client_id_set || request->form.client_id <= 0) { return MHD_NO; } @@ -1031,7 +1167,28 @@ static enum MHD_Result handler( } else if (strcmp(method, "GET") == 0) { if (*upload_data_size != 0) return MHD_NO; - if(strcmp(url, "/clients") == 0) { + if(strcmp(url, "/login") == 0) { + size_t html_len; + char *html = render_login_page(NULL, &html_len); + if (!html) return MHD_NO; + struct MHD_Response *response = MHD_create_response_from_buffer( + html_len, html, MHD_RESPMEM_MUST_FREE); + ret = MHD_add_response_header(response, "Content-Type", "text/html"); + if(ret != MHD_YES) return ret; + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; + } else if(strcmp(url, "/clients") == 0) { + // Auth check + if (!is_authenticated(server->app, connection)) { + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + return ret; + } + size_t html_len; char *html = render_clients_page(request, &html_len); @@ -1054,6 +1211,16 @@ static enum MHD_Result handler( MHD_destroy_response(response); return ret; } else if (strncmp(url, "/client/", 8) == 0) { + // Auth check + if (!is_authenticated(server->app, connection)) { + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + return ret; + } + int64_t client_id; if (sscanf(url + 8, "%ld", &client_id) != 1 || client_id <= 0) { // Bad Request @@ -1091,6 +1258,16 @@ static enum MHD_Result handler( MHD_destroy_response(response); return ret; } else { + // Auth check for default page + if (!is_authenticated(server->app, connection)) { + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(response, "Location", "/login"); + ret = MHD_queue_response(connection, MHD_HTTP_FOUND, response); + MHD_destroy_response(response); + return ret; + } + struct MHD_Response *response = MHD_create_response_from_buffer( strlen(server->page), (void *)server->page, @@ -1127,10 +1304,16 @@ static void request_completed_callback ( static struct MHD_Daemon *d = NULL; -int web_begin(struct Server *server, int port) { +int web_begin(struct Server *server, int port, const char *admin_user, const char *admin_pass) { assert(d == NULL); server->page = PAGE; + server->app->admin_user = admin_user; + server->app->admin_pass = admin_pass; + + if (generate_secret(server->app->session_secret) != 0) { + return 1; + } d = MHD_start_daemon( MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK, @@ -5,8 +5,14 @@ struct MHD_PostProcessor; +#define CLIENT_NAME_MAX 32 +#define CLIENT_SECRET_LEN 33 + struct App { char* dbname; + char session_secret[CLIENT_SECRET_LEN]; + const char *admin_user; + const char *admin_pass; }; void prepare_database(struct App *app); @@ -17,12 +23,9 @@ struct Server { char *page; }; -int web_begin(struct Server *server, int port); +int web_begin(struct Server *server, int port, const char *admin_user, const char *admin_pass); int web_join(struct Server *server); -#define CLIENT_NAME_MAX 32 -#define CLIENT_SECRET_LEN 33 - struct PostCollector { char *data; size_t size; @@ -34,6 +37,10 @@ struct FormData { int name_set; int64_t client_id; // For delete operations int client_id_set; + char username[32]; + int username_set; + char password[64]; + int password_set; }; struct Request { diff --git a/test/http.c b/test/http.c index 27385ab..fa8650d 100644 --- a/test/http.c +++ b/test/http.c @@ -54,12 +54,13 @@ int main(int argc, char **argv) { struct Server server = { .app = &app, }; - web_begin(&server, 8080); + web_begin(&server, 8080, "admin", "changeme"); curl_global_init(CURL_GLOBAL_ALL); CURLcode curlRes; CURL *curl = curl_easy_init(); assert(curl != NULL); + // Default page requires auth, should redirect { curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/"); assert(curlRes == CURLE_OK); @@ -67,6 +68,9 @@ int main(int argc, char **argv) { curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); + curlRes = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + assert(curlRes == CURLE_OK); + struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); @@ -76,11 +80,7 @@ int main(int argc, char **argv) { long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); - assert(code == 200); - - char *ct; - curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); - assert(curlRes == CURLE_OK); + assert(code == 302); // Redirect to login } @@ -342,5 +342,87 @@ int main(int argc, char **argv) { free(result); } + // Test: Full login flow + { + // Use fresh curl with cookie jar + CURL *auth_curl = curl_easy_init(); + assert(auth_curl != NULL); + curl_easy_setopt(auth_curl, CURLOPT_COOKIEFILE, ""); // Enable cookie engine + curl_easy_setopt(auth_curl, CURLOPT_FOLLOWLOCATION, 0L); + curl_easy_setopt(auth_curl, CURLOPT_WRITEFUNCTION, write_to_memory); + struct memory body = {0}; + curl_easy_setopt(auth_curl, CURLOPT_WRITEDATA, &body); + + // Step 1: Unauthenticated access redirects to /login + curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); + curl_easy_perform(auth_curl); + long code; + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 302); + + // Step 2: Wrong credentials show error (not redirect) + free(body.body); body = (struct memory){0}; + curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/login"); + curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, "username=admin&password=wrong"); + curl_easy_perform(auth_curl); + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 200); + assert(strstr(body.body, "Invalid") != NULL); + + // Step 3: Correct credentials redirect and set cookie + free(body.body); body = (struct memory){0}; + curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, "username=admin&password=changeme"); + curl_easy_perform(auth_curl); + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 303); + + // Step 4: Authenticated access works + free(body.body); body = (struct memory){0}; + curl_easy_setopt(auth_curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); + curl_easy_perform(auth_curl); + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 200); + assert(strstr(body.body, "Clients") != NULL); + + // Step 5: Logout clears cookie and redirects + free(body.body); body = (struct memory){0}; + curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/logout"); + curl_easy_setopt(auth_curl, CURLOPT_POST, 1L); + curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, ""); + curl_easy_perform(auth_curl); + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 303); // Redirect to login + + // Step 6: No longer authenticated after logout + free(body.body); body = (struct memory){0}; + curl_easy_setopt(auth_curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); + curl_easy_perform(auth_curl); + curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 302); // Redirect to login (not authenticated) + + free(body.body); + curl_easy_cleanup(auth_curl); + } + + // Test: Invalid session cookie rejected + { + CURL *bad_curl = curl_easy_init(); + assert(bad_curl != NULL); + curl_easy_setopt(bad_curl, CURLOPT_FOLLOWLOCATION, 0L); + curl_easy_setopt(bad_curl, CURLOPT_COOKIE, "session=invalidtoken"); + curl_easy_setopt(bad_curl, CURLOPT_URL, "http://localhost:8080/clients"); + curl_easy_setopt(bad_curl, CURLOPT_WRITEFUNCTION, write_to_memory); + struct memory body = {0}; + curl_easy_setopt(bad_curl, CURLOPT_WRITEDATA, &body); + curl_easy_perform(bad_curl); + long code; + curl_easy_getinfo(bad_curl, CURLINFO_RESPONSE_CODE, &code); + assert(code == 302); // Redirect to login + free(body.body); + curl_easy_cleanup(bad_curl); + } + web_join(&server); } |
