From 1b19afaa339f2f68520d61b634600ddeb04effb7 Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Sat, 21 Feb 2026 11:14:42 +0100 Subject: Split up the rendering from the logic --- src/app.c | 639 +++++++++++++++++++++++++++++ src/app.h | 70 ++++ src/html.c | 599 +++++++++++++++++++++++++++ src/html.h | 17 + src/main.c | 3 +- src/web.c | 1312 +++--------------------------------------------------------- src/web.h | 65 +-- 7 files changed, 1379 insertions(+), 1326 deletions(-) create mode 100644 src/app.c create mode 100644 src/app.h create mode 100644 src/html.c create mode 100644 src/html.h (limited to 'src') diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..9d1ee7a --- /dev/null +++ b/src/app.c @@ -0,0 +1,639 @@ +#include "app.h" +#include "mytime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} + +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; +} + +// https://stackoverflow.com/questions/2336242/recursive-mkdir-system-call-on-unix +static void _mkdir(const char *dir) { + char tmp[PATH_MAX]; + char *p = NULL; + size_t len; + int rc; + + snprintf(tmp, sizeof(tmp),"%s",dir); + len = strlen(tmp); + if (tmp[len - 1] == '/') + tmp[len - 1] = 0; + for (p = tmp + 1; *p; p++) + if (*p == '/') { + *p = 0; + rc = mkdir(tmp, S_IRWXU); + if(rc != 0 && errno != EEXIST) { + abort(); + } + *p = '/'; + } + rc = mkdir(tmp, S_IRWXU); + if(rc != 0 && errno != EEXIST) { + abort(); + } + +} + +void app_startup(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); + + if(memcmp("file::memory", app->dbname, 12) != 0) { + char *full = strdup(app->dbname); + char *dbdir = dirname(full); + _mkdir(dbdir); + free(full); + } + + 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(); + } + } + + 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(); + } + } +} + +int submit_report(struct App *app, char *client, char *secret, int is_finish) { + 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, secret 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); + + 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(); + } + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + } + + if(!is_finish) { + // REV_BEGIN + sqlite3_stmt *stmt; + + // Mark any existing RUNNING backup as INTERRUPTED + if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = 'INTERRUPTED', completed = datetime(?1, 'unixepoch', 'subsec') WHERE client = ?2 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) { + abort(); + } + if(sqlite3_bind_double(stmt, 1, TIME_AS_FLOAT(now)) != SQLITE_OK) { + abort(); + } + if(sqlite3_bind_int64(stmt, 2, clientId) != SQLITE_OK) { + abort(); + } + + if(sqlite3_step(stmt) != SQLITE_DONE) { + abort(); + } + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + stmt = NULL; + + // Insert the new backup + 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(); + } + } else { + // 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(); + } + } + + if(sqlite3_close(conn) != SQLITE_OK) { + abort(); + } + return 0; +} + +int create_client(struct App *app, char *name, struct CreateClientResult *result) { + sqlite3 *conn; + int err; + if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { + abort(); + } + + if(generate_secret(result->secret) != 0) { + abort(); + } + + { + sqlite3_stmt *stmt; + 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(); + } + + result->id = sqlite3_last_insert_rowid(conn); + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + } + + if(sqlite3_close(conn) != SQLITE_OK) { + abort(); + } + return 0; +} + +int delete_client(struct App *app, int64_t client_id) { + sqlite3 *conn; + if(sqlite3_open(app->dbname, &conn) != SQLITE_OK) { + abort(); + } + + 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; +} + +int list_clients(struct App *app, struct ListClientResult *result) { + sqlite3 *conn; + int err; + if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { + abort(); + } + + { + sqlite3_stmt *stmt; + const char *sql = + "SELECT c.id, c.name, c.secret, unixepoch(b_success.completed,'subsec'), b_last.status " + "FROM clients c " + "LEFT JOIN backups b_success ON b_success.client = c.id " + " AND b_success.id = (SELECT id FROM backups WHERE client = c.id AND status = 'FINISHED' ORDER BY id DESC LIMIT 1) " + "LEFT JOIN backups b_last ON b_last.client = c.id " + " AND b_last.id = (SELECT id FROM backups WHERE client = c.id ORDER BY id DESC LIMIT 1) " + "WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2"; + if(sqlite3_prepare_v2(conn, sql, -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); + const uint8_t *secret = sqlite3_column_text(stmt, 2); + const double last_backup = sqlite3_column_double(stmt, 3); + const uint8_t *backup_status = sqlite3_column_text(stmt, 4); + + result->clients[index].id = clientId; + strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX); + strncpy(result->clients[index].secret, secret ? (char*)secret : "", CLIENT_SECRET_LEN); + result->clients[index].last_backup = time_from_double(last_backup); + + if (backup_status == NULL) { + strncpy(result->clients[index].status, "MISSING", BACKUP_STATUS_MAX); + result->clients[index].is_stale = 0; + } else if (strcmp((char*)backup_status, "RUNNING") == 0) { + strncpy(result->clients[index].status, "RUNNING", BACKUP_STATUS_MAX); + result->clients[index].is_stale = 0; + } else if (strcmp((char*)backup_status, "INTERRUPTED") == 0) { + strncpy(result->clients[index].status, "INTERRUPTED", BACKUP_STATUS_MAX); + result->clients[index].is_stale = 0; + } else { + strncpy(result->clients[index].status, "OK", BACKUP_STATUS_MAX); + result->clients[index].is_stale = 0; + } + + index++; + } + if(ret != SQLITE_DONE) { + abort(); + } + + result->clients_len = index; + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + } + + if(sqlite3_close(conn) != SQLITE_OK) { + abort(); + } + return 0; +} + +int get_client(struct App *app, int64_t client_id, struct GetClientOnlyResult *result) { + sqlite3 *conn; + int err; + if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { + abort(); + } + + result->found = 0; + + sqlite3_stmt *stmt; + if(sqlite3_prepare_v2(conn, "SELECT id, name, secret FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) { + abort(); + } + if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { + abort(); + } + + int ret = sqlite3_step(stmt); + if(ret == SQLITE_ROW) { + result->found = 1; + result->id = sqlite3_column_int64(stmt, 0); + const uint8_t *name = sqlite3_column_text(stmt, 1); + const uint8_t *secret = sqlite3_column_text(stmt, 2); + strncpy(result->name, (char*)name, CLIENT_NAME_MAX); + strncpy(result->secret, (char*)secret, CLIENT_SECRET_LEN); + } else if(ret != SQLITE_DONE) { + abort(); + } + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + + if(sqlite3_close(conn) != SQLITE_OK) { + abort(); + } + return 0; +} + +int get_client_backups(struct App *app, int64_t client_id, struct GetBackupsResult *result) { + sqlite3 *conn; + int err; + if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { + abort(); + } + + uint16_t limit = result->backups_len; + + sqlite3_stmt *stmt; + if(sqlite3_prepare_v2(conn, "SELECT id, status, unixepoch(started, 'subsec'), unixepoch(completed, 'subsec') FROM backups WHERE client = ?1 ORDER BY started DESC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) { + abort(); + } + if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { + abort(); + } + if(sqlite3_bind_int(stmt, 2, limit) != SQLITE_OK) { + abort(); + } + + int ret; + int index = 0; + while((ret = sqlite3_step(stmt)) == SQLITE_ROW) { + if(index >= limit) break; + + result->backups[index].id = sqlite3_column_int64(stmt, 0); + + const uint8_t *status = sqlite3_column_text(stmt, 1); + const double started = sqlite3_column_double(stmt, 2); + const double completed = sqlite3_column_double(stmt, 3); + + strncpy(result->backups[index].status, status ? (char*)status : "", BACKUP_STATUS_MAX); + result->backups[index].started = time_from_double(started); + result->backups[index].completed = time_from_double(completed); + + index++; + } + if(ret != SQLITE_DONE && ret != SQLITE_ROW) { + abort(); + } + + result->backups_len = index; + + if(sqlite3_finalize(stmt) != SQLITE_OK) { + abort(); + } + + if(sqlite3_close(conn) != SQLITE_OK) { + abort(); + } + return 0; +} diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..46d2c18 --- /dev/null +++ b/src/app.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +#define CLIENT_NAME_MAX 32 +#define CLIENT_SECRET_LEN 33 +#define BACKUP_STATUS_MAX 16 +#define MAX_BACKUPS_DISPLAY 50 + +struct App { + const char* dbname; + char session_secret[CLIENT_SECRET_LEN]; + const char *admin_user; + const char *admin_pass; +}; + +void app_startup(struct App *app); + +struct CreateClientResult { + int64_t id; + char* name; + char secret[CLIENT_SECRET_LEN]; +}; + +int create_client(struct App *app, char *name, struct CreateClientResult *result); +int delete_client(struct App *app, int64_t client_id); + +struct ListClientResultClient { + int64_t id; + char name[CLIENT_NAME_MAX]; + char secret[CLIENT_SECRET_LEN]; + struct timespec last_backup; + char status[BACKUP_STATUS_MAX]; // "OK", "RUNNING", "STALE", "FAILED", "INTERRUPTED" + int is_stale; +}; + +struct ListClientResult { + uint16_t clients_len; + // Right now this has to be exactly 16 elements long, but it could be longer in the future + struct ListClientResultClient clients[]; +}; + +int list_clients(struct App *app, struct ListClientResult *result); + +struct BackupRecord { + int64_t id; + char status[BACKUP_STATUS_MAX]; + struct timespec started; + struct timespec completed; +}; + +struct GetClientOnlyResult { + int found; + int64_t id; + char name[CLIENT_NAME_MAX]; + char secret[CLIENT_SECRET_LEN]; +}; + +struct GetBackupsResult { + uint16_t backups_len; // On input: max backups to fetch; On output: actual count + struct BackupRecord backups[]; +}; + +int get_client(struct App *app, int64_t client_id, struct GetClientOnlyResult *result); +int get_client_backups(struct App *app, int64_t client_id, struct GetBackupsResult *result); + +// Returns 0 on success, -1 on error, -2 on secret mismatch +int submit_report(struct App *app, char *client, char *secret, int is_finish); diff --git a/src/html.c b/src/html.c new file mode 100644 index 0000000..10a6848 --- /dev/null +++ b/src/html.c @@ -0,0 +1,599 @@ +#include "html.h" +#include "mytime.h" + +#include +#include + +static void emit_stylesheet(FILE *f) { + fprintf(f, + "\n" + ); +} + +static void mask_secret(const char *secret, char *out, size_t out_len) { + size_t len = strlen(secret); + if (len >= 8 && out_len >= 13) { + snprintf(out, out_len, "%.4s....%.4s", secret, secret + len - 4); + } else if (len > 0) { + snprintf(out, out_len, "****"); + } else { + out[0] = '\0'; + } +} + +static const char *status_to_badge_class(const char *status) { + if (strcmp(status, "OK") == 0) return "ok"; + if (strcmp(status, "RUNNING") == 0) return "run"; + if (strcmp(status, "STALE") == 0) return "stale"; + if (strcmp(status, "INTERRUPTED") == 0) return "fail"; + if (strcmp(status, "FINISHED") == 0) return "ok"; + return "ok"; +} + +static const char *status_to_display(const char *status) { + if (strcmp(status, "OK") == 0) return "OK"; + if (strcmp(status, "RUNNING") == 0) return "Running"; + if (strcmp(status, "STALE") == 0) return "Stale"; + if (strcmp(status, "INTERRUPTED") == 0) return "Failed"; + if (strcmp(status, "FINISHED") == 0) return "Done"; + return status; +} + +char *html_render_clients_page(const char *host, const struct ListClientResult *result, size_t *out_len) { + // Count stale clients + int stale_count = 0; + for (int i = 0; i < result->clients_len; i++) { + if (result->clients[i].is_stale) stale_count++; + } + + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) { + return NULL; + } + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - Clients\n"); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "
\n" + " BorgFlag\n" + "
\n" + "
\n" + "
\n" + "

