diff options
Diffstat (limited to 'src')
| -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 |
5 files changed, 814 insertions, 0 deletions
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); + |
