diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2026-02-16 21:43:36 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2026-02-16 21:43:36 +0100 |
| commit | c9e3a4b2816ad23aa201a4f43f357a376947bd49 (patch) | |
| tree | 042530b79b1e3c7c36872b991850002bec9268a8 | |
INITIAL COMMIT
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | Makefile | 79 | ||||
| -rw-r--r-- | src/main.c | 23 | ||||
| -rw-r--r-- | src/mytime.c | 38 | ||||
| -rw-r--r-- | src/mytime.h | 12 | ||||
| -rw-r--r-- | src/web.c | 724 | ||||
| -rw-r--r-- | src/web.h | 17 | ||||
| -rw-r--r-- | test/http.c | 227 |
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); +} |
