summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-02-16 21:43:36 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-02-16 21:43:36 +0100
commitc9e3a4b2816ad23aa201a4f43f357a376947bd49 (patch)
tree042530b79b1e3c7c36872b991850002bec9268a8
INITIAL COMMIT
-rw-r--r--.gitignore6
-rw-r--r--Makefile79
-rw-r--r--src/main.c23
-rw-r--r--src/mytime.c38
-rw-r--r--src/mytime.h12
-rw-r--r--src/web.c724
-rw-r--r--src/web.h17
-rw-r--r--test/http.c227
8 files changed, 1126 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..583c39e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/obj
+/app
+
+.cache/
+compile_commands.json
+borgflag.db
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d5ca000
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,79 @@
+CC ?= gcc
+
+SRCDIR ?= src
+TSTDIR ?= test
+GENDIR ?= gen
+OBJDIR ?= obj
+
+PACKAGES = libmicrohttpd libcjson sqlite3
+
+LIBS = -lm
+INCS = -Isrc/ -Igen/ -I.
+
+CFLAGS ?= -O3 -D_FORTIFY_SOURCE=2 -Wall -g
+CFLAGS += -std=gnu11 -fms-extensions -flto
+
+APP_MAIN_SOURCES = src/main.c
+APP_SOURCES = $(filter-out $(APP_MAIN_SOURCES),$(shell find $(SRCDIR) -name "*.c"))
+APP_OBJS = $(APP_SOURCES:%.c=$(OBJDIR)/%.o)
+APP_MAIN_OBJS = $(APP_MAIN_SOURCES:%.c=$(OBJDIR)/%.o)
+APP_DEPS = $(APP_OBJS:%.o=%.d)
+APP_MAIN_DEPS = $(APP_MAIN_OBJS:%.o=%.d)
+
+TEST_LIB_SOURCES =
+TEST_LIB_OBJS = $(TEST_LIB_SOURCES:%.c=$(OBJDIR)/%.o)
+TEST_LIB_DEPS = $(TEST_LIB_SOURCES:%.c=%.d)
+TEST_LIB_INCS =
+TEST_LIB_CFLAGS =
+TEST_LIB_LIBS = -lcurl
+
+TEST_SOURCES = $(shell find $(TSTDIR) -name "*.c")
+TEST_EXES = $(TEST_SOURCES:%.c=$(OBJDIR)/%)
+TEST_DEPS = $(TEST_SOURCES:%.c=%.d)
+
+LIBS += $(shell pkg-config --libs $(PACKAGES))
+INCS += $(shell pkg-config --cflags $(PACKAGES))
+
+print-% : ; @echo $* = $($*)
+
+-include $(APP_DEPS) $(TEST_LIB_DEPS) $(TEST_DEPS)
+
+# We don't really need to run the tests for bear to record them
+compile_commands.json: clean Makefile $(APP_SOURCES) $(TEST_LIB_SOURCES) $(TEST_SOURCES)
+ @rm -f "$@"
+ bear -- make $(TEST_EXES) app
+
+app: $(APP_MAIN_OBJS) $(APP_OBJS)
+ $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $(APP_MAIN_OBJS) $(APP_OBJS) $(LIBS)
+
+$(OBJDIR)/%.o: %.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) $(INCS) -MMD -o $@ -c $<
+
+clean:
+ @rm -rf $(OBJDIR)
+ @rm -f app
+
+.PHONY: version
+version:
+ @echo "$(COMPTON_VERSION)"
+
+# Tests!
+# Run all tests
+.PHONY: test
+test: $(TEST_EXES)
+ $(foreach test,$(TEST_EXES),./$(test) &&) true
+
+$(OBJDIR)/test/%: $(APP_OBJS) $(TEST_LIB_OBJS) $(OBJDIR)/test/%.o
+ $(CC) $(LDFLAGS) $(TEST_LIB_CFLAGS) $(CFLAGS) -o $@ $^ $(LIBS) $(TEST_LIB_LIBS)
+
+$(OBJDIR)/test/%.o: test/%.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) $(TEST_LIB_CFLAGS) $(TEST_LIB_INCS) $(INCS) -MMD -o $@ -c $<
+
+$(OBJDIR)/test/%.o: $(OBJDIR)/test/%.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) $(TEST_LIB_CFLAGS) $(TEST_LIB_INCS) $(INCS) -MMD -o $@ -c $<
+
+.DEFAULT_GOAL := all
+all: test app
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..17b4727
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,23 @@
+#include "web.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(int argc, char ** argv) {
+ if (argc != 2) {
+ printf("%s PORT\n", argv[0]);
+ return 1;
+ }
+
+ struct App app = {
+ .dbname = "borgflag.db"
+ };
+ struct Server server;
+
+ server.app = &app;
+
+ prepare_database(&app);
+ web_begin(&server, atoi(argv[1]));
+ getchar();
+ web_join(&server);
+}
diff --git a/src/mytime.c b/src/mytime.c
new file mode 100644
index 0000000..ffb7323
--- /dev/null
+++ b/src/mytime.c
@@ -0,0 +1,38 @@
+#include "mytime.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+bool fake_time;
+struct timespec global_time;
+
+struct timespec get_current_time() {
+ if(fake_time) {
+ return global_time;
+ }
+
+ struct timespec val;
+ if(clock_gettime(CLOCK_REALTIME, &val) != 0) {
+ abort();
+ }
+
+ return val;
+}
+
+void enable_fake_time() {
+ assert(!fake_time);
+
+ global_time = (struct timespec){
+ .tv_sec = 0,
+ .tv_nsec = 0,
+ };
+ fake_time = true;
+}
+
+void progress_time(uint64_t diff) {
+ assert(fake_time);
+
+ // Right now the diff is only seconds resolution
+ global_time.tv_sec += diff;
+}
diff --git a/src/mytime.h b/src/mytime.h
new file mode 100644
index 0000000..48eba06
--- /dev/null
+++ b/src/mytime.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <stdint.h>
+#include <time.h>
+
+#define TIME_JOIN(H, M, S) ((H) * 3600 + (M) * 60 + (S))
+#define TIME_AS_FLOAT(T) (((double)T.tv_sec) + ((double)T.tv_nsec / 1000000000.0))
+
+struct timespec get_current_time();
+
+void enable_fake_time();
+void progress_time(uint64_t diff);
diff --git a/src/web.c b/src/web.c
new file mode 100644
index 0000000..810d5d0
--- /dev/null
+++ b/src/web.c
@@ -0,0 +1,724 @@
+#include "web.h"
+#include "mytime.h"
+
+#include <assert.h>
+#include <sys/socket.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <microhttpd.h>
+#include <sqlite3.h>
+#include <cjson/cJSON.h>
+
+#define PAGE "<html><head><title>libmicrohttpd demo</title>"\
+ "</head><body>libmicrohttpd demo</body></html>"
+
+static void sqliteError(void *pArg, int iErrCode, const char *zMsg) {
+ // @CLEANUP: We should stuff these somewhere, but for now just print it
+ fprintf(stderr, "(%d) %s\n", iErrCode, zMsg);
+}
+
+struct PostCollector {
+ char *data;
+ size_t size;
+ size_t capacity;
+};
+
+struct Request {
+ struct Server *server;
+ struct PostCollector pp;
+};
+
+void prepare_database(struct App *app) {
+ // @CLEANUP: Move this to main? doesn't belong here at least
+ sqlite3_config(SQLITE_CONFIG_LOG, sqliteError, NULL);
+ sqlite3_config(SQLITE_CONFIG_URI, 1);
+
+ int err;
+
+ printf("Running migrations against %s\n", app->dbname);
+
+ sqlite3 *conn;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ fprintf(stderr, "(%d) %s\n", err, sqlite3_errmsg(conn));
+ abort();
+ }
+ sqlite3_db_config(conn, SQLITE_DBCONFIG_ENABLE_FKEY, 1, NULL);
+
+ if(sqlite3_exec(conn, "CREATE TABLE IF NOT EXISTS migration (id INTEGER PRIMARY KEY NOT NULL)", NULL, NULL, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT MAX(id) FROM migration", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+
+ uint64_t maxVersion = 0;
+ while((err = sqlite3_step(stmt)) == SQLITE_ROW) {
+ maxVersion = sqlite3_column_int64(stmt, 0);
+ }
+ if(err != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+
+ if(maxVersion < 1) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE TABLE backups ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "client TEXT NOT NULL, "
+ "status TEXT NOT NULL, "
+ "started TEXT NOT NULL, "
+ "completed TEXT "
+ ") STRICT";
+ 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, 1) != 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();
+ }
+ }
+
+ if(maxVersion < 2) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE TABLE clients ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "name TEXT NOT NULL "
+ ") STRICT;"
+ "INSERT INTO clients(name) "
+ "SELECT b.client "
+ "FROM backups b "
+ "GROUP BY b.client;"
+ "CREATE TABLE backups_next ("
+ "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ "client INTEGER NOT NULL, "
+ "status TEXT NOT NULL, "
+ "started TEXT NOT NULL, "
+ "completed TEXT ,"
+ "FOREIGN KEY(client) REFERENCES clients(id)"
+ ") STRICT;"
+ "INSERT INTO backups_next(id, client, status, started, completed) "
+ "SELECT b.id, c.id, b.status, b.started, b.completed "
+ "FROM backups b "
+ "JOIN clients c ON b.client = c.name;"
+ "DROP TABLE backups;"
+ "ALTER TABLE backups_next RENAME TO backups;";
+ 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, 2) != 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();
+ }
+ }
+
+ if(maxVersion < 3) {
+ char *prog =
+ "BEGIN TRANSACTION;"
+ "CREATE UNIQUE INDEX clients_name ON clients(name)";
+ 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, 3) != 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 {
+ REV_BEGIN,
+ REV_FINISH,
+};
+
+// @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) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ struct timespec now = get_current_time();
+
+ int64_t clientId;
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id FROM clients WHERE name = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, client, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_ROW) {
+ abort();
+ }
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+ clientId = sqlite3_column_int64(stmt, 0);
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ switch(event) {
+ case REV_BEGIN: {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO backups (client, status, started) VALUES (?1, ?2, datetime(?3, 'unixepoch', 'subsec'))", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 2, "RUNNING", -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_double(stmt, 3, TIME_AS_FLOAT(now)) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ break;
+ }
+ case REV_FINISH: {
+ uint64_t backupId;
+
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "SELECT id FROM backups WHERE client = ?1 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_ROW) {
+ abort();
+ }
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+ backupId = sqlite3_column_int64(stmt, 0);
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ stmt = NULL;
+
+ if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = ?1, completed = datetime(?2, 'unixepoch', 'subsec') WHERE id = ?3", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, "FINISHED", -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_bind_double(stmt, 2, TIME_AS_FLOAT(now)) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 3, backupId) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ break;
+ }
+ }
+
+ return 0;
+}
+
+struct CreateClientResult {
+ int64_t id;
+ char* name;
+};
+
+int create_client(struct Request *req, char *name, struct CreateClientResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ {
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "INSERT INTO clients (name) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC) != SQLITE_OK) {
+ abort();
+ }
+
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+
+ result->id = sqlite3_last_insert_rowid(conn);
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ return 0;
+}
+
+int list_clients(struct Request *req, char **response, size_t *response_len) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
+
+ *response = malloc(4096);
+ char *cursor = *response;
+ size_t written;
+
+ written = snprintf(cursor, *response + 4096 - cursor, "{ \"page\": [ ");
+ if(written >= *response + 4096 - cursor) abort();
+ cursor += written;
+
+ {
+ 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) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, -1) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 2, 16) != SQLITE_OK) {
+ abort();
+ }
+
+ int ret;
+ int index = 0;
+ while((ret = sqlite3_step(stmt)) == SQLITE_ROW) {
+ assert(index < 16);
+
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) {
+ abort();
+ }
+
+ int64_t clientId = sqlite3_column_int64(stmt, 0);
+ const uint8_t *name = sqlite3_column_text(stmt, 1);
+
+ written = snprintf(cursor, *response + 4096 - cursor, "{ \"id\": \"%ld\", \"name\": \"%s\" }, ", clientId, name);
+ if(written >= *response + 4096 - cursor) abort();
+ cursor += written;
+
+ index++;
+ }
+ if(ret != SQLITE_DONE) {
+ abort();
+ }
+
+ // Remove the last comma and space
+ if(index > 0) cursor -= 2;
+
+ written = snprintf(cursor, *response + 4096 - cursor, " ] }");
+ if(written >= *response + 4096 - cursor) abort();
+ cursor += written;
+
+ *response_len = cursor - *response;
+
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ }
+
+ return 0;
+}
+
+static ssize_t file_reader(void *cls, uint64_t pos, char *buf, size_t max) {
+ FILE *f = cls;
+
+ fseek(f, pos, SEEK_SET);
+ size_t read = fread(buf, 1, max, f);
+ if(read > 0) {
+ return read;
+ }
+
+ if(feof(f)) {
+ return MHD_CONTENT_READER_END_OF_STREAM;
+ }
+
+ abort();
+}
+
+static void file_close(void *cls) {
+ FILE *f = cls;
+ fclose(f);
+}
+
+static enum MHD_Result create_post_collector(struct PostCollector *c) {
+ c->data = malloc(512);
+ c->size = 0;
+ c->capacity = 512;
+
+ return MHD_YES;
+}
+
+static enum MHD_Result collect_post(struct PostCollector *c, const char *data, size_t *data_len) {
+ size_t newSize = c->size + *data_len;
+
+ if(newSize > c->capacity) {
+ while(newSize > c->capacity) c->capacity *= 2;
+ c->data = realloc(c->data, c->capacity);
+ }
+
+ memcpy(c->data + c->size, data, *data_len);
+ c->size = newSize;
+
+ return MHD_YES;
+}
+
+static enum MHD_Result destroy_post_collector(struct PostCollector *c) {
+ if(c->capacity != 0) {
+ free(c->data);
+ c->capacity = 0;
+ }
+
+ return MHD_YES;
+}
+
+static enum MHD_Result handler(
+ void *cls,
+ struct MHD_Connection *connection,
+ const char *url,
+ const char *method,
+ const char *version,
+ const char *upload_data,
+ size_t *upload_data_size,
+ void **ptr
+) {
+ struct Server *server = cls;
+ int ret;
+
+ // For the first call where con_cls points at NULL we have to create the
+ // connection state
+ struct Request *request = *ptr;
+ if (request == NULL) {
+ request = calloc(1, sizeof(struct Request));
+ if(request == NULL) {
+ return MHD_NO;
+ }
+ *ptr = request;
+
+ request->server = server;
+
+ if(strcmp(method, "POST") == 0) {
+ create_post_collector(&request->pp);
+ }
+
+ return MHD_YES;
+ }
+
+ if(strcmp(method, "POST") == 0) {
+ collect_post(&request->pp, upload_data, upload_data_size);
+ if (*upload_data_size != 0) {
+ *upload_data_size = 0;
+ return MHD_YES;
+ }
+
+ if(strcmp(url, "/api/report") == 0) {
+
+ cJSON *rootJson = cJSON_ParseWithLength(request->pp.data, request->pp.size);
+
+ cJSON *clientJson = cJSON_GetObjectItemCaseSensitive(rootJson, "client");
+ if(!cJSON_IsString(clientJson) || clientJson->valuestring == NULL) {
+ abort();
+ }
+
+ cJSON *eventJson = cJSON_GetObjectItemCaseSensitive(rootJson, "event");
+ if(!cJSON_IsString(eventJson) || eventJson->valuestring == NULL) {
+ abort();
+ }
+ enum ReportEvent event;
+ if(strcmp(eventJson->valuestring, "begin_backup") == 0) {
+ event = REV_BEGIN;
+ } else if(strcmp(eventJson->valuestring, "end_backup") == 0) {
+ event = REV_FINISH;
+ } else {
+ abort();
+ }
+
+
+ ret = submit_report(server->app, clientJson->valuestring, event);
+ if(ret != 0) {
+ // Should actually return an error page, but this will do for now
+ return MHD_NO;
+ }
+
+ cJSON_Delete(rootJson);
+ // We've taken in all the data, now we can use it
+ 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_NO_CONTENT,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ } else if(strcmp(url, "/api/client") == 0) {
+ cJSON *rootJson = cJSON_ParseWithLength(request->pp.data, request->pp.size);
+
+ cJSON *nameJson = cJSON_GetObjectItemCaseSensitive(rootJson, "name");
+ if(!cJSON_IsString(nameJson) || nameJson->valuestring == NULL) {
+ abort();
+ }
+
+ struct CreateClientResult result;
+ ret = create_client(request, nameJson->valuestring, &result);
+ if(ret != 0) {
+ // Should actually return an error page, but this will do for now
+ return MHD_NO;
+ }
+
+ cJSON_Delete(rootJson);
+ // We've taken in all the data, now we can use it
+ destroy_post_collector(&request->pp);
+
+ char *buf = malloc(1024);
+ size_t buf_len = snprintf(buf, 1024, "{ \"id\": \"%ld\" }", result.id);
+ if(buf_len >= 1024) {
+ abort();
+ }
+
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ buf_len,
+ buf,
+ MHD_RESPMEM_MUST_FREE
+ );
+ ret = MHD_add_response_header(response, "Content-Type", "application/json");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ } else {
+ // Not found
+ 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_NOT_FOUND,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ }
+ } else if (strcmp(method, "GET") == 0) {
+ if (*upload_data_size != 0) return MHD_NO;
+
+ if(strcmp(url, "/api/client") == 0) {
+ char *body;
+ size_t body_len;
+ ret = list_clients(request, &body, &body_len);
+ if(ret != 0) {
+ // Should actually return an error page, but this will do for now
+ return MHD_NO;
+ }
+
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ body_len,
+ body,
+ MHD_RESPMEM_MUST_FREE
+ );
+ ret = MHD_add_response_header(response, "Content-Type", "application/json");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ } else {
+ // @SECURITY: We should restrict this lookup. Look at openat2(2)
+ char path[512];
+ if(snprintf(path, sizeof(path), "static/%s", url) >= sizeof(path)) abort();
+ struct stat statbuf;
+ FILE *f = NULL;
+ if(stat(path, &statbuf) == 0 && (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode))) {
+ f = fopen(path, "rb");
+ }
+
+ if(f != NULL) {
+ struct MHD_Response *response = MHD_create_response_from_callback(
+ statbuf.st_size, statbuf.st_blksize,
+ &file_reader, f,
+ &file_close
+ );
+
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ } else {
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ strlen(server->page),
+ (void *)server->page,
+ MHD_RESPMEM_PERSISTENT
+ );
+
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
+ }
+ }
+ }
+
+ return MHD_NO;
+}
+
+static void request_completed_callback (
+ void *cls,
+ struct MHD_Connection *connection,
+ void **con_cls,
+ enum MHD_RequestTerminationCode toe
+) {
+ struct Request *request = *con_cls;
+
+ destroy_post_collector(&request->pp);
+ free(request);
+}
+
+static struct MHD_Daemon *d = NULL;
+
+int web_begin(struct Server *server, int port) {
+ assert(d == NULL);
+
+ server->page = PAGE;
+
+ d = MHD_start_daemon(
+ MHD_USE_THREAD_PER_CONNECTION,
+ port,
+ NULL, NULL,
+ &handler, server,
+ MHD_OPTION_NOTIFY_COMPLETED, &request_completed_callback, NULL,
+ MHD_OPTION_END
+ );
+
+ if (d == NULL) return 1;
+
+ return 0;
+}
+
+int web_join(struct Server *server) {
+ assert(d != NULL);
+
+ MHD_stop_daemon(d);
+ d = NULL;
+
+ return 0;
+}
diff --git a/src/web.h b/src/web.h
new file mode 100644
index 0000000..4b7e89f
--- /dev/null
+++ b/src/web.h
@@ -0,0 +1,17 @@
+#pragma once
+
+struct App {
+ char* dbname;
+};
+
+void prepare_database(struct App *app);
+
+struct Server {
+ struct App *app;
+
+ char *page;
+};
+
+int web_begin(struct Server *server, int port);
+int web_join(struct Server *server);
+
diff --git a/test/http.c b/test/http.c
new file mode 100644
index 0000000..fa7755d
--- /dev/null
+++ b/test/http.c
@@ -0,0 +1,227 @@
+#include "web.h"
+
+#include "mytime.h"
+
+#include <assert.h>
+#include <curl/curl.h>
+#include <stdlib.h>
+#include <string.h>
+
+// @PASTE Stolen from libcurl documentation
+struct memory {
+ char *body;
+ size_t size;
+};
+
+static size_t write_to_memory(char *data, size_t size, size_t nmemb, void *clientp) {
+ size_t realsize = size * nmemb;
+ struct memory *mem = (struct memory *)clientp;
+
+ char *ptr = realloc(mem->body, mem->size + realsize + 1);
+ if(!ptr) return 0; /* out of memory */
+
+ mem->body = ptr;
+ memcpy(&(mem->body[mem->size]), data, realsize);
+ mem->size += realsize;
+ mem->body[mem->size] = 0;
+
+ return realsize;
+}
+
+static size_t read_from_memory(char *data, size_t size, size_t nmemb, void *clientp) {
+ size_t realsize = size * nmemb;
+ struct memory *mem = (struct memory *)clientp;
+
+ realsize = realsize > mem->size ? mem->size : realsize;
+
+ memcpy(data, mem->body, realsize);
+ mem->size -= realsize;
+ mem->body += realsize;
+
+ return realsize;
+}
+
+int main(int argc, char **argv) {
+ enable_fake_time();
+
+ // @DEP: The shared-cache mode is deprecated in sqlite, but it's still
+ // there and useful right now.
+ struct App app = {
+ .dbname = "file::memory:?cache=shared",
+ };
+ prepare_database(&app);
+ struct Server server = {
+ .app = &app,
+ };
+ web_begin(&server, 8080);
+ curl_global_init(CURL_GLOBAL_ALL);
+ CURLcode curlRes;
+ CURL *curl = curl_easy_init();
+ assert(curl != NULL);
+
+ {
+ curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/");
+ 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 == 200);
+
+ char *ct;
+ curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
+ assert(curlRes == CURLE_OK);
+
+ }
+
+ {
+ curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/client");
+ 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 = "{\"name\": \"client1\"}",
+ .size = strlen(req_body.body),
+ };
+ 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 == 200);
+
+ char *ct;
+ curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
+ assert(curlRes == CURLE_OK);
+ assert(ct != NULL);
+ assert(strcmp(ct, "application/json") == 0);
+
+ assert(body.size >= 0);
+ assert(strcmp(
+ body.body,
+ "{ "
+ "\"id\": \"1\" "
+ "}"
+ ) == 0);
+ body = (struct memory){0};
+ }
+
+ {
+ 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\", \"event\": \"begin_backup\"}",
+ .size = strlen(req_body.body),
+ };
+ 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 == 204);
+
+ char *ct;
+ curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
+ assert(curlRes == CURLE_OK);
+ assert(ct != NULL);
+ assert(strcmp(ct, "application/json") == 0);
+
+ assert(body.size == 0);
+ body = (struct memory){0};
+ }
+
+ progress_time(TIME_JOIN(0, 30, 0));
+
+ {
+ 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\", \"event\": \"end_backup\"}",
+ .size = strlen(req_body.body),
+ };
+ 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 == 204);
+
+ char *ct;
+ curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
+ assert(curlRes == CURLE_OK);
+ assert(ct != NULL);
+ assert(strcmp(ct, "application/json") == 0);
+
+ assert(body.size == 0);
+ body = (struct memory){0};
+ }
+
+ web_join(&server);
+}