\n" + " Clients\n" + " %d registered", result->clients_len); + + if (stale_count > 0) { + fprintf(f, "%d stale", stale_count); + } + + fprintf(f, + "\n" + " \n" + "

\n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + "
\n" + " Name\n" + " Secret\n" + " Last Backup\n" + " Status\n" + " \n" + "
\n" + ); + + for (int i = 0; i < result->clients_len; i++) { + char masked_secret[16]; + mask_secret(result->clients[i].secret, masked_secret, sizeof(masked_secret)); + + fprintf(f, + " \n" + " %s\n" + " %s\n" + " %s\n" + " %s\n" + " \n" + " \n", + result->clients[i].id, + result->clients[i].is_stale ? "stale" : "", + result->clients[i].name, + masked_secret, + result->clients[i].last_backup.tv_sec > 0.0 ? format_time(result->clients[i].last_backup).buf : "-", + status_to_badge_class(result->clients[i].status), + status_to_display(result->clients[i].status), + result->clients[i].id + ); + } + + fprintf(f, + "
\n" + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} + +char *html_render_client_detail_page(const char *host, const struct GetClientOnlyResult *client_result, const struct GetBackupsResult *backups_result, size_t *out_len) { + // Determine status and staleness + int is_stale = 0; + const char *current_status = "OK"; + if (backups_result->backups_len > 0) { + const char *last_status = backups_result->backups[0].status; + if (strcmp(last_status, "RUNNING") == 0) { + current_status = "RUNNING"; + } else if (strcmp(last_status, "INTERRUPTED") == 0) { + current_status = "INTERRUPTED"; + } + } + + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) { + return NULL; + } + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - %s\n", client_result->name); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "\n" + "
\n" + " BorgFlag\n" + "
\n" + "
\n" + "
\n" + " ← Back to clients\n" + " \n" + "

