summaryrefslogtreecommitdiff
path: root/src/web.c
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-02-17 23:34:53 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-02-17 23:34:53 +0100
commit78745e06f4a4365e9caa8ee12fb0d9e15fb59300 (patch)
treefb24257a0942704408e0ed1c2513a0f1cdd8c20e /src/web.c
parentc9e3a4b2816ad23aa201a4f43f357a376947bd49 (diff)
Simple client CRUD
Diffstat (limited to 'src/web.c')
-rw-r--r--src/web.c300
1 files changed, 201 insertions, 99 deletions
diff --git a/src/web.c b/src/web.c
index 810d5d0..8c37bfe 100644
--- a/src/web.c
+++ b/src/web.c
@@ -22,16 +22,6 @@ static void sqliteError(void *pArg, int iErrCode, const char *zMsg) {
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
@@ -310,14 +300,12 @@ int submit_report(struct App *app, char *client, enum ReportEvent event) {
}
}
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
return 0;
}
-struct CreateClientResult {
- int64_t id;
- char* name;
-};
-
int create_client(struct Request *req, char *name, struct CreateClientResult *result) {
sqlite3 *conn;
int err;
@@ -345,23 +333,43 @@ int create_client(struct Request *req, char *name, struct CreateClientResult *re
}
}
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
return 0;
}
-int list_clients(struct Request *req, char **response, size_t *response_len) {
+int delete_client(struct Request *req, int64_t client_id) {
sqlite3 *conn;
- int err;
- if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) {
+ if(sqlite3_open(req->server->app->dbname, &conn) != SQLITE_OK) {
abort();
}
- *response = malloc(4096);
- char *cursor = *response;
- size_t written;
+ sqlite3_stmt *stmt;
+ if(sqlite3_prepare_v2(conn, "DELETE FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_step(stmt) != SQLITE_DONE) {
+ abort();
+ }
+ if(sqlite3_finalize(stmt) != SQLITE_OK) {
+ abort();
+ }
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
+ return 0;
+}
- written = snprintf(cursor, *response + 4096 - cursor, "{ \"page\": [ ");
- if(written >= *response + 4096 - cursor) abort();
- cursor += written;
+int list_clients(struct Request *req, struct ListClientResult *result) {
+ sqlite3 *conn;
+ int err;
+ if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) {
+ abort();
+ }
{
sqlite3_stmt *stmt;
@@ -387,9 +395,8 @@ int list_clients(struct Request *req, char **response, size_t *response_len) {
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;
+ result->clients[index].id = clientId;
+ strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX);
index++;
}
@@ -397,42 +404,77 @@ int list_clients(struct Request *req, char **response, size_t *response_len) {
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;
+ result->clients_len = index;
if(sqlite3_finalize(stmt) != SQLITE_OK) {
abort();
}
}
+ if(sqlite3_close(conn) != SQLITE_OK) {
+ abort();
+ }
return 0;
}
-static ssize_t file_reader(void *cls, uint64_t pos, char *buf, size_t max) {
- FILE *f = cls;
+static char *render_clients_page(struct Request *request, size_t *out_len) {
+ int ret;
+ struct ListClientResult *result = malloc(sizeof(struct ListClientResult) + sizeof(struct ListClientResultClient) * 16);
+ ret = list_clients(request, result);
+ if(ret != 0) {
+ free(result);
+ return NULL;
+ }
- fseek(f, pos, SEEK_SET);
- size_t read = fread(buf, 1, max, f);
- if(read > 0) {
- return read;
+ char *buf = NULL;
+ size_t buf_len = 0;
+ FILE *f = open_memstream(&buf, &buf_len);
+ if (!f) {
+ free(result);
+ return NULL;
}
- if(feof(f)) {
- return MHD_CONTENT_READER_END_OF_STREAM;
+ fprintf(f,
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>\n"
+ " <title>Clients</title>\n"
+ "</head>\n"
+ "<body>\n"
+ " <h1>Clients</h1>\n"
+ " <table border=\"1\">\n"
+ " <tr><th>Name</th><th>Actions</th></tr>\n");
+
+ for (int i = 0; i < result->clients_len; i++) {
+ fprintf(f,
+ " <tr>\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"
+ " <button type=\"submit\">Delete</button>\n"
+ " </form>\n"
+ " </td>\n"
+ " </tr>\n",
+ result->clients[i].name,
+ result->clients[i].id
+ );
}
- abort();
-}
+ fprintf(f,
+ " </table>\n"
+ " <h2>Add New Client</h2>\n"
+ " <form method=\"POST\" action=\"/clients\">\n"
+ " <input type=\"text\" name=\"name\" maxlength=\"31\" placeholder=\"Client name\" required>\n"
+ " <button type=\"submit\">Add Client</button>\n"
+ " </form>\n"
+ "</body>\n"
+ "</html>\n");
-static void file_close(void *cls) {
- FILE *f = cls;
fclose(f);
+ *out_len = buf_len;
+ free(result);
+ return buf;
}
static enum MHD_Result create_post_collector(struct PostCollector *c) {
@@ -466,6 +508,34 @@ static enum MHD_Result destroy_post_collector(struct PostCollector *c) {
return MHD_YES;
}
+static enum MHD_Result
+form_iterator(void *cls,
+ enum MHD_ValueKind kind,
+ const char *key,
+ const char *filename,
+ const char *content_type,
+ const char *transfer_encoding,
+ const char *data,
+ uint64_t off,
+ size_t size)
+{
+ struct Request *request = cls;
+
+ if (strcmp(key, "name") == 0 && off + size < CLIENT_NAME_MAX) {
+ memcpy(request->form.name + off, data, size);
+ request->form.name[off + size] = '\0';
+ request->form.name_set = 1;
+ } else if (strcmp(key, "client_id") == 0 && off == 0) {
+ char buf[32] = {0};
+ size_t copy_size = size < 31 ? size : 31;
+ memcpy(buf, data, copy_size);
+ request->form.client_id = strtoll(buf, NULL, 10);
+ request->form.client_id_set = 1;
+ }
+
+ return MHD_YES;
+}
+
static enum MHD_Result handler(
void *cls,
struct MHD_Connection *connection,
@@ -492,20 +562,75 @@ static enum MHD_Result handler(
request->server = server;
if(strcmp(method, "POST") == 0) {
- create_post_collector(&request->pp);
+ if(strcmp(url, "/clients") == 0 || strcmp(url, "/clients/delete") == 0) {
+ request->post_proc = MHD_create_post_processor(
+ connection, 1024, &form_iterator, request);
+ if (request->post_proc == NULL) {
+ free(request);
+ return MHD_NO;
+ }
+ } else {
+ 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 (request->post_proc != NULL) {
+ if (*upload_data_size != 0) {
+ MHD_post_process(request->post_proc, upload_data, *upload_data_size);
+ *upload_data_size = 0;
+ return MHD_YES;
+ }
+ } else {
+ 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) {
+ if(strcmp(url, "/clients") == 0) {
+ MHD_destroy_post_processor(request->post_proc);
+ request->post_proc = NULL;
+
+ if (!request->form.name_set || request->form.name[0] == '\0') {
+ return MHD_NO;
+ }
+
+ struct CreateClientResult result;
+ ret = create_client(request, request->form.name, &result);
+ if(ret != 0) return MHD_NO;
+
+ // Post-Redirect-Get: redirect back to GET /clients
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ 0, "", MHD_RESPMEM_PERSISTENT);
+ ret = MHD_add_response_header(response, "Location", "/clients");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(connection, MHD_HTTP_SEE_OTHER, response);
+ MHD_destroy_response(response);
+ return ret;
+ } else if(strcmp(url, "/clients/delete") == 0) {
+ MHD_destroy_post_processor(request->post_proc);
+ request->post_proc = NULL;
+
+ if (!request->form.client_id_set || request->form.client_id <= 0) {
+ return MHD_NO;
+ }
+
+ ret = delete_client(request, request->form.client_id);
+ if(ret != 0) return MHD_NO;
+
+ struct MHD_Response *response = MHD_create_response_from_buffer(
+ 0, "", MHD_RESPMEM_PERSISTENT);
+ ret = MHD_add_response_header(response, "Location", "/clients");
+ if(ret != MHD_YES) return ret;
+ ret = MHD_queue_response(connection, MHD_HTTP_SEE_OTHER, response);
+ MHD_destroy_response(response);
+ return ret;
+ } else if(strcmp(url, "/api/report") == 0) {
cJSON *rootJson = cJSON_ParseWithLength(request->pp.data, request->pp.size);
@@ -613,21 +738,20 @@ static enum MHD_Result handler(
} 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
+ if(strcmp(url, "/clients") == 0) {
+ size_t html_len;
+ char *html = render_clients_page(request, &html_len);
+
+ if (!html) {
return MHD_NO;
}
struct MHD_Response *response = MHD_create_response_from_buffer(
- body_len,
- body,
+ html_len,
+ html,
MHD_RESPMEM_MUST_FREE
);
- ret = MHD_add_response_header(response, "Content-Type", "application/json");
+ ret = MHD_add_response_header(response, "Content-Type", "text/html");
if(ret != MHD_YES) return ret;
ret = MHD_queue_response(
connection,
@@ -637,44 +761,19 @@ static enum MHD_Result handler(
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
- );
+ 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;
- }
+ ret = MHD_queue_response(
+ connection,
+ MHD_HTTP_OK,
+ response
+ );
+ MHD_destroy_response(response);
+ return ret;
}
}
@@ -689,6 +788,9 @@ static void request_completed_callback (
) {
struct Request *request = *con_cls;
+ if (request->post_proc != NULL) {
+ MHD_destroy_post_processor(request->post_proc);
+ }
destroy_post_collector(&request->pp);
free(request);
}