summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--ci/build.yaml6
-rw-r--r--src/main.c10
-rw-r--r--src/web.c191
-rw-r--r--src/web.h15
-rw-r--r--test/http.c94
6 files changed, 299 insertions, 19 deletions
diff --git a/Makefile b/Makefile
index d5ca000..0067f0a 100644
--- a/Makefile
+++ b/Makefile
@@ -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:
diff --git a/src/main.c b/src/main.c
index 17b4727..2cd5ceb 100644
--- a/src/main.c
+++ b/src/main.c
@@ -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);
}
diff --git a/src/web.c b/src/web.c
index e0d6d55..c55278a 100644
--- a/src/web.c
+++ b/src/web.c
@@ -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,
diff --git a/src/web.h b/src/web.h
index 27dcde3..3a6e315 100644
--- a/src/web.h
+++ b/src/web.h
@@ -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);
}