%s

\n" + "

%d backups %s

\n", + is_stale ? " class=\"stale\"" : "", + client_result->name, + backups_result->backups_len, + status_to_badge_class(current_status), + status_to_display(current_status) + ); + + if (is_stale) { + fprintf(f, + "

Warning: Last backup was more than 24 hours ago. Expected every 24 hours.

\n" + ); + } + + fprintf(f, + " Secret: %s\n" + " \n" + "
\n" + " API Snippets\n" + "
\n" + "
\n" + "

Start backup

\n" + "
curl -X POST http://%s/api/report \\\n"
+		"     -H \"Content-Type: application/json\" \\\n"
+		"     -d '{\n"
+		"           \"client\": \"%s\",\n"
+		"           \"secret\": \"%s\",\n"
+		"           \"event\":  \"begin_backup\"\n"
+		"         }'
\n" + "
\n" + "
\n" + "

End backup

\n" + "
curl -X POST http://%s/api/report \\\n"
+		"     -H \"Content-Type: application/json\" \\\n"
+		"     -d '{\n"
+		"           \"client\": \"%s\",\n"
+		"           \"secret\": \"%s\",\n"
+		"           \"event\":  \"end_backup\"\n"
+		"         }'
\n" + "
\n" + "
\n" + "
\n", + client_result->secret, + host ? host : "localhost", + client_result->name, + client_result->secret, + host ? host : "localhost", + client_result->name, + client_result->secret + ); + + fprintf(f, "

Recent Backups

\n"); + + if(backups_result->backups_len == 0) { + fprintf(f, "

No backups recorded.

\n"); + } else { + fprintf(f, "
\n"); + + for (int i = 0; i < backups_result->backups_len; i++) { + const char *badge_class; + const char *badge_text; + if (strcmp(backups_result->backups[i].status, "RUNNING") == 0) { + badge_class = "run"; + badge_text = "Running"; + } else if (strcmp(backups_result->backups[i].status, "INTERRUPTED") == 0) { + badge_class = "fail"; + badge_text = "Failed"; + } else { + badge_class = "ok"; + badge_text = "Done"; + } + + struct timespec duration = time_sub(&backups_result->backups[i].completed, &backups_result->backups[i].started); + fprintf(f, + "
%s%s
\n", + format_time(backups_result->backups[i].started).buf, + format_duration(duration).buf, + badge_class, + badge_text + ); + } + + fprintf(f, "
\n"); + } + + fprintf(f, + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} + +char *html_render_login_page(const char *error_msg, size_t *out_len) { + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) return NULL; + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - Login\n"); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "
\n" + "
\n" + "

BorgFlag

\n"); + + if (error_msg) { + fprintf(f, "
%s
\n", error_msg); + } + + fprintf(f, + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} diff --git a/src/html.h b/src/html.h new file mode 100644 index 0000000..f244cdb --- /dev/null +++ b/src/html.h @@ -0,0 +1,17 @@ +#pragma once + +#include "app.h" +#include + +char *html_render_login_page(const char *error_msg, size_t *out_len); + +char *html_render_clients_page( + const char *host, + const struct ListClientResult *clients, + size_t *out_len); + +char *html_render_client_detail_page( + const char *host, + const struct GetClientOnlyResult *client, + const struct GetBackupsResult *backups, + size_t *out_len); diff --git a/src/main.c b/src/main.c index e5d872d..9a87d7c 100644 --- a/src/main.c +++ b/src/main.c @@ -1,3 +1,4 @@ +#include "app.h" #include "web.h" #include @@ -40,7 +41,7 @@ int main(int argc, char ** argv) { signal(SIGTERM, signal_shutdown); signal(SIGINT, signal_shutdown); - prepare_database(&app); + app_startup(&app); web_begin(&server, atoi(argv[1]), admin_user, admin_pass); while (!shutdown_requested) { diff --git a/src/web.c b/src/web.c index 6dd18e1..9c0789b 100644 --- a/src/web.c +++ b/src/web.c @@ -1,21 +1,15 @@ #include "web.h" +#include "app.h" +#include "html.h" #include "mytime.h" #include -#include -#include -#include -#include -#include #include -#include #include #include #include #include -#include #include -#include #include #include #include @@ -23,11 +17,6 @@ #define PAGE "libmicrohttpd demo"\ "libmicrohttpd demo" -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); -} - static int generate_secret(char *out) { unsigned char bytes[16]; int fd = open("/dev/urandom", O_RDONLY); @@ -64,1243 +53,6 @@ static int is_authenticated(struct App *app, struct MHD_Connection *connection) return strcmp(cookie, expected) == 0; } -// https://stackoverflow.com/questions/2336242/recursive-mkdir-system-call-on-unix -static void _mkdir(const char *dir) { - char tmp[PATH_MAX]; - char *p = NULL; - size_t len; - int rc; - - snprintf(tmp, sizeof(tmp),"%s",dir); - len = strlen(tmp); - if (tmp[len - 1] == '/') - tmp[len - 1] = 0; - for (p = tmp + 1; *p; p++) - if (*p == '/') { - *p = 0; - rc = mkdir(tmp, S_IRWXU); - if(rc != 0 && errno != EEXIST) { - abort(); - } - *p = '/'; - } - rc = mkdir(tmp, S_IRWXU); - if(rc != 0 && errno != EEXIST) { - abort(); - } - -} - -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); - - if(memcmp("file::memory", app->dbname, 12) != 0) { - char *full = strdup(app->dbname); - char *dbdir = dirname(full); - _mkdir(dbdir); - free(full); - } - - 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(); - } - } - - 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 { - REV_BEGIN, - REV_FINISH, -}; - -// @CLEANUP: This should live somewhere else, but I don't have a spot for it yet. -// 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) { - abort(); - } - - struct timespec now = get_current_time(); - - int64_t clientId; - { - sqlite3_stmt *stmt; - 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) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_ROW) { - abort(); - } - - if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) { - abort(); - } - 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(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - switch(event) { - case REV_BEGIN: { - sqlite3_stmt *stmt; - - // Mark any existing RUNNING backup as INTERRUPTED - if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = 'INTERRUPTED', completed = datetime(?1, 'unixepoch', 'subsec') WHERE client = ?2 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_double(stmt, 1, TIME_AS_FLOAT(now)) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 2, clientId) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - stmt = NULL; - - // Insert the new backup - 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; - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -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(); - } - - if(generate_secret(result->secret) != 0) { - abort(); - } - - { - sqlite3_stmt *stmt; - 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(); - } - - result->id = sqlite3_last_insert_rowid(conn); - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int delete_client(struct Request *req, int64_t client_id) { - sqlite3 *conn; - if(sqlite3_open(req->server->app->dbname, &conn) != SQLITE_OK) { - abort(); - } - - 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; -} - -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; - const char *sql = - "SELECT c.id, c.name, c.secret, unixepoch(b_success.completed,'subsec'), b_last.status " - "FROM clients c " - "LEFT JOIN backups b_success ON b_success.client = c.id " - " AND b_success.id = (SELECT id FROM backups WHERE client = c.id AND status = 'FINISHED' ORDER BY id DESC LIMIT 1) " - "LEFT JOIN backups b_last ON b_last.client = c.id " - " AND b_last.id = (SELECT id FROM backups WHERE client = c.id ORDER BY id DESC LIMIT 1) " - "WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2"; - if(sqlite3_prepare_v2(conn, sql, -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); - const uint8_t *secret = sqlite3_column_text(stmt, 2); - const double last_backup = sqlite3_column_double(stmt, 3); - const uint8_t *backup_status = sqlite3_column_text(stmt, 4); - - result->clients[index].id = clientId; - strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX); - strncpy(result->clients[index].secret, secret ? (char*)secret : "", CLIENT_SECRET_LEN); - result->clients[index].last_backup = time_from_double(last_backup); - - if (backup_status == NULL) { - strncpy(result->clients[index].status, "MISSING", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else if (strcmp((char*)backup_status, "RUNNING") == 0) { - strncpy(result->clients[index].status, "RUNNING", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else if (strcmp((char*)backup_status, "INTERRUPTED") == 0) { - strncpy(result->clients[index].status, "INTERRUPTED", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else { - strncpy(result->clients[index].status, "OK", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } - - index++; - } - if(ret != SQLITE_DONE) { - abort(); - } - - result->clients_len = index; - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int get_client(struct Request *req, int64_t client_id, struct GetClientOnlyResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - result->found = 0; - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id, name, secret FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { - abort(); - } - - int ret = sqlite3_step(stmt); - if(ret == SQLITE_ROW) { - result->found = 1; - result->id = sqlite3_column_int64(stmt, 0); - const uint8_t *name = sqlite3_column_text(stmt, 1); - const uint8_t *secret = sqlite3_column_text(stmt, 2); - strncpy(result->name, (char*)name, CLIENT_NAME_MAX); - strncpy(result->secret, (char*)secret, CLIENT_SECRET_LEN); - } else if(ret != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int get_client_backups(struct Request *req, int64_t client_id, struct GetBackupsResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - uint16_t limit = result->backups_len; - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id, status, unixepoch(started, 'subsec'), unixepoch(completed, 'subsec') FROM backups WHERE client = ?1 ORDER BY started DESC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int(stmt, 2, limit) != SQLITE_OK) { - abort(); - } - - int ret; - int index = 0; - while((ret = sqlite3_step(stmt)) == SQLITE_ROW) { - if(index >= limit) break; - - result->backups[index].id = sqlite3_column_int64(stmt, 0); - - const uint8_t *status = sqlite3_column_text(stmt, 1); - const double started = sqlite3_column_double(stmt, 2); - const double completed = sqlite3_column_double(stmt, 3); - - strncpy(result->backups[index].status, status ? (char*)status : "", BACKUP_STATUS_MAX); - result->backups[index].started = time_from_double(started); - result->backups[index].completed = time_from_double(completed); - - index++; - } - if(ret != SQLITE_DONE && ret != SQLITE_ROW) { - abort(); - } - - result->backups_len = index; - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -static void emit_stylesheet(FILE *f) { - fprintf(f, - "\n" - ); -} - -static void mask_secret(const char *secret, char *out, size_t out_len) { - size_t len = strlen(secret); - if (len >= 8 && out_len >= 13) { - snprintf(out, out_len, "%.4s....%.4s", secret, secret + len - 4); - } else if (len > 0) { - snprintf(out, out_len, "****"); - } else { - out[0] = '\0'; - } -} - -static const char *status_to_badge_class(const char *status) { - if (strcmp(status, "OK") == 0) return "ok"; - if (strcmp(status, "RUNNING") == 0) return "run"; - if (strcmp(status, "STALE") == 0) return "stale"; - if (strcmp(status, "INTERRUPTED") == 0) return "fail"; - if (strcmp(status, "FINISHED") == 0) return "ok"; - return "ok"; -} - -static const char *status_to_display(const char *status) { - if (strcmp(status, "OK") == 0) return "OK"; - if (strcmp(status, "RUNNING") == 0) return "Running"; - if (strcmp(status, "STALE") == 0) return "Stale"; - if (strcmp(status, "INTERRUPTED") == 0) return "Failed"; - if (strcmp(status, "FINISHED") == 0) return "Done"; - return status; -} - -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; - } - - // Count stale clients - int stale_count = 0; - for (int i = 0; i < result->clients_len; i++) { - if (result->clients[i].is_stale) stale_count++; - } - - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) { - free(result); - return NULL; - } - - fprintf(f, - "\n" - "\n" - "\n" - " \n" - " \n" - " BorgFlag - Clients\n"); - emit_stylesheet(f); - fprintf(f, - "\n" - "\n" - "
\n" - " BorgFlag\n" - "
\n" - "
\n" - "
\n" - "

\n" - " Clients\n" - " %d registered", result->clients_len); - - if (stale_count > 0) { - fprintf(f, "%d stale", stale_count); - } - - fprintf(f, - "\n" - " \n" - "

\n" - "
\n" - "
\n" - " \n" - " \n" - "
\n" - " \n" - "
\n" - "
\n" - " Name\n" - " Secret\n" - " Last Backup\n" - " Status\n" - " \n" - ); - - for (int i = 0; i < result->clients_len; i++) { - char masked_secret[16]; - mask_secret(result->clients[i].secret, masked_secret, sizeof(masked_secret)); - - fprintf(f, - " \n" - " %s\n" - " %s\n" - " %s\n" - " %s\n" - " \n" - " \n", - result->clients[i].id, - result->clients[i].is_stale ? " class=\"stale\"" : "", - result->clients[i].name, - masked_secret, - result->clients[i].last_backup.tv_sec > 0.0 ? format_time(result->clients[i].last_backup).buf : "-", - status_to_badge_class(result->clients[i].status), - status_to_display(result->clients[i].status), - result->clients[i].id - ); - } - - fprintf(f, - "
\n" - "
\n" - "\n" - "\n"); - - fclose(f); - *out_len = buf_len; - free(result); - return buf; -} - -static char *render_client_detail_page(struct Request *request, int64_t client_id, size_t *out_len) { - struct GetClientOnlyResult client_result; - int ret = get_client(request, client_id, &client_result); - if(ret != 0) { - return NULL; - } - - if(!client_result.found) { - return NULL; - } - - struct GetBackupsResult *backups_result = malloc(sizeof(struct GetBackupsResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY); - backups_result->backups_len = MAX_BACKUPS_DISPLAY; - ret = get_client_backups(request, client_id, backups_result); - if(ret != 0) { - free(backups_result); - return NULL; - } - - // Determine status and staleness - int is_stale = 0; - const char *current_status = "OK"; - if (backups_result->backups_len > 0) { - const char *last_status = backups_result->backups[0].status; - if (strcmp(last_status, "RUNNING") == 0) { - current_status = "RUNNING"; - } else if (strcmp(last_status, "INTERRUPTED") == 0) { - current_status = "INTERRUPTED"; - } - } - - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) { - free(backups_result); - return NULL; - } - - fprintf(f, - "\n" - "\n" - "\n" - " \n" - " \n" - " BorgFlag - %s\n", client_result.name); - emit_stylesheet(f); - fprintf(f, - "\n" - "\n" - "\n" - "
\n" - " BorgFlag\n" - "
\n" - "
\n" - "
\n" - " ← Back to clients\n" - " \n" - "

%s

\n" - "

%d backups %s

\n", - is_stale ? " class=\"stale\"" : "", - client_result.name, - backups_result->backups_len, - status_to_badge_class(current_status), - status_to_display(current_status) - ); - - if (is_stale) { - fprintf(f, - "

Warning: Last backup was more than 24 hours ago. Expected every 24 hours.

\n" - ); - } - - fprintf(f, - " Secret: %s\n" - " \n" - "
\n" - " API Snippets\n" - "
\n" - "
\n" - "

Start backup

\n" - "
curl -X POST http://%s/api/report \\\n"
-		"     -H \"Content-Type: application/json\" \\\n"
-		"     -d '{\n"
-		"           \"client\": \"%s\",\n"
-		"           \"secret\": \"%s\",\n"
-		"           \"event\":  \"begin_backup\"\n"
-		"         }'
\n" - "
\n" - "
\n" - "

End backup

\n" - "
curl -X POST http://%s/api/report \\\n"
-		"     -H \"Content-Type: application/json\" \\\n"
-		"     -d '{\n"
-		"           \"client\": \"%s\",\n"
-		"           \"secret\": \"%s\",\n"
-		"           \"event\":  \"end_backup\"\n"
-		"         }'
\n" - "
\n" - "
\n" - "
\n", - client_result.secret, - request->host ? request->host : "localhost", - client_result.name, - client_result.secret, - request->host ? request->host : "localhost", - client_result.name, - client_result.secret - ); - - fprintf(f, "

Recent Backups

\n"); - - if(backups_result->backups_len == 0) { - fprintf(f, "

No backups recorded.

\n"); - } else { - fprintf(f, "
\n"); - - for (int i = 0; i < backups_result->backups_len; i++) { - const char *badge_class; - const char *badge_text; - if (strcmp(backups_result->backups[i].status, "RUNNING") == 0) { - badge_class = "run"; - badge_text = "Running"; - } else if (strcmp(backups_result->backups[i].status, "INTERRUPTED") == 0) { - badge_class = "fail"; - badge_text = "Failed"; - } else { - badge_class = "ok"; - badge_text = "Done"; - } - - struct timespec duration = time_sub(&backups_result->backups[i].completed, &backups_result->backups[i].started); - fprintf(f, - "
%s%s
\n", - format_time(backups_result->backups[i].started).buf, - format_duration(duration).buf, - badge_class, - badge_text - ); - } - - fprintf(f, "
\n"); - } - - fprintf(f, - "
\n" - "\n" - "\n"); - - fclose(f); - *out_len = buf_len; - free(backups_result); - return buf; -} - -static char *render_login_page(const char *error_msg, size_t *out_len) { - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) return NULL; - - fprintf(f, - "\n" - "\n" - "\n" - " \n" - " \n" - " BorgFlag - Login\n"); - emit_stylesheet(f); - fprintf(f, - "\n" - "\n" - "
\n" - "
\n" - "

BorgFlag

\n"); - - if (error_msg) { - fprintf(f, "
%s
\n", error_msg); - } - - fprintf(f, - "
\n" - " \n" - " \n" - "
\n" - "
\n" - " \n" - " \n" - "
\n" - " \n" - "
\n" - "
\n" - "\n" - "\n"); - - fclose(f); - *out_len = buf_len; - return buf; -} - static enum MHD_Result create_post_collector(struct PostCollector *c) { c->data = malloc(512); c->size = 0; @@ -1438,7 +190,7 @@ static enum MHD_Result handler( if (!request->form.username_set || !request->form.password_set) { size_t html_len; - char *html = render_login_page("Invalid credentials", &html_len); + char *html = html_render_login_page("Invalid credentials", &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1451,7 +203,7 @@ static enum MHD_Result handler( if (!validate_credentials(server->app, request->form.username, request->form.password)) { size_t html_len; - char *html = render_login_page("Invalid credentials", &html_len); + char *html = html_render_login_page("Invalid credentials", &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1495,7 +247,7 @@ static enum MHD_Result handler( } struct CreateClientResult result; - ret = create_client(request, request->form.name, &result); + ret = create_client(server->app, request->form.name, &result); if(ret != 0) return MHD_NO; // Post-Redirect-Get: redirect back to GET /clients @@ -1524,7 +276,7 @@ static enum MHD_Result handler( return MHD_NO; } - ret = delete_client(request, request->form.client_id); + ret = delete_client(server->app, request->form.client_id); if(ret != 0) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( @@ -1552,17 +304,17 @@ static enum MHD_Result handler( if(!cJSON_IsString(eventJson) || eventJson->valuestring == NULL) { abort(); } - enum ReportEvent event; + int is_finish; if(strcmp(eventJson->valuestring, "begin_backup") == 0) { - event = REV_BEGIN; + is_finish = 0; } else if(strcmp(eventJson->valuestring, "end_backup") == 0) { - event = REV_FINISH; + is_finish = 1; } else { abort(); } - ret = submit_report(server->app, clientJson->valuestring, secretJson->valuestring, event); + ret = submit_report(server->app, clientJson->valuestring, secretJson->valuestring, is_finish); if(ret == -2) { cJSON_Delete(rootJson); destroy_post_collector(&request->pp); @@ -1614,7 +366,7 @@ static enum MHD_Result handler( } struct CreateClientResult result; - ret = create_client(request, nameJson->valuestring, &result); + ret = create_client(server->app, nameJson->valuestring, &result); if(ret != 0) { // Should actually return an error page, but this will do for now return MHD_NO; @@ -1668,7 +420,7 @@ static enum MHD_Result handler( if(strcmp(url, "/login") == 0) { size_t html_len; - char *html = render_login_page(NULL, &html_len); + char *html = html_render_login_page(NULL, &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1688,8 +440,18 @@ static enum MHD_Result handler( return ret; } + // Fetch clients from app layer + struct ListClientResult *clients = malloc(sizeof(struct ListClientResult) + sizeof(struct ListClientResultClient) * 16); + ret = list_clients(server->app, clients); + if(ret != 0) { + free(clients); + return MHD_NO; + } + + // Render HTML size_t html_len; - char *html = render_clients_page(request, &html_len); + char *html = html_render_clients_page(request->host, clients, &html_len); + free(clients); if (!html) { return MHD_NO; @@ -1730,8 +492,34 @@ static enum MHD_Result handler( return ret; } + // Fetch client data from app layer + struct GetClientOnlyResult client_result; + ret = get_client(server->app, client_id, &client_result); + if(ret != 0) { + return MHD_NO; + } + + if(!client_result.found) { + // Not Found + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response); + MHD_destroy_response(response); + return ret; + } + + struct GetBackupsResult *backups_result = malloc(sizeof(struct GetBackupsResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY); + backups_result->backups_len = MAX_BACKUPS_DISPLAY; + ret = get_client_backups(server->app, client_id, backups_result); + if(ret != 0) { + free(backups_result); + return MHD_NO; + } + + // Render HTML size_t html_len; - char *html = render_client_detail_page(request, client_id, &html_len); + char *html = html_render_client_detail_page(request->host, &client_result, backups_result, &html_len); + free(backups_result); if (!html) { // Not Found diff --git a/src/web.h b/src/web.h index dd6e824..2bd85ea 100644 --- a/src/web.h +++ b/src/web.h @@ -1,25 +1,12 @@ #pragma once +#include "app.h" + #include #include -#include struct MHD_PostProcessor; -#define CLIENT_NAME_MAX 32 -#define CLIENT_SECRET_LEN 33 -#define BACKUP_STATUS_MAX 16 -#define MAX_BACKUPS_DISPLAY 50 - -struct App { - const char* dbname; - char session_secret[CLIENT_SECRET_LEN]; - const char *admin_user; - const char *admin_pass; -}; - -void prepare_database(struct App *app); - struct Server { struct App *app; @@ -53,51 +40,3 @@ struct Request { struct FormData form; const char *host; }; - -struct CreateClientResult { - int64_t id; - char* name; - char secret[CLIENT_SECRET_LEN]; -}; - -int create_client(struct Request *req, char *name, struct CreateClientResult *result); -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 timespec last_backup; - char status[BACKUP_STATUS_MAX]; // "OK", "RUNNING", "STALE", "FAILED", "INTERRUPTED" - int is_stale; -}; - -struct ListClientResult { - uint16_t clients_len; - // Right now this has to be exactly 16 elements long, but it could be longer in the future - struct ListClientResultClient clients[]; -}; - -int list_clients(struct Request *req, struct ListClientResult *result); - -struct BackupRecord { - int64_t id; - char status[BACKUP_STATUS_MAX]; - struct timespec started; - struct timespec completed; -}; - -struct GetClientOnlyResult { - int found; - int64_t id; - char name[CLIENT_NAME_MAX]; - char secret[CLIENT_SECRET_LEN]; -}; - -struct GetBackupsResult { - uint16_t backups_len; // On input: max backups to fetch; On output: actual count - struct BackupRecord backups[]; -}; - -int get_client(struct Request *req, int64_t client_id, struct GetClientOnlyResult *result); -int get_client_backups(struct Request *req, int64_t client_id, struct GetBackupsResult *result); -- cgit v1.2.3