summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-02-18 18:34:02 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-02-18 18:34:02 +0100
commit5fbbd990d6b42f5e7534cdb68fa5c368a4a69169 (patch)
tree3db3b955fcceb71fbb8ea161643a3a1044b2e4f2
parent78745e06f4a4365e9caa8ee12fb0d9e15fb59300 (diff)
Generate secrets for clients
-rw-r--r--src/web.c103
-rw-r--r--src/web.h3
-rw-r--r--test/http.c68
3 files changed, 157 insertions, 17 deletions
diff --git a/src/web.c b/src/web.c
index 8c37bfe..df2dc81 100644
--- a/src/web.c
+++ b/src/web.c
@@ -13,6 +13,7 @@
#include <microhttpd.h>
#include <sqlite3.h>
#include <cjson/cJSON.h>
+#include <fcntl.h>
#define PAGE "<html><head><title>libmicrohttpd demo</title>"\
"</head><body>libmicrohttpd demo</body></html>"
@@ -22,6 +23,21 @@ static void sqliteError(void *pArg, int iErrCode, const char *zMsg) {
fprintf(stderr, "(%d) %s\n", iErrCode, zMsg);
}
+static int generate_secret(char *out) {
+ unsigned char bytes[16];
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0) return -1;
+ ssize_t n = read(fd, bytes, 16);
+ close(fd);
+ if (n != 16) return -1;
+ static const char hex[] = "0123456789abcdef";
+ for (int i = 0; i < 16; i++) {
+ out[i*2] = hex[(bytes[i] >> 4) & 0x0f];
+ out[i*2+1] = hex[bytes[i] & 0x0f];
+ }
+ out[32] = '\0';
+ return 0;
+}
void prepare_database(struct App *app) {
// @CLEANUP: Move this to main? doesn't belong here at least
@@ -176,6 +192,36 @@ void prepare_database(struct App *app) {
abort();
}
}
+
+ if(maxVersion < 4) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "ALTER TABLE clients ADD COLUMN secret TEXT NOT NULL DEFAULT ''";
+ if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_int64(stmt, 1, 4) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+ }
}
enum ReportEvent {
@@ -184,7 +230,8 @@ enum ReportEvent {
};
// @CLEANUP: This should live somewhere else, but I don't have a spot for it yet.
-int submit_report(struct App *app, char *client, enum ReportEvent event) {
+// Returns 0 on success, -1 on error, -2 on secret mismatch
+int submit_report(struct App *app, char *client, char *secret, enum ReportEvent event) {
sqlite3 *conn;
int err;
if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
@@ -196,7 +243,7 @@ int submit_report(struct App *app, char *client, enum ReportEvent event) {
int64_t clientId;
{
sqlite3_stmt *stmt;
- if(sqlite3_prepare_v2(conn, "SELECT id FROM clients WHERE name = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ if(sqlite3_prepare_v2(conn, "SELECT id, secret FROM clients WHERE name = ?1", -1, &stmt, NULL) != SQLITE_OK) {
abort();
}
if(sqlite3_bind_text(stmt, 1, client, -1, SQLITE_STATIC) != SQLITE_OK) {
@@ -212,6 +259,13 @@ int submit_report(struct App *app, char *client, enum ReportEvent event) {
}
clientId = sqlite3_column_int64(stmt, 0);
+ const char *stored_secret = (const char *)sqlite3_column_text(stmt, 1);
+ if(stored_secret == NULL || strcmp(stored_secret, secret) != 0) {
+ sqlite3_finalize(stmt);
+ sqlite3_close(conn);
+ return -2;
+ }
+
if(sqlite3_step(stmt) != SQLITE_DONE) {
abort();
}
@@ -313,14 +367,21 @@ int create_client(struct Request *req, char *name, struct CreateClientResult *re
abort();
}
+ if(generate_secret(result->secret) != 0) {
+ abort();
+ }
+
{
sqlite3_stmt *stmt;
- if(sqlite3_prepare_v2(conn, "INSERT INTO clients (name) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ if(sqlite3_prepare_v2(conn, "INSERT INTO clients (name, secret) VALUES (?1, ?2)", -1, &stmt, NULL) != SQLITE_OK) {
abort();
}
if(sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC) != SQLITE_OK) {
abort();
}
+ if(sqlite3_bind_text(stmt, 2, result->secret, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
if(sqlite3_step(stmt) != SQLITE_DONE) {
abort();
@@ -373,7 +434,7 @@ int list_clients(struct Request *req, struct ListClientResult *result) {
{
sqlite3_stmt *stmt;
- if(sqlite3_prepare_v2(conn, "SELECT c.id, c.name FROM clients c WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) {
+ if(sqlite3_prepare_v2(conn, "SELECT c.id, c.name, c.secret FROM clients c WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) {
abort();
}
if(sqlite3_bind_int64(stmt, 1, -1) != SQLITE_OK) {
@@ -394,9 +455,11 @@ int list_clients(struct Request *req, struct ListClientResult *result) {
int64_t clientId = sqlite3_column_int64(stmt, 0);
const uint8_t *name = sqlite3_column_text(stmt, 1);
+ const uint8_t *secret = sqlite3_column_text(stmt, 2);
result->clients[index].id = clientId;
strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX);
+ strncpy(result->clients[index].secret, (char*)secret, CLIENT_SECRET_LEN);
index++;
}
@@ -443,12 +506,13 @@ static char *render_clients_page(struct Request *request, size_t *out_len) {
"<body>\n"
" <h1>Clients</h1>\n"
" <table border=\"1\">\n"
- " <tr><th>Name</th><th>Actions</th></tr>\n");
+ " <tr><th>Name</th><th>Secret</th><th>Actions</th></tr>\n");
for (int i = 0; i < result->clients_len; i++) {
fprintf(f,
" <tr>\n"
" <td>%s</td>\n"
+ " <td>%s</td>\n"
" <td>\n"
" <form method=\"POST\" action=\"/clients/delete\" style=\"display:inline\">\n"
" <input type=\"hidden\" name=\"client_id\" value=\"%ld\">\n"
@@ -457,6 +521,7 @@ static char *render_clients_page(struct Request *request, size_t *out_len) {
" </td>\n"
" </tr>\n",
result->clients[i].name,
+ result->clients[i].secret,
result->clients[i].id
);
}
@@ -639,6 +704,11 @@ static enum MHD_Result handler(
abort();
}
+ cJSON *secretJson = cJSON_GetObjectItemCaseSensitive(rootJson, "secret");
+ if(!cJSON_IsString(secretJson) || secretJson->valuestring == NULL) {
+ abort();
+ }
+
cJSON *eventJson = cJSON_GetObjectItemCaseSensitive(rootJson, "event");
if(!cJSON_IsString(eventJson) || eventJson->valuestring == NULL) {
abort();
@@ -653,7 +723,26 @@ static enum MHD_Result handler(
}
- ret = submit_report(server->app, clientJson->valuestring, event);
+ ret = submit_report(server->app, clientJson->valuestring, secretJson->valuestring, event);
+ if(ret == -2) {
+ cJSON_Delete(rootJson);
+ destroy_post_collector(&request->pp);
+
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ 0,
+ "",
+ MHD_RESPMEM_PERSISTENT
+ );
+ ret = MHD_add_response_header(response, "Content-Type", "application/json");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_UNAUTHORIZED,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ }
if(ret != 0) {
// Should actually return an error page, but this will do for now
return MHD_NO;
@@ -697,7 +786,7 @@ static enum MHD_Result handler(
destroy_post_collector(&request->pp);
char *buf = malloc(1024);
- size_t buf_len = snprintf(buf, 1024, "{ \"id\": \"%ld\" }", result.id);
+ size_t buf_len = snprintf(buf, 1024, "{ \"id\": \"%ld\", \"secret\": \"%s\" }", result.id, result.secret);
if(buf_len >= 1024) {
abort();
}
diff --git a/src/web.h b/src/web.h
index 96e5f7f..95c0e27 100644
--- a/src/web.h
+++ b/src/web.h
@@ -21,6 +21,7 @@ int web_begin(struct Server *server, int port);
int web_join(struct Server *server);
#define CLIENT_NAME_MAX 32
+#define CLIENT_SECRET_LEN 33
struct PostCollector {
char *data;
@@ -45,6 +46,7 @@ struct Request {
struct CreateClientResult {
int64_t id;
char* name;
+ char secret[CLIENT_SECRET_LEN];
};
int create_client(struct Request *req, char *name, struct CreateClientResult *result);
@@ -53,6 +55,7 @@ int delete_client(struct Request *req, int64_t client_id);
struct ListClientResultClient {
int64_t id;
char name[CLIENT_NAME_MAX];
+ char secret[CLIENT_SECRET_LEN];
};
struct ListClientResult {
diff --git a/test/http.c b/test/http.c
index fa7755d..8309d5e 100644
--- a/test/http.c
+++ b/test/http.c
@@ -6,6 +6,7 @@
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
+#include <cjson/cJSON.h>
// @PASTE Stolen from libcurl documentation
struct memory {
@@ -83,6 +84,7 @@ int main(int argc, char **argv) {
}
+ char client_secret[CLIENT_SECRET_LEN] = {0};
{
curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/client");
assert(curlRes == CURLE_OK);
@@ -124,12 +126,18 @@ int main(int argc, char **argv) {
assert(strcmp(ct, "application/json") == 0);
assert(body.size >= 0);
- assert(strcmp(
- body.body,
- "{ "
- "\"id\": \"1\" "
- "}"
- ) == 0);
+ // Parse response to extract secret
+ cJSON *rootJson = cJSON_ParseWithLength(body.body, body.size);
+ assert(rootJson != NULL);
+ cJSON *idJson = cJSON_GetObjectItemCaseSensitive(rootJson, "id");
+ assert(cJSON_IsString(idJson));
+ assert(strcmp(idJson->valuestring, "1") == 0);
+ cJSON *secretJson = cJSON_GetObjectItemCaseSensitive(rootJson, "secret");
+ assert(cJSON_IsString(secretJson));
+ assert(strlen(secretJson->valuestring) == 32);
+ strncpy(client_secret, secretJson->valuestring, CLIENT_SECRET_LEN);
+ cJSON_Delete(rootJson);
+ free(body.body);
body = (struct memory){0};
}
@@ -146,9 +154,11 @@ int main(int argc, char **argv) {
curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory);
assert(curlRes == CURLE_OK);
+ char req_buf[256];
+ snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"begin_backup\"}", client_secret);
struct memory req_body = {
- .body = "{\"client\": \"client1\", \"event\": \"begin_backup\"}",
- .size = strlen(req_body.body),
+ .body = req_buf,
+ .size = strlen(req_buf),
};
curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body);
assert(curlRes == CURLE_OK);
@@ -192,9 +202,11 @@ int main(int argc, char **argv) {
curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory);
assert(curlRes == CURLE_OK);
+ char req_buf[256];
+ snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"end_backup\"}", client_secret);
struct memory req_body = {
- .body = "{\"client\": \"client1\", \"event\": \"end_backup\"}",
- .size = strlen(req_body.body),
+ .body = req_buf,
+ .size = strlen(req_buf),
};
curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body);
assert(curlRes == CURLE_OK);
@@ -223,5 +235,41 @@ int main(int argc, char **argv) {
body = (struct memory){0};
}
+ // Test: wrong secret should return 401
+ {
+ curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/report");
+ assert(curlRes == CURLE_OK);
+
+ curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
+ assert(curlRes == CURLE_OK);
+
+ curlRes = curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ assert(curlRes == CURLE_OK);
+
+ curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory);
+ assert(curlRes == CURLE_OK);
+
+ struct memory req_body = {
+ .body = "{\"client\": \"client1\", \"secret\": \"wrongsecret\", \"event\": \"begin_backup\"}",
+ .size = strlen("{\"client\": \"client1\", \"secret\": \"wrongsecret\", \"event\": \"begin_backup\"}"),
+ };
+ curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body);
+ assert(curlRes == CURLE_OK);
+
+ curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory);
+ assert(curlRes == CURLE_OK);
+
+ struct memory body = {0};
+ curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
+ assert(curlRes == CURLE_OK);
+
+ curlRes = curl_easy_perform(curl);
+ assert(curlRes == CURLE_OK);
+
+ long code;
+ curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
+ assert(code == 401); // Unauthorized
+ }
+
web_join(&server);
}