trainlog/tui/src/web_server.c

1385 lines
60 KiB
C

#include "trainlog/web_server.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <microhttpd.h>
#include <netinet/in.h>
#include <sys/random.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <uuid/uuid.h>
#include <yyjson.h>
#include "trainlog/web_dashboard.h"
#include "trainlog/web_prepared_items.h"
#include "trainlog/web_sessions.h"
#include "trainlog/dashboard_layout.h"
#include "trainlog/web_preferences.h"
#include "web_assets.h"
#define TRAINLOG_HTTP_CONNECTION_LIMIT 32U
#define TRAINLOG_HTTP_PER_IP_LIMIT 32U
#define TRAINLOG_HTTP_CONNECTION_MEMORY_LIMIT (16U * 1024U)
#define TRAINLOG_HTTP_HEADER_LIMIT (8U * 1024U)
#define TRAINLOG_HTTP_BODY_LIMIT (128U * 1024U)
#define TRAINLOG_HTTP_LEGACY_BODY_LIMIT (4U * 1024U)
#define TRAINLOG_HTTP_TIMEOUT_SECONDS 10U
#define TRAINLOG_HTTP_BACKLOG 32U
#define TRAINLOG_HTTP_POLL_MAX_MS 250ULL
#define TRAINLOG_SYNC_ACCEPTED_JSON_CAPACITY 256U
typedef struct TrainlogWebContext {
uint16_t port;
TrainlogDatabase *database;
const char *database_path;
char state_path[PATH_MAX + 32U];
pid_t worker_pid;
char csrf_token[65];
bool invalid_layout_reported;
bool invalid_preferences_reported;
} TrainlogWebContext;
typedef struct TrainlogHttpRequestState {
size_t body_size;
bool body_too_large;
char body[TRAINLOG_HTTP_BODY_LIMIT + 1U];
} TrainlogHttpRequestState;
static bool valid_sync_id(const char *value) {
return value != NULL && strlen(value) == 39U && strncmp(value, "sy_", 3U) == 0 &&
uuid_parse(value + 3U, (unsigned char[16]){0}) == 0;
}
bool trainlog_web_sync_accepted_serialize(const char *request_id,
const char *run_id,
char *output,
size_t capacity,
size_t *output_size) {
int written;
if (output == NULL || capacity == 0U || output_size == NULL) {
return false;
}
output[0] = '\0';
*output_size = 0U;
if (!valid_sync_id(request_id) || !valid_sync_id(run_id)) {
return false;
}
written = snprintf(output,
capacity,
"{\"api_version\":1,\"phase\":\"requested\",\"result\":\"running\","
"\"progress_revision\":1,\"request_id\":\"%s\",\"run_id\":\"%s\","
"\"status_url\":\"/api/v1/sync/status\"}\n",
request_id,
run_id);
if (written < 0 || (size_t)written >= capacity) {
output[0] = '\0';
return false;
}
*output_size = (size_t)written;
return true;
}
static void web_diagnostic(char *output, size_t capacity, const char *message) {
if (output != NULL && capacity > 0U) {
(void)snprintf(output, capacity, "%s", message);
}
}
static enum MHD_Result queue_layout_json(struct MHD_Connection *connection,
unsigned int status,
const char *body,
uint64_t revision,
const char *csrf_token) {
struct MHD_Response *response;
enum MHD_Result result;
char etag[32];
response = MHD_create_response_from_buffer(strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
if (response == NULL) {
return MHD_NO;
}
(void)snprintf(etag, sizeof(etag), "\"%llu\"", (unsigned long long)revision);
if (MHD_add_response_header(
response, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json; charset=utf-8") != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, "no-store") != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff") !=
MHD_YES ||
MHD_add_response_header(response,
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'") !=
MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag) != MHD_YES ||
MHD_add_response_header(response, "X-Trainlog-CSRF-Token", csrf_token) != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONNECTION, "close") != MHD_YES) {
MHD_destroy_response(response);
return MHD_NO;
}
result = MHD_queue_response(connection, status, response);
MHD_destroy_response(response);
return result;
}
static enum MHD_Result queue_json(struct MHD_Connection *connection,
unsigned int status,
const char *body,
const char *allow) {
struct MHD_Response *response;
enum MHD_Result result;
response = MHD_create_response_from_buffer(strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
if (response == NULL) {
return MHD_NO;
}
if (MHD_add_response_header(
response, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json; charset=utf-8") != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff") !=
MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, "no-store") != MHD_YES ||
MHD_add_response_header(response,
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'") !=
MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONNECTION, "close") != MHD_YES) {
MHD_destroy_response(response);
return MHD_NO;
}
if (allow != NULL &&
MHD_add_response_header(response, MHD_HTTP_HEADER_ALLOW, allow) != MHD_YES) {
MHD_destroy_response(response);
return MHD_NO;
}
result = MHD_queue_response(connection, status, response);
MHD_destroy_response(response);
return result;
}
static enum MHD_Result queue_sync_json_sized(struct MHD_Connection *connection,
unsigned int status,
const char *body,
size_t body_size,
const char *csrf_token,
const char *location) {
struct MHD_Response *response =
MHD_create_response_from_buffer(body_size, (void *)body, MHD_RESPMEM_MUST_COPY);
enum MHD_Result result;
if (response == NULL) {
return MHD_NO;
}
if (MHD_add_response_header(
response, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json; charset=utf-8") != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, "no-store") != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff") !=
MHD_YES ||
MHD_add_response_header(response,
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'; base-uri 'none'") !=
MHD_YES ||
MHD_add_response_header(response, "X-Trainlog-CSRF-Token", csrf_token) != MHD_YES ||
(location != NULL &&
MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION, location) != MHD_YES)) {
MHD_destroy_response(response);
return MHD_NO;
}
result = MHD_queue_response(connection, status, response);
MHD_destroy_response(response);
return result;
}
static enum MHD_Result queue_sync_json(struct MHD_Connection *connection,
unsigned int status,
const char *body,
const char *csrf_token,
const char *location) {
return queue_sync_json_sized(connection, status, body, strlen(body), csrf_token, location);
}
static bool read_bounded_file(const char *path, char *output, size_t capacity) {
int descriptor;
struct stat info;
size_t used = 0U;
if (path == NULL || output == NULL || capacity < 2U) {
return false;
}
descriptor = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (descriptor < 0) {
return false;
}
if (fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0 ||
(uintmax_t)info.st_size >= (uintmax_t)capacity) {
(void)close(descriptor);
return false;
}
while (used < (size_t)info.st_size) {
ssize_t count = read(descriptor, output + used, (size_t)info.st_size - used);
if (count < 0 && errno == EINTR) {
continue;
}
if (count <= 0) {
(void)close(descriptor);
return false;
}
used += (size_t)count;
}
output[used] = '\0';
(void)close(descriptor);
return true;
}
static bool sync_state_active(const char *json, char request_id[64]) {
yyjson_doc *document = yyjson_read(json, strlen(json), 0U);
yyjson_val *root;
yyjson_val *phase;
yyjson_val *request;
const char *text;
bool active = false;
if (document == NULL) {
return false;
}
root = yyjson_doc_get_root(document);
phase = yyjson_obj_get(root, "phase");
request = yyjson_obj_get(root, "request_id");
if (yyjson_is_str(request) && strlen(yyjson_get_str(request)) < 64U) {
(void)snprintf(request_id, 64U, "%s", yyjson_get_str(request));
}
if (yyjson_is_str(phase)) {
text = yyjson_get_str(phase);
active = strcmp(text, "requested") == 0 ||
strcmp(text, "waiting_android_publication") == 0 || strcmp(text, "running") == 0 ||
strcmp(text, "local_import_committed") == 0 || strcmp(text, "published") == 0 ||
strcmp(text, "waiting_acknowledgement") == 0 || strcmp(text, "peer_consumed") == 0;
}
yyjson_doc_free(document);
return active;
}
static bool valid_sync_request(const char *body, size_t size, char request_id[64]) {
yyjson_doc *document = yyjson_read(body, size, 0U);
yyjson_val *root;
yyjson_val *request;
yyjson_val *trigger;
const char *id;
if (document == NULL) {
return false;
}
root = yyjson_doc_get_root(document);
if (!yyjson_is_obj(root) || yyjson_obj_size(root) != 2U) {
yyjson_doc_free(document);
return false;
}
request = yyjson_obj_get(root, "request_id");
trigger = yyjson_obj_get(root, "trigger");
if (!yyjson_is_str(request) || !yyjson_is_str(trigger) ||
strcmp(yyjson_get_str(trigger), "web") != 0) {
yyjson_doc_free(document);
return false;
}
id = yyjson_get_str(request);
if (strlen(id) != 39U || strncmp(id, "sy_", 3U) != 0 ||
uuid_parse(id + 3U, (unsigned char[16]){0}) != 0) {
yyjson_doc_free(document);
return false;
}
(void)snprintf(request_id, 64U, "%s", id);
yyjson_doc_free(document);
return true;
}
static void generate_sync_run_id(char run_id[64]) {
uuid_t identifier;
char uuid[37];
uuid_generate_random(identifier);
uuid_unparse_lower(identifier, uuid);
(void)snprintf(run_id, 64U, "sy_%s", uuid);
}
static bool write_initial_sync_state(const TrainlogWebContext *context,
const char *request_id,
const char *run_id,
char output[2048]) {
char temporary[PATH_MAX + 64U];
char timestamp[40];
time_t now = time(NULL);
struct tm broken_down;
int descriptor;
int written;
ssize_t count;
if (now == (time_t)-1 || gmtime_r(&now, &broken_down) == NULL ||
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &broken_down) == 0U) {
return false;
}
written = snprintf(
output,
2048U,
"{\"ai_midpoint\":{\"result\":\"not_configured\"},\"ai_post_sync\":{\"result\":\"not_"
"configured\"},"
"\"diagnostic\":\"\",\"domains\":{},\"drafts\":[],\"effective_mode\":\"full_generation_"
"v1\","
"\"finished_at\":null,\"inbound_generation_id\":null,\"missing_capabilities\":[],"
"\"outbound_generation_id\":null,\"phase\":\"requested\",\"producer_peer_id\":null,"
"\"consumer_peer_id\":null,\"progress_revision\":1,\"request_id\":\"%s\","
"\"requested_mode\":\"full_generation_v1\",\"result\":\"running\",\"run_id\":\"%s\","
"\"sessions_reconciled\":null,\"started_at\":\"%s\",\"trigger\":\"web\",\"updated_at\":\"%"
"s\"}\n",
request_id,
run_id,
timestamp,
timestamp);
if (written < 0 || written >= 2048) {
return false;
}
if (snprintf(temporary, sizeof(temporary), "%s.new.%ld", context->state_path, (long)getpid()) <
0) {
return false;
}
descriptor = open(temporary, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (descriptor < 0) {
return false;
}
count = write(descriptor, output, (size_t)written);
if (count != written || fsync(descriptor) != 0 || close(descriptor) != 0 ||
rename(temporary, context->state_path) != 0) {
(void)close(descriptor);
(void)unlink(temporary);
return false;
}
return true;
}
static bool launch_sync_worker(TrainlogWebContext *context,
const char *config,
const char *run_id,
const char *request_id) {
char orchestrator[PATH_MAX];
const char *tools = getenv("TRAINLOG_SYNC_TOOLS_DIR");
int path_length;
if (tools == NULL) {
tools = TRAINLOG_TOOLS_DIR;
}
/* CONTRACT: deployment may relocate the fixed shipped helper directory,
* but an HTTP request can never select this executable or its arguments. */
path_length = snprintf(orchestrator, sizeof(orchestrator), "%s/sync_orchestrator.py", tools);
if (tools[0] != '/' || path_length < 0 || (size_t)path_length >= sizeof(orchestrator)) {
return false;
}
pid_t child = fork();
if (child < 0) {
return false;
}
if (child == 0) {
(void)setpgid(0, 0);
execlp("python3",
"python3",
orchestrator,
"--database",
context->database_path,
"--state",
context->state_path,
"--config",
config,
"--run-id",
run_id,
"--request-id",
request_id,
(char *)NULL);
_exit(127);
}
context->worker_pid = child;
return true;
}
static enum MHD_Result queue_asset(struct MHD_Connection *connection,
const TrainlogWebAsset *asset) {
static const char CSP[] = "default-src 'self'; script-src 'self'; "
"style-src 'self'; connect-src 'self'; img-src 'self'; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'";
const char *cache = asset->immutable ? "public, max-age=31536000, immutable" : "no-cache";
struct MHD_Response *response =
MHD_create_response_from_buffer(asset->size, (void *)asset->bytes, MHD_RESPMEM_PERSISTENT);
enum MHD_Result result;
if (response == NULL) {
return MHD_NO;
}
if (MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, asset->mime) != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff") !=
MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, cache) != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, asset->etag) != MHD_YES ||
MHD_add_response_header(response, "Content-Security-Policy", CSP) != MHD_YES ||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONNECTION, "close") != MHD_YES) {
MHD_destroy_response(response);
return MHD_NO;
}
result = MHD_queue_response(connection, MHD_HTTP_OK, response);
MHD_destroy_response(response);
return result;
}
static bool is_ui_route(const char *url) {
static const char *const routes[] = {"/", "/analyse", "/programmes", "/seances", "/exercices"};
size_t index;
for (index = 0U; index < sizeof(routes) / sizeof(routes[0]); ++index) {
if (strcmp(url, routes[index]) == 0) {
return true;
}
}
/* CONTRACT: Sessions detail identities are client-side path segments. A
* direct load must receive the SPA, while the /api guard below remains
* authoritative for every adapter route. */
if (strncmp(url, "/seances/", strlen("/seances/")) == 0) {
return true;
}
return false;
}
static bool valid_host(const TrainlogWebContext *context, const char *host) {
char expected[32];
int written;
if (host == NULL) {
return false;
}
if (strcmp(host, "127.0.0.1") == 0) {
return true;
}
written = snprintf(expected, sizeof(expected), "127.0.0.1:%u", (unsigned int)context->port);
return written > 0 && (size_t)written < sizeof(expected) && strcmp(host, expected) == 0;
}
static bool declared_body_too_large(struct MHD_Connection *connection) {
const char *value =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_LENGTH);
char *end = NULL;
unsigned long long size;
if (value == NULL) {
return false;
}
errno = 0;
size = strtoull(value, &end, 10);
return errno != 0 || end == value || *end != '\0' || size > TRAINLOG_HTTP_BODY_LIMIT;
}
static bool valid_origin(const TrainlogWebContext *context, const char *origin) {
char direct[48];
if (origin == NULL) {
return false;
}
(void)snprintf(direct, sizeof(direct), "http://127.0.0.1:%u", (unsigned int)context->port);
return strcmp(origin, direct) == 0 || strcmp(origin, "http://trainlog.perf") == 0;
}
static bool valid_csrf(const TrainlogWebContext *context, const char *provided) {
size_t index;
unsigned char difference = 0U;
if (provided == NULL || strlen(provided) != 64U) {
return false;
}
for (index = 0U; index < 64U; ++index) {
difference |= (unsigned char)(provided[index] ^ context->csrf_token[index]);
}
return difference == 0U;
}
static bool generate_csrf_token(char output[65]) {
unsigned char random_bytes[32];
size_t used = 0U;
size_t index;
while (used < sizeof(random_bytes)) {
ssize_t count = getrandom(random_bytes + used, sizeof(random_bytes) - used, 0);
if (count < 0 && errno == EINTR) {
continue;
}
if (count <= 0) {
return false;
}
used += (size_t)count;
}
for (index = 0U; index < sizeof(random_bytes); ++index) {
(void)snprintf(output + index * 2U, 3U, "%02x", random_bytes[index]);
}
return true;
}
static bool parse_if_match(const char *value, uint64_t *revision) {
char *end = NULL;
unsigned long long parsed;
if (value == NULL || value[0] != '"') {
return false;
}
errno = 0;
parsed = strtoull(value + 1, &end, 10);
if (errno != 0 || end == value + 1 || end[0] != '"' || end[1] != '\0' ||
parsed > TRAINLOG_DASHBOARD_LAYOUT_MAX_REVISION) {
return false;
}
*revision = (uint64_t)parsed;
return true;
}
static bool
parse_page_number(const char *value, size_t default_value, size_t maximum, size_t *out) {
char *end = NULL;
unsigned long long parsed;
if (value == NULL) {
*out = default_value;
return true;
}
errno = 0;
parsed = strtoull(value, &end, 10);
if (errno != 0 || end == value || *end != '\0' || parsed > maximum) {
return false;
}
*out = (size_t)parsed;
return true;
}
static enum MHD_Result queue_owned_sessions_json(struct MHD_Connection *connection,
TrainlogStatus status,
char *json,
size_t json_size) {
enum MHD_Result result;
if (status == TRAINLOG_STATUS_NOT_FOUND) {
free(json);
return queue_json(connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
if (status != TRAINLOG_STATUS_OK || json == NULL || json_size == 0U) {
free(json);
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"sessions_unavailable\"}\n",
NULL);
}
(void)json_size;
result = queue_json(connection, MHD_HTTP_OK, json, NULL);
free(json);
return result;
}
static enum MHD_Result handle_request(void *closure,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **request_closure) {
static const char HEALTH[] = "{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
"\"version\":\"" TRAINLOG_VERSION "\"}\n";
TrainlogWebContext *context = closure;
TrainlogHttpRequestState *state = *request_closure;
const union MHD_ConnectionInfo *header_info;
const TrainlogWebAsset *asset;
const char *host;
bool is_get;
bool is_head;
(void)version;
if (state == NULL) {
state = calloc(1U, sizeof(*state));
if (state == NULL) {
return MHD_NO;
}
state->body_too_large = declared_body_too_large(connection);
*request_closure = state;
return MHD_YES;
}
if (*upload_data_size > 0U) {
if (*upload_data_size >
TRAINLOG_HTTP_BODY_LIMIT - (state->body_size > TRAINLOG_HTTP_BODY_LIMIT
? TRAINLOG_HTTP_BODY_LIMIT
: state->body_size)) {
state->body_too_large = true;
} else {
(void)memcpy(state->body + state->body_size, upload_data, *upload_data_size);
state->body_size += *upload_data_size;
state->body[state->body_size] = '\0';
}
*upload_data_size = 0U;
return MHD_YES;
}
header_info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_REQUEST_HEADER_SIZE);
if (header_info == NULL || header_info->header_size > TRAINLOG_HTTP_HEADER_LIMIT) {
return queue_json(connection,
MHD_HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE,
"{\"error\":\"headers_too_large\"}\n",
NULL);
}
if (state->body_too_large ||
(strncmp(url, "/api/v1/sessions/", strlen("/api/v1/sessions/")) != 0 &&
state->body_size > TRAINLOG_HTTP_LEGACY_BODY_LIMIT)) {
return queue_json(
connection, MHD_HTTP_CONTENT_TOO_LARGE, "{\"error\":\"body_too_large\"}\n", NULL);
}
host = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_HOST);
if (!valid_host(context, host)) {
return queue_json(connection, MHD_HTTP_BAD_REQUEST, "{\"error\":\"invalid_host\"}\n", NULL);
}
is_get = strcmp(method, MHD_HTTP_METHOD_GET) == 0;
is_head = strcmp(method, MHD_HTTP_METHOD_HEAD) == 0;
if (strcmp(url, "/api/v1/health") == 0) {
if (!is_get) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET");
}
return queue_json(connection, MHD_HTTP_OK, HEALTH, NULL);
}
if (strcmp(url, "/api/v1/sync/status") == 0) {
static const char IDLE_DISABLED[] =
"{\"api_version\":1,\"enabled\":false,\"phase\":\"idle\","
"\"result\":\"disabled\",\"last_success\":null}\n";
static const char IDLE_ENABLED[] = "{\"api_version\":1,\"enabled\":true,\"phase\":\"idle\","
"\"result\":\"idle\",\"last_success\":null}\n";
char status_json[65537];
if (!is_get) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET");
}
if (!read_bounded_file(context->state_path, status_json, sizeof(status_json))) {
return queue_sync_json(connection,
MHD_HTTP_OK,
getenv("TRAINLOG_SYNC_GENERATION_CONFIG") == NULL ? IDLE_DISABLED
: IDLE_ENABLED,
context->csrf_token,
NULL);
}
return queue_sync_json(connection, MHD_HTTP_OK, status_json, context->csrf_token, NULL);
}
if (strcmp(url, "/api/v1/sync") == 0) {
const char *origin;
const char *csrf;
const char *content_type;
const char *config = getenv("TRAINLOG_SYNC_GENERATION_CONFIG");
char request_id[64] = "";
char existing_id[64] = "";
char existing[65537];
char initial[2048];
char run_id[64];
char accepted[TRAINLOG_SYNC_ACCEPTED_JSON_CAPACITY];
size_t accepted_size;
if (strcmp(method, MHD_HTTP_METHOD_POST) != 0) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"POST");
}
origin = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Origin");
csrf = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "X-Trainlog-CSRF-Token");
if (!valid_origin(context, origin) || !valid_csrf(context, csrf)) {
return queue_json(
connection, MHD_HTTP_FORBIDDEN, "{\"error\":\"mutation_forbidden\"}\n", NULL);
}
content_type =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
if (content_type == NULL || strcmp(content_type, "application/json") != 0) {
return queue_json(connection,
MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"{\"error\":\"unsupported_media_type\"}\n",
NULL);
}
if (!valid_sync_request(state->body, state->body_size, request_id)) {
return queue_json(
connection, MHD_HTTP_BAD_REQUEST, "{\"error\":\"invalid_sync_request\"}\n", NULL);
}
if (config == NULL || config[0] != '/' || strlen(config) >= PATH_MAX) {
return queue_json(
connection, MHD_HTTP_CONFLICT, "{\"error\":\"full_generation_disabled\"}\n", NULL);
}
if (read_bounded_file(context->state_path, existing, sizeof(existing))) {
bool active = sync_state_active(existing, existing_id);
if (strcmp(existing_id, request_id) == 0) {
return queue_sync_json(connection,
active ? MHD_HTTP_ACCEPTED : MHD_HTTP_OK,
existing,
context->csrf_token,
"/api/v1/sync/status");
}
if (active) {
return queue_json(
connection, MHD_HTTP_CONFLICT, "{\"error\":\"sync_already_running\"}\n", NULL);
}
}
generate_sync_run_id(run_id);
/* INVARIANT: a request is made durable only after its complete 202 response is known to
* fit, so no admitted job can become unreportable because of response truncation. */
if (!trainlog_web_sync_accepted_serialize(
request_id, run_id, accepted, sizeof(accepted), &accepted_size) ||
!write_initial_sync_state(context, request_id, run_id, initial) ||
!launch_sync_worker(context, config, run_id, request_id)) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"sync_worker_start_failed\"}\n",
NULL);
}
return queue_sync_json_sized(connection,
MHD_HTTP_ACCEPTED,
accepted,
accepted_size,
context->csrf_token,
"/api/v1/sync/status");
}
if (strcmp(url, "/api/v1/dashboard") == 0) {
TrainlogWebDashboardQuery query;
TrainlogWebDashboardSnapshot snapshot;
char json[TRAINLOG_WEB_JSON_CAPACITY];
size_t json_size;
time_t now;
if (!is_get) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET");
}
now = time(NULL);
if (now == (time_t)-1 || (int64_t)now != (int64_t)(time_t)now) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"clock_unavailable\"}\n",
NULL);
}
query.reference_unix_second = (int64_t)now;
if (trainlog_web_dashboard_load(context->database, &query, &snapshot) !=
TRAINLOG_STATUS_OK ||
trainlog_web_dashboard_serialize(&snapshot, json, sizeof(json), &json_size) !=
TRAINLOG_STATUS_OK ||
json_size == 0U) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"dashboard_unavailable\"}\n",
NULL);
}
return queue_json(connection, MHD_HTTP_OK, json, NULL);
}
if (strcmp(url, "/api/v1/prepared-items") == 0) {
char json[TRAINLOG_WEB_PREPARED_ITEMS_JSON_CAPACITY];
size_t json_size = 0U;
TrainlogWebPreparedItems items;
if (!is_get) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET");
}
if (trainlog_web_prepared_items_load(context->database, &items) != TRAINLOG_STATUS_OK ||
trainlog_web_prepared_items_serialize(&items, json, sizeof(json), &json_size) !=
TRAINLOG_STATUS_OK) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"prepared_items_unavailable\"}\n",
NULL);
}
return queue_json(connection, MHD_HTTP_OK, json, NULL);
}
if (strncmp(url, "/api/v1/sessions", strlen("/api/v1/sessions")) == 0) {
static const struct {
const char *path;
TrainlogWebSessionCollection collection;
} collections[] = {
{"/api/v1/sessions/preparations", TRAINLOG_WEB_SESSION_PREPARATIONS},
{"/api/v1/sessions/proposals", TRAINLOG_WEB_SESSION_PROPOSALS},
{"/api/v1/sessions/drafts", TRAINLOG_WEB_SESSION_DRAFTS},
{"/api/v1/sessions/history", TRAINLOG_WEB_SESSION_HISTORY},
};
const char *offset_value;
const char *limit_value;
const char *search;
char *json = NULL;
size_t json_size = 0U;
size_t offset;
size_t limit;
size_t index;
if (!is_get && (strcmp(url, "/api/v1/sessions/preparations") == 0 ||
strncmp(url,
"/api/v1/sessions/preparation/",
strlen("/api/v1/sessions/preparation/")) == 0)) {
const char *origin = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Origin");
const char *csrf =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "X-Trainlog-CSRF-Token");
const char *content_type = MHD_lookup_connection_value(
connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
const char *request_id =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "X-Trainlog-Request-ID");
const char *if_match =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MATCH);
const char *identity = NULL;
const char *expected = "";
char expected_buffer[TRAINLOG_ID_MAX + 1U];
char identity_buffer[TRAINLOG_ID_MAX + 1U];
bool delivery = false;
bool withdrawal = false;
TrainlogStatus status;
if ((strcmp(url, "/api/v1/sessions/preparations") == 0 &&
strcmp(method, MHD_HTTP_METHOD_POST) != 0) ||
(strcmp(url, "/api/v1/sessions/preparations") != 0 &&
strcmp(method, MHD_HTTP_METHOD_PUT) != 0 &&
strcmp(method, MHD_HTTP_METHOD_POST) != 0 &&
strcmp(method, MHD_HTTP_METHOD_DELETE) != 0)) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"POST, PUT, DELETE");
}
if (!valid_origin(context, origin) || !valid_csrf(context, csrf)) {
return queue_json(
connection, MHD_HTTP_FORBIDDEN, "{\"error\":\"mutation_forbidden\"}\n", NULL);
}
if (content_type == NULL || strcmp(content_type, "application/json") != 0 ||
request_id == NULL || request_id[0] == '\0' || strlen(request_id) > 128U) {
return queue_json(connection,
MHD_HTTP_BAD_REQUEST,
"{\"error\":\"invalid_mutation_headers\"}\n",
NULL);
}
if (strcmp(url, "/api/v1/sessions/preparations") != 0) {
size_t length;
identity = url + strlen("/api/v1/sessions/preparation/");
length = strlen(identity);
if (length > strlen("/deliver") &&
strcmp(identity + length - strlen("/deliver"), "/deliver") == 0) {
size_t identity_length = length - strlen("/deliver");
if (identity_length == 0U || identity_length > TRAINLOG_ID_MAX) {
return queue_json(connection,
MHD_HTTP_BAD_REQUEST,
"{\"error\":\"invalid_identity\"}\n",
NULL);
}
(void)memcpy(identity_buffer, identity, identity_length);
identity_buffer[identity_length] = '\0';
identity = identity_buffer;
delivery = true;
}
withdrawal = !delivery && strcmp(method, MHD_HTTP_METHOD_DELETE) == 0;
if ((delivery && strcmp(method, MHD_HTTP_METHOD_POST) != 0) ||
(!delivery && !withdrawal && strcmp(method, MHD_HTTP_METHOD_PUT) != 0)) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
delivery ? "POST" : "PUT, DELETE");
}
if (if_match == NULL || if_match[0] != '"') {
return queue_json(connection,
MHD_HTTP_PRECONDITION_REQUIRED,
"{\"error\":\"precondition_required\"}\n",
NULL);
}
length = strlen(if_match);
if (length < 3U || length - 2U > TRAINLOG_ID_MAX || if_match[length - 1U] != '"') {
return queue_json(connection,
MHD_HTTP_BAD_REQUEST,
"{\"error\":\"invalid_revision\"}\n",
NULL);
}
(void)memcpy(expected_buffer, if_match + 1, length - 2U);
expected_buffer[length - 2U] = '\0';
expected = expected_buffer;
}
if (delivery) {
status = trainlog_web_sessions_deliver_json(
context->database, identity, expected, request_id, &json, &json_size);
} else if (withdrawal) {
status = trainlog_web_sessions_withdraw_json(
context->database, identity, expected, request_id, &json, &json_size);
} else {
status = trainlog_web_sessions_save_json(context->database,
identity,
expected,
request_id,
state->body,
state->body_size,
&json,
&json_size);
}
if (status == TRAINLOG_STATUS_CONFLICT) {
free(json);
return queue_json(
connection, MHD_HTTP_CONFLICT, "{\"error\":\"revision_conflict\"}\n", NULL);
}
if (status == TRAINLOG_STATUS_NOT_FOUND) {
free(json);
return queue_json(
connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
if (status == TRAINLOG_STATUS_INVALID_ARGUMENT) {
free(json);
return queue_json(connection,
MHD_HTTP_UNPROCESSABLE_CONTENT,
"{\"error\":\"invalid_preparation\"}\n",
NULL);
}
return queue_owned_sessions_json(connection, status, json, json_size);
}
if (!is_get) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET");
}
offset_value = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "offset");
limit_value = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "limit");
search = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "search");
if (!parse_page_number(offset_value, 0U, 1000000U, &offset) ||
!parse_page_number(limit_value, 24U, TRAINLOG_WEB_SESSIONS_PAGE_MAX, &limit) ||
limit == 0U) {
return queue_json(
connection, MHD_HTTP_BAD_REQUEST, "{\"error\":\"invalid_page\"}\n", NULL);
}
if (strcmp(url, "/api/v1/sessions/catalog") == 0) {
TrainlogStatus status = trainlog_web_sessions_catalog_json(
context->database, search, offset, limit, &json, &json_size);
return queue_owned_sessions_json(connection, status, json, json_size);
}
for (index = 0U; index < sizeof(collections) / sizeof(collections[0]); ++index) {
if (strcmp(url, collections[index].path) == 0) {
TrainlogWebSessionsPageQuery query = {
collections[index].collection, offset, limit, search};
TrainlogStatus status =
trainlog_web_sessions_list_json(context->database, &query, &json, &json_size);
return queue_owned_sessions_json(connection, status, json, json_size);
}
}
{
static const char DETAIL_PREFIX[] = "/api/v1/sessions/";
const char *kind = url + strlen(DETAIL_PREFIX);
const char *separator = strchr(kind, '/');
char kind_buffer[32];
if (separator != NULL && separator[1] != '\0' &&
(size_t)(separator - kind) < sizeof(kind_buffer)) {
TrainlogStatus status;
(void)memcpy(kind_buffer, kind, (size_t)(separator - kind));
kind_buffer[separator - kind] = '\0';
status = trainlog_web_sessions_detail_json(
context->database, kind_buffer, separator + 1, &json, &json_size);
return queue_owned_sessions_json(connection, status, json, json_size);
}
}
return queue_json(connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
if (strcmp(url, "/api/v1/web-preferences") == 0) {
TrainlogWebPreferences preferences;
TrainlogWebPreferences saved;
TrainlogWebPreferencesSource source;
TrainlogWebPreferencesResult preferences_result;
char json[TRAINLOG_WEB_PREFERENCES_JSON_CAPACITY];
size_t json_size;
uint64_t expected;
const char *origin;
const char *csrf;
const char *if_match;
if (is_get) {
preferences_result = trainlog_web_preferences_load(&preferences, &source);
if (preferences_result != TRAINLOG_WEB_PREFERENCES_OK ||
!trainlog_web_preferences_serialize(&preferences,
source,
true,
json,
sizeof(json),
&json_size)) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"preferences_unavailable\"}\n",
NULL);
}
if (source == TRAINLOG_WEB_PREFERENCES_INVALID_PERSISTED &&
!context->invalid_preferences_reported) {
(void)fprintf(stderr,
"Trainlog Web : préférences persistées invalides, défaut utilisé\n");
context->invalid_preferences_reported = true;
}
return queue_layout_json(connection,
MHD_HTTP_OK,
json,
preferences.revision,
context->csrf_token);
}
if (strcmp(method, MHD_HTTP_METHOD_PUT) != 0) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET, PUT");
}
origin = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Origin");
csrf = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "X-Trainlog-CSRF-Token");
if (!valid_origin(context, origin) || !valid_csrf(context, csrf)) {
return queue_json(
connection, MHD_HTTP_FORBIDDEN, "{\"error\":\"mutation_forbidden\"}\n", NULL);
}
if_match =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MATCH);
if (!parse_if_match(if_match, &expected)) {
return queue_json(connection,
MHD_HTTP_PRECONDITION_REQUIRED,
"{\"error\":\"precondition_required\"}\n",
NULL);
}
{
const char *content_type = MHD_lookup_connection_value(
connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
if (content_type == NULL || strcmp(content_type, "application/json") != 0) {
return queue_json(connection,
MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"{\"error\":\"unsupported_media_type\"}\n",
NULL);
}
}
preferences_result =
trainlog_web_preferences_parse(state->body, state->body_size, &preferences);
if (preferences_result != TRAINLOG_WEB_PREFERENCES_OK ||
preferences.revision != expected) {
return queue_json(connection,
MHD_HTTP_BAD_REQUEST,
"{\"error\":\"invalid_preferences\"}\n",
NULL);
}
preferences_result =
trainlog_web_preferences_save(&preferences, expected, &saved);
if (preferences_result == TRAINLOG_WEB_PREFERENCES_CONFLICT) {
return queue_json(connection,
MHD_HTTP_PRECONDITION_FAILED,
"{\"error\":\"revision_conflict\"}\n",
NULL);
}
if (preferences_result != TRAINLOG_WEB_PREFERENCES_OK ||
!trainlog_web_preferences_serialize(&saved,
TRAINLOG_WEB_PREFERENCES_PERSISTED,
true,
json,
sizeof(json),
&json_size)) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"preferences_save_failed\"}\n",
NULL);
}
return queue_layout_json(
connection, MHD_HTTP_OK, json, saved.revision, context->csrf_token);
}
if (strcmp(url, "/api/v1/dashboard-layout") == 0) {
TrainlogDashboardLayout layout;
TrainlogDashboardLayout saved;
TrainlogDashboardLayoutSource source;
TrainlogDashboardLayoutResult layout_result;
char json[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
size_t json_size;
uint64_t expected;
const char *origin;
const char *csrf;
const char *if_match;
/* WHY: loopback alone does not stop a hostile webpage from submitting
* requests to a local service. CONTRACT: every mutation needs exact
* trusted Origin, the startup-random header token and an If-Match ETag.
* INVARIANT: proxy Host rewriting remains strict and no CORS is added. */
if (is_get) {
layout_result = trainlog_dashboard_layout_load(&layout, &source);
if (layout_result != TRAINLOG_DASHBOARD_LAYOUT_OK ||
!trainlog_dashboard_layout_serialize(
&layout, source, true, json, sizeof(json), &json_size) ||
json_size == 0U) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"layout_unavailable\"}\n",
NULL);
}
if (source == TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED &&
!context->invalid_layout_reported) {
(void)fprintf(stderr,
"Trainlog Web : agencement persisté invalide, défaut utilisé\n");
context->invalid_layout_reported = true;
}
return queue_layout_json(
connection, MHD_HTTP_OK, json, layout.revision, context->csrf_token);
}
if (strcmp(method, MHD_HTTP_METHOD_PUT) != 0 &&
strcmp(method, MHD_HTTP_METHOD_DELETE) != 0) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET, PUT, DELETE");
}
origin = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Origin");
csrf = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "X-Trainlog-CSRF-Token");
if (!valid_origin(context, origin) || !valid_csrf(context, csrf)) {
return queue_json(
connection, MHD_HTTP_FORBIDDEN, "{\"error\":\"mutation_forbidden\"}\n", NULL);
}
if_match =
MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MATCH);
if (!parse_if_match(if_match, &expected)) {
return queue_json(connection,
MHD_HTTP_PRECONDITION_REQUIRED,
"{\"error\":\"precondition_required\"}\n",
NULL);
}
if (strcmp(method, MHD_HTTP_METHOD_DELETE) == 0) {
if (state->body_size != 0U) {
return queue_json(
connection, MHD_HTTP_BAD_REQUEST, "{\"error\":\"unexpected_body\"}\n", NULL);
}
layout_result = trainlog_dashboard_layout_delete(expected);
if (layout_result == TRAINLOG_DASHBOARD_LAYOUT_CONFLICT) {
return queue_json(connection,
MHD_HTTP_PRECONDITION_FAILED,
"{\"error\":\"revision_conflict\"}\n",
NULL);
}
if (layout_result != TRAINLOG_DASHBOARD_LAYOUT_OK) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"layout_delete_failed\"}\n",
NULL);
}
trainlog_dashboard_layout_default(&layout);
if (!trainlog_dashboard_layout_serialize(&layout,
TRAINLOG_DASHBOARD_LAYOUT_DEFAULT,
true,
json,
sizeof(json),
&json_size)) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"layout_unavailable\"}\n",
NULL);
}
return queue_layout_json(connection, MHD_HTTP_OK, json, 0U, context->csrf_token);
}
{
const char *content_type = MHD_lookup_connection_value(
connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
if (content_type == NULL || strcmp(content_type, "application/json") != 0) {
return queue_json(connection,
MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"{\"error\":\"unsupported_media_type\"}\n",
NULL);
}
}
layout_result = trainlog_dashboard_layout_parse(state->body, state->body_size, &layout);
if (layout_result != TRAINLOG_DASHBOARD_LAYOUT_OK || layout.revision != expected) {
return queue_json(
connection, MHD_HTTP_BAD_REQUEST, "{\"error\":\"invalid_layout\"}\n", NULL);
}
layout_result = trainlog_dashboard_layout_save(&layout, expected, &saved);
if (layout_result == TRAINLOG_DASHBOARD_LAYOUT_CONFLICT) {
return queue_json(connection,
MHD_HTTP_PRECONDITION_FAILED,
"{\"error\":\"revision_conflict\"}\n",
NULL);
}
if (layout_result != TRAINLOG_DASHBOARD_LAYOUT_OK ||
!trainlog_dashboard_layout_serialize(&saved,
TRAINLOG_DASHBOARD_LAYOUT_PERSISTED,
true,
json,
sizeof(json),
&json_size)) {
return queue_json(connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"{\"error\":\"layout_save_failed\"}\n",
NULL);
}
return queue_layout_json(
connection, MHD_HTTP_OK, json, saved.revision, context->csrf_token);
}
/* INVARIANT: API paths never fall through to the SPA index. */
if (strncmp(url, "/api/", 5U) == 0) {
return queue_json(connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
if (strstr(url, "..") != NULL) {
return queue_json(connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
asset = is_ui_route(url) ? trainlog_web_index_asset() : trainlog_web_asset_find(url);
if (asset == NULL) {
return queue_json(connection, MHD_HTTP_NOT_FOUND, "{\"error\":\"not_found\"}\n", NULL);
}
if (!is_get && !is_head) {
return queue_json(connection,
MHD_HTTP_METHOD_NOT_ALLOWED,
"{\"error\":\"method_not_allowed\"}\n",
"GET, HEAD");
}
return queue_asset(connection, asset);
}
static void request_completed(void *closure,
struct MHD_Connection *connection,
void **request_closure,
enum MHD_RequestTerminationCode termination) {
(void)closure;
(void)connection;
(void)termination;
free(*request_closure);
*request_closure = NULL;
}
static enum MHD_Result
accept_loopback_only(void *closure, const struct sockaddr *address, socklen_t address_length) {
const struct sockaddr_in *ipv4;
(void)closure;
if (address == NULL || address_length < (socklen_t)sizeof(*ipv4) ||
address->sa_family != AF_INET) {
return MHD_NO;
}
ipv4 = (const struct sockaddr_in *)address;
return ipv4->sin_addr.s_addr == htonl(INADDR_LOOPBACK) ? MHD_YES : MHD_NO;
}
int trainlog_web_server_run(TrainlogDatabase *database,
const char *database_path,
uint16_t port,
const volatile sig_atomic_t *stop_requested,
char *diagnostic,
size_t diagnostic_capacity) {
struct sockaddr_in bind_address;
struct MHD_Daemon *daemon;
TrainlogWebContext context;
int result = 0;
if (database == NULL || database_path == NULL || database_path[0] != '/' ||
stop_requested == NULL) {
return -1;
}
web_diagnostic(diagnostic, diagnostic_capacity, "");
if (!trainlog_web_assets_available()) {
web_diagnostic(diagnostic, diagnostic_capacity, "support frontend absent de ce binaire");
return -1;
}
(void)memset(&bind_address, 0, sizeof(bind_address));
bind_address.sin_family = AF_INET;
bind_address.sin_port = htons(port);
bind_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
context.port = port;
context.database = database;
context.database_path = database_path;
context.worker_pid = -1;
if (snprintf(
context.state_path, sizeof(context.state_path), "%s.sync-run.json", database_path) <
0 ||
strlen(context.state_path) >= sizeof(context.state_path) - 1U) {
web_diagnostic(diagnostic, diagnostic_capacity, "chemin d'état sync trop long");
return -1;
}
context.invalid_layout_reported = false;
context.invalid_preferences_reported = false;
if (!generate_csrf_token(context.csrf_token)) {
web_diagnostic(diagnostic, diagnostic_capacity, "impossible de générer la protection CSRF");
return -1;
}
errno = 0;
daemon = MHD_start_daemon(MHD_USE_ERROR_LOG | MHD_USE_NO_THREAD_SAFETY,
0U,
accept_loopback_only,
NULL,
handle_request,
&context,
MHD_OPTION_SOCK_ADDR_LEN,
(socklen_t)sizeof(bind_address),
&bind_address,
MHD_OPTION_CONNECTION_LIMIT,
TRAINLOG_HTTP_CONNECTION_LIMIT,
MHD_OPTION_PER_IP_CONNECTION_LIMIT,
TRAINLOG_HTTP_PER_IP_LIMIT,
MHD_OPTION_CONNECTION_MEMORY_LIMIT,
(size_t)TRAINLOG_HTTP_CONNECTION_MEMORY_LIMIT,
MHD_OPTION_CONNECTION_TIMEOUT,
TRAINLOG_HTTP_TIMEOUT_SECONDS,
MHD_OPTION_LISTEN_BACKLOG_SIZE,
TRAINLOG_HTTP_BACKLOG,
MHD_OPTION_NOTIFY_COMPLETED,
request_completed,
NULL,
MHD_OPTION_END);
if (daemon == NULL) {
char message[256];
(void)snprintf(message,
sizeof(message),
"impossible d'écouter sur 127.0.0.1:%u%s%s",
(unsigned int)port,
errno == 0 ? "" : " : ",
errno == 0 ? "" : strerror(errno));
web_diagnostic(diagnostic, diagnostic_capacity, message);
return -1;
}
(void)printf("Trainlog Web : http://127.0.0.1:%u/\n", (unsigned int)port);
(void)fflush(stdout);
while (*stop_requested == 0) {
fd_set read_set;
fd_set write_set;
fd_set except_set;
struct timeval timeout;
unsigned long long timeout_ms = TRAINLOG_HTTP_POLL_MAX_MS;
int maximum_fd = 0;
int selected;
if (context.worker_pid > 0) {
int worker_status;
if (waitpid(context.worker_pid, &worker_status, WNOHANG) == context.worker_pid) {
context.worker_pid = -1;
}
}
FD_ZERO(&read_set);
FD_ZERO(&write_set);
FD_ZERO(&except_set);
if (MHD_get_fdset(daemon, &read_set, &write_set, &except_set, &maximum_fd) != MHD_YES) {
web_diagnostic(
diagnostic, diagnostic_capacity, "impossible de préparer la boucle HTTP");
result = -1;
break;
}
if (MHD_get_timeout(daemon, &timeout_ms) != MHD_YES ||
timeout_ms > TRAINLOG_HTTP_POLL_MAX_MS) {
timeout_ms = TRAINLOG_HTTP_POLL_MAX_MS;
}
timeout.tv_sec = (time_t)(timeout_ms / 1000ULL);
timeout.tv_usec = (suseconds_t)((timeout_ms % 1000ULL) * 1000ULL);
selected = select(maximum_fd + 1, &read_set, &write_set, &except_set, &timeout);
if (selected < 0) {
if (errno == EINTR) {
continue;
}
web_diagnostic(diagnostic, diagnostic_capacity, "échec de la boucle HTTP locale");
result = -1;
break;
}
if (*stop_requested == 0 &&
MHD_run_from_select(daemon, &read_set, &write_set, &except_set) != MHD_YES) {
web_diagnostic(diagnostic, diagnostic_capacity, "échec du traitement HTTP local");
result = -1;
break;
}
}
MHD_stop_daemon(daemon);
if (context.worker_pid > 0) {
int worker_status;
pid_t waited = waitpid(context.worker_pid, &worker_status, WNOHANG);
if (waited == 0) {
size_t attempt;
struct timespec delay = {0, 100000000L};
(void)kill(-context.worker_pid, SIGTERM);
for (attempt = 0U; attempt < 50U; ++attempt) {
waited = waitpid(context.worker_pid, &worker_status, WNOHANG);
if (waited == context.worker_pid || waited < 0) {
break;
}
(void)nanosleep(&delay, NULL);
}
if (waited == 0) {
(void)kill(-context.worker_pid, SIGKILL);
(void)waitpid(context.worker_pid, &worker_status, 0);
}
}
}
return result;
}