feat(web): add local HTTP infrastructure
This commit is contained in:
parent
456713586b
commit
7597a167f8
16 changed files with 936 additions and 107 deletions
10
README.md
10
README.md
|
|
@ -19,8 +19,8 @@ over direct MTP; it never copies SQLite database files between devices.
|
||||||
| Local Web (planned) | Analyze and visualize canonical data; prepare programs and sessions through typed Trainlog Core services. |
|
| Local Web (planned) | Analyze and visualize canonical data; prepare programs and sessions through typed Trainlog Core services. |
|
||||||
|
|
||||||
No interface reconstructs business truth from SQLite tables. The local Web is
|
No interface reconstructs business truth from SQLite tables. The local Web is
|
||||||
a sibling adapter, not an extension of the TUI; `TRAINLOG_WEB_V1` is currently
|
a sibling adapter, not an extension of the TUI. Its loopback-only CLI/HTTP
|
||||||
contract-only and has no implementation.
|
infrastructure is implemented; its frontend and business API are not.
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
|
|
@ -137,6 +137,7 @@ requires compatible versions of:
|
||||||
- libudev;
|
- libudev;
|
||||||
- libmtp;
|
- libmtp;
|
||||||
- Notcurses Core;
|
- Notcurses Core;
|
||||||
|
- GNU libmicrohttpd;
|
||||||
- the standard math library.
|
- the standard math library.
|
||||||
|
|
||||||
Distribution packages may pull additional transitive libraries, including
|
Distribution packages may pull additional transitive libraries, including
|
||||||
|
|
@ -164,7 +165,8 @@ Desktop builds require:
|
||||||
- a C17 compiler toolchain;
|
- a C17 compiler toolchain;
|
||||||
- `pkg-config`;
|
- `pkg-config`;
|
||||||
- Notcurses Core development headers;
|
- Notcurses Core development headers;
|
||||||
- SQLite, libuuid, utf8proc, libudev, and libmtp development headers;
|
- SQLite, libuuid, utf8proc, libudev, libmtp, and GNU libmicrohttpd development
|
||||||
|
headers;
|
||||||
- Python 3 for generated sources, validators, import/export helpers, and tests.
|
- Python 3 for generated sources, validators, import/export helpers, and tests.
|
||||||
|
|
||||||
Android builds require JDK 17, an Android SDK supporting the configured API
|
Android builds require JDK 17, an Android SDK supporting the configured API
|
||||||
|
|
@ -229,7 +231,7 @@ As of 2026-09-15:
|
||||||
- `APP_SHELL_V1=IMPLEMENTED_AWAITING_VISUAL_REVIEW_2`;
|
- `APP_SHELL_V1=IMPLEMENTED_AWAITING_VISUAL_REVIEW_2`;
|
||||||
- `TRAINLOG_AI_SESSION_DRAFT_V1=VALIDATION_PENDING` pending a real Drive plus
|
- `TRAINLOG_AI_SESSION_DRAFT_V1=VALIDATION_PENDING` pending a real Drive plus
|
||||||
Android-triggered bidirectional smoke test.
|
Android-triggered bidirectional smoke test.
|
||||||
- `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED`.
|
- `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED`.
|
||||||
|
|
||||||
The latest executable result belongs in
|
The latest executable result belongs in
|
||||||
[current state](docs/current_state.md), not in multiple README narratives.
|
[current state](docs/current_state.md), not in multiple README narratives.
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,30 @@ opened and migrated once before HTTP listening, used without concurrent direct
|
||||||
SQLite-handle access, and closed by the process owner after server shutdown. A
|
SQLite-handle access, and closed by the process owner after server shutdown. A
|
||||||
single-threaded event loop is the preferred server architecture.
|
single-threaded event loop is the preferred server architecture.
|
||||||
|
|
||||||
|
`WEB_CLI_HTTP_INFRASTRUCTURE_V1=PASS/FROZEN` implements that owner. CLI parsing
|
||||||
|
precedes all path, database, terminal and HTTP initialization. The common owner
|
||||||
|
resolves the private XDG database path, opens/migrates it exactly once, dispatches
|
||||||
|
to either the unchanged TUI or Web adapter, then closes it exactly once.
|
||||||
|
`--help`, `--version` and CLI errors stop before path or database access.
|
||||||
|
|
||||||
|
The HTTP adapter uses GNU libmicrohttpd in external-select mode with
|
||||||
|
`MHD_USE_NO_THREAD_SAFETY`: libmicrohttpd creates no polling or request thread.
|
||||||
|
All callbacks, future Core calls and SQLite access therefore run serially on
|
||||||
|
the process event-loop thread. A concrete IPv4 `sockaddr_in` binds only
|
||||||
|
`127.0.0.1`; port zero remains unavailable through the CLI. SIGINT/SIGTERM
|
||||||
|
handlers only set a `sig_atomic_t` flag. The loop then stops libmicrohttpd,
|
||||||
|
returns to the process owner and lets it close the database.
|
||||||
|
|
||||||
|
The only implemented route is read-only `GET /api/v1/health`; `/` and every
|
||||||
|
unknown route return 404, and other methods on health return 405. The adapter
|
||||||
|
bounds concurrent/per-IP connections at 32, listen backlog at 32, per-connection
|
||||||
|
memory at 16 KiB, decoded request headers at 8 KiB, request bodies at 4 KiB and
|
||||||
|
idle time at 10 seconds. It validates `Host` against `127.0.0.1` and the active
|
||||||
|
port, emits explicit JSON content type, `nosniff`, `no-store`, a deny-all CSP
|
||||||
|
and no permissive CORS header. Assets, telemetry and remote resources are
|
||||||
|
absent. Before the first mutation route, origin validation and explicit
|
||||||
|
CSRF protection remain mandatory in addition to these transport bounds.
|
||||||
|
|
||||||
Every Web page retains one application shell:
|
Every Web page retains one application shell:
|
||||||
|
|
||||||
- Header navigation: Dashboard, Analyse, Programmes, Séances, Exercices;
|
- Header navigation: Dashboard, Analyse, Programmes, Séances, Exercices;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ parallel implementation of its rules.
|
||||||
| Dashboard characterization | `WEB_DASHBOARD_CHARACTERIZATION_V1=PASS/FROZEN` |
|
| Dashboard characterization | `WEB_DASHBOARD_CHARACTERIZATION_V1=PASS/FROZEN` |
|
||||||
| Dashboard Core read model | `WEB_DASHBOARD_CORE_READ_MODEL_V1=PASS/FROZEN` |
|
| Dashboard Core read model | `WEB_DASHBOARD_CORE_READ_MODEL_V1=PASS/FROZEN` |
|
||||||
| TUI Dashboard adoption | `WEB_TUI_READ_MODEL_ADOPTION_V1=PASS/FROZEN` |
|
| TUI Dashboard adoption | `WEB_TUI_READ_MODEL_ADOPTION_V1=PASS/FROZEN` |
|
||||||
| Local Web | `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED` |
|
| Web CLI/HTTP infrastructure | `WEB_CLI_HTTP_INFRASTRUCTURE_V1=PASS/FROZEN` |
|
||||||
|
| Local Web | `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED` |
|
||||||
|
|
||||||
Desktop and Android schema numbers are independent. Neither changes the frozen
|
Desktop and Android schema numbers are independent. Neither changes the frozen
|
||||||
Trainlog JSON V1 contract.
|
Trainlog JSON V1 contract.
|
||||||
|
|
@ -136,8 +137,10 @@ the result. No Dashboard HTTP endpoint exists yet.
|
||||||
|
|
||||||
`TRAINLOG_WEB_V1` architecture, API independence, local-network boundary,
|
`TRAINLOG_WEB_V1` architecture, API independence, local-network boundary,
|
||||||
browser shell, Dashboard, layout ownership, build/runtime separation, and
|
browser shell, Dashboard, layout ownership, build/runtime separation, and
|
||||||
security invariants are now canonical. No Web server, frontend, CLI option,
|
security invariants are canonical. The CLI and local HTTP infrastructure are
|
||||||
HTTP endpoint, embedded asset, or layout persistence is implemented yet.
|
implemented: `-w`/`--web`, optional `--port`, exact IPv4 loopback binding and
|
||||||
|
the technical `/api/v1/health` route. No frontend, embedded asset, Dashboard
|
||||||
|
route, business mutation, browser launch or layout persistence exists yet.
|
||||||
|
|
||||||
## Data semantics
|
## Data semantics
|
||||||
|
|
||||||
|
|
@ -169,6 +172,10 @@ The Dashboard Core extraction tranche passes **62/62 normal Meson tests** and
|
||||||
**62/62 ASan/UBSan Meson tests**, including the standalone public-header and
|
**62/62 ASan/UBSan Meson tests**, including the standalone public-header and
|
||||||
Core characterization targets, plus both canonical JSON validators.
|
Core characterization targets, plus both canonical JSON validators.
|
||||||
|
|
||||||
|
The CLI/HTTP infrastructure tranche passes **67/67 normal Meson tests**, adding
|
||||||
|
pure CLI parsing, process-level help/version checks and an isolated loopback
|
||||||
|
HTTP lifecycle/security suite. The same suite is required under ASan/UBSan.
|
||||||
|
|
||||||
`TRAINLOG_I18N_V0_1_1=PASS` is covered by desktop presentation, persistence,
|
`TRAINLOG_I18N_V0_1_1=PASS` is covered by desktop presentation, persistence,
|
||||||
formatting, layout-invariance and source-derived text-boundary tests, plus
|
formatting, layout-invariance and source-derived text-boundary tests, plus
|
||||||
Android resource-parity, language-owner, typed sync-presentation, and
|
Android resource-parity, language-owner, typed sync-presentation, and
|
||||||
|
|
@ -181,9 +188,8 @@ smoke test are not automated. The latter is why
|
||||||
|
|
||||||
## Active limitations
|
## Active limitations
|
||||||
|
|
||||||
- `TRAINLOG_WEB_V1` remains contract-only; Dashboard characterization, Core
|
- `TRAINLOG_WEB_V1` has only its Core prerequisite and CLI/HTTP infrastructure;
|
||||||
extraction and TUI adoption are frozen. The next implementation cursor is
|
the next cursor is the frontend shell. No Dashboard HTTP route exists.
|
||||||
CLI/local HTTP infrastructure, not React.
|
|
||||||
- `APP_SHELL_V1` still awaits the recorded human visual/accessibility review.
|
- `APP_SHELL_V1` still awaits the recorded human visual/accessibility review.
|
||||||
- AI proposal exchange still awaits one real Drive plus Android-triggered
|
- AI proposal exchange still awaits one real Drive plus Android-triggered
|
||||||
bidirectional smoke test.
|
bidirectional smoke test.
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,20 @@ Desktop schema v18, Android schema v17, Notcurses, direct
|
||||||
Zones V1, Training Feedback V1/V2, STATS V1, and Session Generator V1 are
|
Zones V1, Training Feedback V1/V2, STATS V1, and Session Generator V1 are
|
||||||
implemented. Session Generator V1 is hidden pending V2. AI session-draft
|
implemented. Session Generator V1 is hidden pending V2. AI session-draft
|
||||||
exchange and APP_SHELL_V1 retain their explicit manual validation/review gates.
|
exchange and APP_SHELL_V1 retain their explicit manual validation/review gates.
|
||||||
`TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED`; its local-only
|
`TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_STARTED`; its local-only
|
||||||
architecture is canonical, but no Web code exists yet.
|
CLI/HTTP infrastructure exists, but no frontend or business endpoint exists.
|
||||||
|
|
||||||
## Current cursor
|
## Current cursor
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CURRENT_OPERATIONAL_CURSOR=WEB_CLI_HTTP_INFRASTRUCTURE_V1
|
CURRENT_OPERATIONAL_CURSOR=WEB_FRONTEND_SHELL_V1
|
||||||
```
|
```
|
||||||
|
|
||||||
`WEB_DASHBOARD_CORE_READ_MODEL_V1=PASS/FROZEN` and
|
`WEB_CLI_HTTP_INFRASTRUCTURE_V1=PASS/FROZEN`. `trainlog -w` and `--web` now
|
||||||
`WEB_TUI_READ_MODEL_ADOPTION_V1=PASS/FROZEN`. The characterized Dashboard SQL,
|
dispatch through the common process owner to a bounded, single-threaded
|
||||||
facts, limits and calculations now live in the typed Core service
|
loopback HTTP adapter after one database open. Only `/api/v1/health` exists.
|
||||||
`trainlog_dashboard_load()`; the TUI supplies the selected period and current
|
The next slice is the frontend shell and embedded production assets; it does
|
||||||
time, then presents the returned snapshot. Adoption was necessary to prove
|
not authorize Dashboard business routes or new metrics.
|
||||||
parity and was therefore completed in the extraction tranche rather than left
|
|
||||||
as an empty roadmap step. The next authorized slice is local CLI/HTTP
|
|
||||||
infrastructure; it does not yet authorize React or Dashboard Web metrics.
|
|
||||||
|
|
||||||
## TRAINLOG_WEB_V1 implementation gate
|
## TRAINLOG_WEB_V1 implementation gate
|
||||||
|
|
||||||
|
|
@ -50,7 +47,7 @@ Implementation order is fixed as:
|
||||||
WEB_DASHBOARD_CHARACTERIZATION_V1
|
WEB_DASHBOARD_CHARACTERIZATION_V1
|
||||||
-> WEB_DASHBOARD_CORE_READ_MODEL_V1 [PASS/FROZEN]
|
-> WEB_DASHBOARD_CORE_READ_MODEL_V1 [PASS/FROZEN]
|
||||||
-> WEB_TUI_READ_MODEL_ADOPTION_V1 [PASS/FROZEN]
|
-> WEB_TUI_READ_MODEL_ADOPTION_V1 [PASS/FROZEN]
|
||||||
-> WEB_CLI_HTTP_INFRASTRUCTURE_V1
|
-> WEB_CLI_HTTP_INFRASTRUCTURE_V1 [PASS/FROZEN]
|
||||||
-> WEB_FRONTEND_SHELL_V1
|
-> WEB_FRONTEND_SHELL_V1
|
||||||
-> WEB_DASHBOARD_V1
|
-> WEB_DASHBOARD_V1
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,20 @@ A Trainlog feature is not complete without relevant validation.
|
||||||
Frozen formats, persistence migrations, synchronization semantics, and user-data
|
Frozen formats, persistence migrations, synchronization semantics, and user-data
|
||||||
mutations require executable coverage where practical.
|
mutations require executable coverage where practical.
|
||||||
|
|
||||||
|
`WEB_CLI_HTTP_INFRASTRUCTURE_V1` adds pure CLI tests for the default TUI mode,
|
||||||
|
both Web switches, help, version, unknown and repeated options, positional
|
||||||
|
arguments, Web-only `--port`, missing/non-numeric/negative/zero/out-of-range
|
||||||
|
ports, 65535, and option ordering. Process-level tests execute `--help` and
|
||||||
|
`--version` without initializing the database, TUI or server.
|
||||||
|
|
||||||
|
The isolated HTTP test opens one in-memory database, starts the production
|
||||||
|
single-threaded adapter on an explicitly reserved loopback port, and verifies
|
||||||
|
health JSON/version, content type, `nosniff`, CSP, absence of CORS, 404, 405
|
||||||
|
with `Allow: GET`, invalid Host, oversized body, oversized-header rejection,
|
||||||
|
SIGTERM shutdown and explicit failure on an already occupied port. It never
|
||||||
|
uses the Internet. A separate real-launch smoke uses an isolated XDG directory,
|
||||||
|
performs the health request and stops the production binary with SIGINT.
|
||||||
|
|
||||||
## 2. Frozen Trainlog JSON v1
|
## 2. Frozen Trainlog JSON v1
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
|
||||||
27
tui/include/trainlog/cli.h
Normal file
27
tui/include/trainlog/cli.h
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef TRAINLOG_CLI_H
|
||||||
|
#define TRAINLOG_CLI_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum TrainlogRunMode {
|
||||||
|
TRAINLOG_RUN_TUI = 0,
|
||||||
|
TRAINLOG_RUN_WEB,
|
||||||
|
TRAINLOG_RUN_HELP,
|
||||||
|
TRAINLOG_RUN_VERSION
|
||||||
|
} TrainlogRunMode;
|
||||||
|
|
||||||
|
typedef struct TrainlogCliOptions {
|
||||||
|
TrainlogRunMode mode;
|
||||||
|
uint16_t port;
|
||||||
|
} TrainlogCliOptions;
|
||||||
|
|
||||||
|
/* CONTRACT: parsing is side-effect free apart from getopt's process-local
|
||||||
|
* state, which is reset for every call. No database, terminal or server is
|
||||||
|
* initialized. diagnostic is always NUL-terminated when capacity is nonzero. */
|
||||||
|
int trainlog_cli_parse(int argc, char *const argv[], TrainlogCliOptions *options,
|
||||||
|
char *diagnostic, size_t diagnostic_capacity);
|
||||||
|
|
||||||
|
void trainlog_cli_print_usage(const char *program_name);
|
||||||
|
|
||||||
|
#endif
|
||||||
11
tui/include/trainlog/paths.h
Normal file
11
tui/include/trainlog/paths.h
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#ifndef TRAINLOG_PATHS_H
|
||||||
|
#define TRAINLOG_PATHS_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Resolve and create the private XDG data directory, then return trainlog.db.
|
||||||
|
* The caller owns output; truncation and non-directory collisions fail. */
|
||||||
|
int trainlog_paths_database(char *output, size_t output_capacity,
|
||||||
|
char *diagnostic, size_t diagnostic_capacity);
|
||||||
|
|
||||||
|
#endif
|
||||||
17
tui/include/trainlog/web_server.h
Normal file
17
tui/include/trainlog/web_server.h
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#ifndef TRAINLOG_WEB_SERVER_H
|
||||||
|
#define TRAINLOG_WEB_SERVER_H
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
|
||||||
|
/* Blocking, single-threaded local HTTP adapter. Port zero is reserved for
|
||||||
|
* controlled test harnesses and is rejected by the CLI. The database remains
|
||||||
|
* borrowed and process-owned; this infrastructure route never reads it. */
|
||||||
|
int trainlog_web_server_run(TrainlogDatabase *database, uint16_t port,
|
||||||
|
const volatile sig_atomic_t *stop_requested,
|
||||||
|
char *diagnostic, size_t diagnostic_capacity);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -6,6 +6,7 @@ udev_dep = dependency('libudev', required: true)
|
||||||
mtp_dep = dependency('libmtp', required: true)
|
mtp_dep = dependency('libmtp', required: true)
|
||||||
|
|
||||||
notcurses_dep = dependency('notcurses-core', required: true)
|
notcurses_dep = dependency('notcurses-core', required: true)
|
||||||
|
microhttpd_dep = dependency('libmicrohttpd', required: true)
|
||||||
|
|
||||||
utf8proc_dep = dependency('libutf8proc', required: false)
|
utf8proc_dep = dependency('libutf8proc', required: false)
|
||||||
if not utf8proc_dep.found()
|
if not utf8proc_dep.found()
|
||||||
|
|
@ -83,6 +84,7 @@ strict_c_args = [
|
||||||
'-Wconversion',
|
'-Wconversion',
|
||||||
'-Wformat=2',
|
'-Wformat=2',
|
||||||
'-Wshadow',
|
'-Wshadow',
|
||||||
|
'-DTRAINLOG_VERSION="@0@"'.format(meson.project_version()),
|
||||||
]
|
]
|
||||||
|
|
||||||
trainlog_core_sources = files(
|
trainlog_core_sources = files(
|
||||||
|
|
@ -140,7 +142,9 @@ trainlog_core_dep = declare_dependency(
|
||||||
)
|
)
|
||||||
|
|
||||||
trainlog_tui_sources = files(
|
trainlog_tui_sources = files(
|
||||||
|
'src/cli.c',
|
||||||
'src/main.c',
|
'src/main.c',
|
||||||
|
'src/paths.c',
|
||||||
'src/app_shell.c',
|
'src/app_shell.c',
|
||||||
'src/presentation.c',
|
'src/presentation.c',
|
||||||
'src/chart.c',
|
'src/chart.c',
|
||||||
|
|
@ -148,6 +152,7 @@ trainlog_tui_sources = files(
|
||||||
'src/terminal.c',
|
'src/terminal.c',
|
||||||
'src/theme.c',
|
'src/theme.c',
|
||||||
'src/tui.c',
|
'src/tui.c',
|
||||||
|
'src/web_server.c',
|
||||||
)
|
)
|
||||||
|
|
||||||
trainlog_exe = executable(
|
trainlog_exe = executable(
|
||||||
|
|
@ -156,6 +161,7 @@ trainlog_exe = executable(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
trainlog_core_dep,
|
trainlog_core_dep,
|
||||||
notcurses_dep,
|
notcurses_dep,
|
||||||
|
microhttpd_dep,
|
||||||
],
|
],
|
||||||
c_args: strict_c_args,
|
c_args: strict_c_args,
|
||||||
install: true,
|
install: true,
|
||||||
|
|
@ -217,6 +223,32 @@ test_dashboard_header = executable(
|
||||||
)
|
)
|
||||||
test('dashboard_header', test_dashboard_header)
|
test('dashboard_header', test_dashboard_header)
|
||||||
|
|
||||||
|
test_cli = executable(
|
||||||
|
'test_cli',
|
||||||
|
['tests/test_cli.c', 'src/cli.c'],
|
||||||
|
include_directories: trainlog_include,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
test('cli', test_cli)
|
||||||
|
|
||||||
|
test_web_server = executable(
|
||||||
|
'test_web_server',
|
||||||
|
['tests/test_web_server.c', 'src/web_server.c'],
|
||||||
|
dependencies: [trainlog_core_dep, microhttpd_dep],
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
test('web_server', test_web_server, timeout: 30)
|
||||||
|
test('cli_help', trainlog_exe, args: ['--help'])
|
||||||
|
test('cli_version', trainlog_exe, args: ['--version'])
|
||||||
|
|
||||||
|
test_web_headers = executable(
|
||||||
|
'test_web_headers',
|
||||||
|
'tests/test_web_headers.c',
|
||||||
|
include_directories: trainlog_include,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
test('web_headers', test_web_headers)
|
||||||
|
|
||||||
test_training_knowledge = executable(
|
test_training_knowledge = executable(
|
||||||
'test_training_knowledge',
|
'test_training_knowledge',
|
||||||
'tests/test_training_knowledge.c',
|
'tests/test_training_knowledge.c',
|
||||||
|
|
|
||||||
118
tui/src/cli.c
Normal file
118
tui/src/cli.c
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include "trainlog/cli.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define TRAINLOG_WEB_DEFAULT_PORT 8080U
|
||||||
|
|
||||||
|
static void cli_diagnostic(char *output, size_t capacity, const char *message)
|
||||||
|
{
|
||||||
|
if (output != NULL && capacity > 0U)
|
||||||
|
(void)snprintf(output, capacity, "%s", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_port(const char *value, uint16_t *port)
|
||||||
|
{
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long parsed;
|
||||||
|
if (value == NULL || value[0] == '\0' || value[0] == '-') return -1;
|
||||||
|
errno = 0;
|
||||||
|
parsed = strtoul(value, &end, 10);
|
||||||
|
if (errno != 0 || end == value || *end != '\0' || parsed == 0UL ||
|
||||||
|
parsed > 65535UL) return -1;
|
||||||
|
*port = (uint16_t)parsed;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int trainlog_cli_parse(int argc, char *const argv[], TrainlogCliOptions *options,
|
||||||
|
char *diagnostic, size_t diagnostic_capacity)
|
||||||
|
{
|
||||||
|
static const struct option long_options[] = {
|
||||||
|
{"web", no_argument, NULL, 'w'},
|
||||||
|
{"port", required_argument, NULL, 1},
|
||||||
|
{"help", no_argument, NULL, 'h'},
|
||||||
|
{"version", no_argument, NULL, 'V'},
|
||||||
|
{NULL, 0, NULL, 0}
|
||||||
|
};
|
||||||
|
bool web_seen = false;
|
||||||
|
bool port_seen = false;
|
||||||
|
bool help_seen = false;
|
||||||
|
bool version_seen = false;
|
||||||
|
int option;
|
||||||
|
if (options == NULL || argc < 1 || argv == NULL) return -1;
|
||||||
|
options->mode = TRAINLOG_RUN_TUI;
|
||||||
|
options->port = TRAINLOG_WEB_DEFAULT_PORT;
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity, "");
|
||||||
|
opterr = 0;
|
||||||
|
optind = 0;
|
||||||
|
while ((option = getopt_long(argc, argv, "whV", long_options, NULL)) != -1) {
|
||||||
|
switch (option) {
|
||||||
|
case 'w':
|
||||||
|
if (web_seen) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"l'option Web est répétée");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
web_seen = true;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (port_seen) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"l'option --port est répétée");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (parse_port(optarg, &options->port) != 0) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"--port attend un entier compris entre 1 et 65535");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
port_seen = true;
|
||||||
|
break;
|
||||||
|
case 'h': help_seen = true; break;
|
||||||
|
case 'V': version_seen = true; break;
|
||||||
|
case '?':
|
||||||
|
default:
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"option inconnue ou argument manquant");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optind != argc) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"aucun argument positionnel n'est accepté");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if ((help_seen && version_seen) ||
|
||||||
|
((help_seen || version_seen) && (web_seen || port_seen))) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"--help et --version doivent être utilisés seuls");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (port_seen && !web_seen) {
|
||||||
|
cli_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"--port n'est valide qu'avec -w ou --web");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (help_seen) options->mode = TRAINLOG_RUN_HELP;
|
||||||
|
else if (version_seen) options->mode = TRAINLOG_RUN_VERSION;
|
||||||
|
else if (web_seen) options->mode = TRAINLOG_RUN_WEB;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void trainlog_cli_print_usage(const char *program_name)
|
||||||
|
{
|
||||||
|
const char *name = program_name != NULL && program_name[0] != '\0'
|
||||||
|
? program_name : "trainlog";
|
||||||
|
(void)printf(
|
||||||
|
"Usage : %s [--help | --version | -w|--web [--port PORT]]\n"
|
||||||
|
" sans option lancer la TUI\n"
|
||||||
|
" -w, --web lancer le serveur Web local\n"
|
||||||
|
" --port PORT port Web explicite (1..65535, défaut 8080)\n"
|
||||||
|
" --help afficher cette aide\n"
|
||||||
|
" --version afficher la version\n", name);
|
||||||
|
}
|
||||||
174
tui/src/main.c
174
tui/src/main.c
|
|
@ -1,118 +1,112 @@
|
||||||
/**
|
/** @file main.c @brief Trainlog process owner and interface dispatcher. */
|
||||||
* @file main.c
|
|
||||||
* @brief Trainlog TUI process entry point.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "trainlog/cli.h"
|
||||||
#include "trainlog/database.h"
|
#include "trainlog/database.h"
|
||||||
|
#include "trainlog/paths.h"
|
||||||
#include "trainlog/tui.h"
|
#include "trainlog/tui.h"
|
||||||
|
#include "trainlog/web_server.h"
|
||||||
|
|
||||||
static int ensure_directory(const char *path)
|
static volatile sig_atomic_t web_stop_requested;
|
||||||
|
|
||||||
|
static void request_web_stop(int signal_number)
|
||||||
{
|
{
|
||||||
if (mkdir(path, 0700) == 0 || errno == EEXIST) {
|
(void)signal_number;
|
||||||
|
web_stop_requested = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int run_web(TrainlogDatabase *database, uint16_t port)
|
||||||
|
{
|
||||||
|
struct sigaction action;
|
||||||
|
struct sigaction previous_interrupt;
|
||||||
|
struct sigaction previous_terminate;
|
||||||
|
char diagnostic[256];
|
||||||
|
int result;
|
||||||
|
(void)memset(&action, 0, sizeof(action));
|
||||||
|
action.sa_handler = request_web_stop;
|
||||||
|
(void)sigemptyset(&action.sa_mask);
|
||||||
|
web_stop_requested = 0;
|
||||||
|
if (sigaction(SIGINT, &action, &previous_interrupt) != 0) {
|
||||||
|
(void)fprintf(stderr,
|
||||||
|
"trainlog: impossible d'installer les gestionnaires de signaux\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (sigaction(SIGTERM, &action, &previous_terminate) != 0) {
|
||||||
|
(void)sigaction(SIGINT, &previous_interrupt, NULL);
|
||||||
|
(void)fprintf(stderr,
|
||||||
|
"trainlog: impossible d'installer les gestionnaires de signaux\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
result = trainlog_web_server_run(database, port, &web_stop_requested,
|
||||||
|
diagnostic, sizeof(diagnostic));
|
||||||
|
(void)sigaction(SIGINT, &previous_interrupt, NULL);
|
||||||
|
(void)sigaction(SIGTERM, &previous_terminate, NULL);
|
||||||
|
if (result != 0) {
|
||||||
|
(void)fprintf(stderr, "trainlog: serveur Web : %s\n",
|
||||||
|
diagnostic[0] == '\0' ? "échec non détaillé" : diagnostic);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int build_database_path(char *output, size_t output_size)
|
int main(int argc, char *argv[])
|
||||||
{
|
|
||||||
const char *xdg = getenv("XDG_DATA_HOME");
|
|
||||||
const char *home = getenv("HOME");
|
|
||||||
char base[PATH_MAX];
|
|
||||||
char trainlog_dir[PATH_MAX];
|
|
||||||
int written;
|
|
||||||
|
|
||||||
if (xdg != NULL && xdg[0] != '\0') {
|
|
||||||
written = snprintf(base, sizeof(base), "%s", xdg);
|
|
||||||
} else if (home != NULL && home[0] != '\0') {
|
|
||||||
written = snprintf(base, sizeof(base), "%s/.local/share", home);
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (written < 0 || (size_t)written >= sizeof(base)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Creating ~/.local and ~/.local/share recursively would normally require
|
|
||||||
* a generic mkdir -p helper. On Arch these parents already exist for a
|
|
||||||
* desktop user; XDG_DATA_HOME custom paths are likewise expected to have
|
|
||||||
* their parent created by the user.
|
|
||||||
*/
|
|
||||||
if (ensure_directory(base) != 0 && errno != EEXIST) {
|
|
||||||
/* Continue only when the base already exists. */
|
|
||||||
struct stat info;
|
|
||||||
if (stat(base, &info) != 0 || !S_ISDIR(info.st_mode)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
written = snprintf(
|
|
||||||
trainlog_dir,
|
|
||||||
sizeof(trainlog_dir),
|
|
||||||
"%s/trainlog",
|
|
||||||
base
|
|
||||||
);
|
|
||||||
if (written < 0 || (size_t)written >= sizeof(trainlog_dir)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ensure_directory(trainlog_dir) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
written = snprintf(
|
|
||||||
output,
|
|
||||||
output_size,
|
|
||||||
"%s/trainlog.db",
|
|
||||||
trainlog_dir
|
|
||||||
);
|
|
||||||
|
|
||||||
return written >= 0 && (size_t)written < output_size ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
{
|
||||||
char database_path[PATH_MAX];
|
char database_path[PATH_MAX];
|
||||||
|
char path_diagnostic[256];
|
||||||
char database_diagnostic[256];
|
char database_diagnostic[256];
|
||||||
|
char cli_diagnostic[256];
|
||||||
|
TrainlogCliOptions options;
|
||||||
TrainlogDatabase *database = NULL;
|
TrainlogDatabase *database = NULL;
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
int result;
|
int result;
|
||||||
|
|
||||||
if (build_database_path(database_path, sizeof(database_path)) != 0) {
|
if (trainlog_cli_parse(argc, argv, &options, cli_diagnostic,
|
||||||
(void)fprintf(stderr, "trainlog: unable to create data directory\n");
|
sizeof(cli_diagnostic)) != 0) {
|
||||||
|
(void)fprintf(stderr, "trainlog: %s\n", cli_diagnostic);
|
||||||
|
trainlog_cli_print_usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (options.mode == TRAINLOG_RUN_HELP) {
|
||||||
|
trainlog_cli_print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (options.mode == TRAINLOG_RUN_VERSION) {
|
||||||
|
(void)printf("trainlog %s\n", TRAINLOG_VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* CONTRACT: newly created data directories and SQLite files are private
|
||||||
|
* to the local user, independently of the invoking shell's umask. */
|
||||||
|
(void)umask(0077);
|
||||||
|
if (trainlog_paths_database(database_path, sizeof(database_path),
|
||||||
|
path_diagnostic, sizeof(path_diagnostic)) != 0) {
|
||||||
|
(void)fprintf(stderr, "trainlog: chemin de données : %s\n",
|
||||||
|
path_diagnostic);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
status = trainlog_database_open_with_diagnostic(database_path, &database,
|
||||||
status = trainlog_database_open_with_diagnostic(
|
database_diagnostic, sizeof(database_diagnostic));
|
||||||
database_path,
|
|
||||||
&database,
|
|
||||||
database_diagnostic,
|
|
||||||
sizeof(database_diagnostic)
|
|
||||||
);
|
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
if (status != TRAINLOG_STATUS_OK) {
|
||||||
(void)fprintf(
|
(void)fprintf(stderr,
|
||||||
stderr,
|
"trainlog: impossible d'ouvrir la base '%s' (statut %d) : %s\n",
|
||||||
"trainlog: unable to open database '%s' (status %d): %s\n",
|
database_path, (int)status,
|
||||||
database_path,
|
database_diagnostic[0] == '\0'
|
||||||
(int)status,
|
? "diagnostic SQLite indisponible" : database_diagnostic);
|
||||||
database_diagnostic[0] != '\0'
|
|
||||||
? database_diagnostic
|
|
||||||
: "no SQLite diagnostic available"
|
|
||||||
);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (chmod(database_path, 0600) != 0) {
|
||||||
result = trainlog_tui_run(database);
|
(void)fprintf(stderr,
|
||||||
|
"trainlog: impossible de protéger les permissions de la base\n");
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
result = options.mode == TRAINLOG_RUN_WEB
|
||||||
|
? run_web(database, options.port)
|
||||||
|
: trainlog_tui_run(database);
|
||||||
trainlog_database_close(database);
|
trainlog_database_close(database);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
tui/src/paths.c
Normal file
74
tui/src/paths.c
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
#include "trainlog/paths.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
static void path_diagnostic(char *output, size_t capacity, const char *message)
|
||||||
|
{
|
||||||
|
if (output != NULL && capacity > 0U)
|
||||||
|
(void)snprintf(output, capacity, "%s", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ensure_private_directory(const char *path)
|
||||||
|
{
|
||||||
|
struct stat info;
|
||||||
|
if (mkdir(path, 0700) == 0) return 0;
|
||||||
|
if (errno != EEXIST || stat(path, &info) != 0 || !S_ISDIR(info.st_mode))
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int trainlog_paths_database(char *output, size_t output_capacity,
|
||||||
|
char *diagnostic, size_t diagnostic_capacity)
|
||||||
|
{
|
||||||
|
const char *xdg = getenv("XDG_DATA_HOME");
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
char base[PATH_MAX];
|
||||||
|
char trainlog_dir[PATH_MAX];
|
||||||
|
int written;
|
||||||
|
if (output == NULL || output_capacity == 0U) return -1;
|
||||||
|
output[0] = '\0';
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity, "");
|
||||||
|
if (xdg != NULL && xdg[0] != '\0')
|
||||||
|
written = snprintf(base, sizeof(base), "%s", xdg);
|
||||||
|
else if (home != NULL && home[0] != '\0')
|
||||||
|
written = snprintf(base, sizeof(base), "%s/.local/share", home);
|
||||||
|
else {
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"XDG_DATA_HOME et HOME sont absents");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (written < 0 || (size_t)written >= sizeof(base)) {
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"chemin de données trop long");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (ensure_private_directory(base) != 0) {
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"répertoire de données inaccessible");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
written = snprintf(trainlog_dir, sizeof(trainlog_dir), "%s/trainlog", base);
|
||||||
|
if (written < 0 || (size_t)written >= sizeof(trainlog_dir) ||
|
||||||
|
ensure_private_directory(trainlog_dir) != 0) {
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"répertoire Trainlog inaccessible ou trop long");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (chmod(trainlog_dir, 0700) != 0) {
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"permissions du répertoire Trainlog impossibles à garantir");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
written = snprintf(output, output_capacity, "%s/trainlog.db", trainlog_dir);
|
||||||
|
if (written < 0 || (size_t)written >= output_capacity) {
|
||||||
|
output[0] = '\0';
|
||||||
|
path_diagnostic(diagnostic, diagnostic_capacity,
|
||||||
|
"chemin de base de données trop long");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
249
tui/src/web_server.c
Normal file
249
tui/src/web_server.c
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#include "trainlog/web_server.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <microhttpd.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/select.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 (4U * 1024U)
|
||||||
|
#define TRAINLOG_HTTP_TIMEOUT_SECONDS 10U
|
||||||
|
#define TRAINLOG_HTTP_BACKLOG 32U
|
||||||
|
#define TRAINLOG_HTTP_POLL_MAX_MS 250ULL
|
||||||
|
|
||||||
|
typedef struct TrainlogWebContext {
|
||||||
|
uint16_t port;
|
||||||
|
} TrainlogWebContext;
|
||||||
|
|
||||||
|
typedef struct TrainlogHttpRequestState {
|
||||||
|
size_t body_size;
|
||||||
|
bool body_too_large;
|
||||||
|
} TrainlogHttpRequestState;
|
||||||
|
|
||||||
|
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_json(struct MHD_Connection *connection,
|
||||||
|
unsigned int status, const char *body)
|
||||||
|
{
|
||||||
|
struct MHD_Response *response;
|
||||||
|
enum MHD_Result result;
|
||||||
|
response = MHD_create_response_from_buffer(strlen(body), (void *)body,
|
||||||
|
MHD_RESPMEM_PERSISTENT);
|
||||||
|
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 (status == MHD_HTTP_METHOD_NOT_ALLOWED &&
|
||||||
|
MHD_add_response_header(response, MHD_HTTP_HEADER_ALLOW, "GET") !=
|
||||||
|
MHD_YES) {
|
||||||
|
MHD_destroy_response(response);
|
||||||
|
return MHD_NO;
|
||||||
|
}
|
||||||
|
result = MHD_queue_response(connection, status, response);
|
||||||
|
MHD_destroy_response(response);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 char *host;
|
||||||
|
(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 state->body_size += *upload_data_size;
|
||||||
|
(void)upload_data;
|
||||||
|
*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");
|
||||||
|
if (state->body_too_large)
|
||||||
|
return queue_json(connection, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||||
|
"{\"error\":\"body_too_large\"}\n");
|
||||||
|
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");
|
||||||
|
if (strcmp(url, "/api/v1/health") != 0)
|
||||||
|
return queue_json(connection, MHD_HTTP_NOT_FOUND,
|
||||||
|
"{\"error\":\"not_found\"}\n");
|
||||||
|
if (strcmp(method, MHD_HTTP_METHOD_GET) != 0)
|
||||||
|
return queue_json(connection, MHD_HTTP_METHOD_NOT_ALLOWED,
|
||||||
|
"{\"error\":\"method_not_allowed\"}\n");
|
||||||
|
return queue_json(connection, MHD_HTTP_OK, HEALTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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 || stop_requested == NULL) return -1;
|
||||||
|
web_diagnostic(diagnostic, diagnostic_capacity, "");
|
||||||
|
(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;
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
68
tui/tests/test_cli.c
Normal file
68
tui/tests/test_cli.c
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "trainlog/cli.h"
|
||||||
|
|
||||||
|
#define CHECK(value) do { if (!(value)) { \
|
||||||
|
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
|
||||||
|
__FILE__, __LINE__, #value); return false; } } while (0)
|
||||||
|
|
||||||
|
static bool parses(int argc, char **argv, TrainlogRunMode mode,
|
||||||
|
unsigned int port)
|
||||||
|
{
|
||||||
|
TrainlogCliOptions options;
|
||||||
|
char diagnostic[128];
|
||||||
|
CHECK(trainlog_cli_parse(argc, argv, &options, diagnostic,
|
||||||
|
sizeof(diagnostic)) == 0);
|
||||||
|
CHECK(diagnostic[0] == '\0');
|
||||||
|
CHECK(options.mode == mode);
|
||||||
|
CHECK((unsigned int)options.port == port);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rejects(int argc, char **argv)
|
||||||
|
{
|
||||||
|
TrainlogCliOptions options;
|
||||||
|
char diagnostic[128];
|
||||||
|
CHECK(trainlog_cli_parse(argc, argv, &options, diagnostic,
|
||||||
|
sizeof(diagnostic)) != 0);
|
||||||
|
CHECK(diagnostic[0] != '\0');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
char *plain[] = {"trainlog", NULL};
|
||||||
|
char *short_web[] = {"trainlog", "-w", NULL};
|
||||||
|
char *long_web[] = {"trainlog", "--web", NULL};
|
||||||
|
char *port[] = {"trainlog", "-w", "--port", "65535", NULL};
|
||||||
|
char *port_first[] = {"trainlog", "--port", "8081", "--web", NULL};
|
||||||
|
char *help[] = {"trainlog", "--help", NULL};
|
||||||
|
char *version[] = {"trainlog", "--version", NULL};
|
||||||
|
char *unknown[] = {"trainlog", "--unknown", NULL};
|
||||||
|
char *port_alone[] = {"trainlog", "--port", "8080", NULL};
|
||||||
|
char *port_missing[] = {"trainlog", "-w", "--port", NULL};
|
||||||
|
char *port_text[] = {"trainlog", "-w", "--port", "abc", NULL};
|
||||||
|
char *port_negative[] = {"trainlog", "-w", "--port", "-1", NULL};
|
||||||
|
char *port_zero[] = {"trainlog", "-w", "--port", "0", NULL};
|
||||||
|
char *port_large[] = {"trainlog", "-w", "--port", "65536", NULL};
|
||||||
|
char *duplicate_web[] = {"trainlog", "-w", "--web", NULL};
|
||||||
|
char *duplicate_port[] = {"trainlog", "-w", "--port", "80",
|
||||||
|
"--port", "81", NULL};
|
||||||
|
char *mixed_help[] = {"trainlog", "--help", "--web", NULL};
|
||||||
|
char *positional[] = {"trainlog", "extra", NULL};
|
||||||
|
return parses(1, plain, TRAINLOG_RUN_TUI, 8080U) &&
|
||||||
|
parses(2, short_web, TRAINLOG_RUN_WEB, 8080U) &&
|
||||||
|
parses(2, long_web, TRAINLOG_RUN_WEB, 8080U) &&
|
||||||
|
parses(4, port, TRAINLOG_RUN_WEB, 65535U) &&
|
||||||
|
parses(4, port_first, TRAINLOG_RUN_WEB, 8081U) &&
|
||||||
|
parses(2, help, TRAINLOG_RUN_HELP, 8080U) &&
|
||||||
|
parses(2, version, TRAINLOG_RUN_VERSION, 8080U) &&
|
||||||
|
rejects(2, unknown) && rejects(3, port_alone) &&
|
||||||
|
rejects(3, port_missing) && rejects(4, port_text) &&
|
||||||
|
rejects(4, port_negative) && rejects(4, port_zero) &&
|
||||||
|
rejects(4, port_large) && rejects(3, duplicate_web) &&
|
||||||
|
rejects(6, duplicate_port) && rejects(3, mixed_help) &&
|
||||||
|
rejects(2, positional) ? 0 : 1;
|
||||||
|
}
|
||||||
10
tui/tests/test_web_headers.c
Normal file
10
tui/tests/test_web_headers.c
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/* Standalone public-header compilation contract for the Web infrastructure. */
|
||||||
|
#include "trainlog/cli.h"
|
||||||
|
#include "trainlog/paths.h"
|
||||||
|
#include "trainlog/web_server.h"
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
TrainlogCliOptions options = {TRAINLOG_RUN_TUI, 8080U};
|
||||||
|
return options.mode == TRAINLOG_RUN_TUI && options.port == 8080U ? 0 : 1;
|
||||||
|
}
|
||||||
186
tui/tests/test_web_server.c
Normal file
186
tui/tests/test_web_server.c
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.h"
|
||||||
|
#include "trainlog/web_server.h"
|
||||||
|
|
||||||
|
#define CHECK(value) do { if (!(value)) { \
|
||||||
|
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
|
||||||
|
__FILE__, __LINE__, #value); return false; } } while (0)
|
||||||
|
|
||||||
|
static volatile sig_atomic_t child_stop;
|
||||||
|
|
||||||
|
static void stop_child(int signal_number)
|
||||||
|
{
|
||||||
|
(void)signal_number;
|
||||||
|
child_stop = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int reserve_port(uint16_t *port, int keep_open)
|
||||||
|
{
|
||||||
|
struct sockaddr_in address;
|
||||||
|
socklen_t length = (socklen_t)sizeof(address);
|
||||||
|
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (socket_fd < 0) return -1;
|
||||||
|
(void)memset(&address, 0, sizeof(address));
|
||||||
|
address.sin_family = AF_INET;
|
||||||
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
address.sin_port = 0;
|
||||||
|
if (bind(socket_fd, (const struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||||
|
getsockname(socket_fd, (struct sockaddr *)&address, &length) != 0 ||
|
||||||
|
(keep_open != 0 && listen(socket_fd, 1) != 0)) {
|
||||||
|
(void)close(socket_fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*port = ntohs(address.sin_port);
|
||||||
|
if (keep_open == 0) {
|
||||||
|
(void)close(socket_fd);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return socket_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int connect_with_retry(uint16_t port)
|
||||||
|
{
|
||||||
|
struct sockaddr_in address;
|
||||||
|
struct timespec delay = {0, 10000000L};
|
||||||
|
size_t attempt;
|
||||||
|
(void)memset(&address, 0, sizeof(address));
|
||||||
|
address.sin_family = AF_INET;
|
||||||
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
address.sin_port = htons(port);
|
||||||
|
for (attempt = 0U; attempt < 200U; ++attempt) {
|
||||||
|
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (socket_fd >= 0 && connect(socket_fd,
|
||||||
|
(const struct sockaddr *)&address, sizeof(address)) == 0)
|
||||||
|
return socket_fd;
|
||||||
|
if (socket_fd >= 0) (void)close(socket_fd);
|
||||||
|
(void)nanosleep(&delay, NULL);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool exchange(uint16_t port, const char *request, char *response,
|
||||||
|
size_t capacity)
|
||||||
|
{
|
||||||
|
size_t sent = 0U;
|
||||||
|
size_t used = 0U;
|
||||||
|
size_t length = strlen(request);
|
||||||
|
int socket_fd = connect_with_retry(port);
|
||||||
|
CHECK(socket_fd >= 0);
|
||||||
|
while (sent < length) {
|
||||||
|
ssize_t count = send(socket_fd, request + sent, length - sent, 0);
|
||||||
|
CHECK(count > 0);
|
||||||
|
sent += (size_t)count;
|
||||||
|
}
|
||||||
|
(void)shutdown(socket_fd, SHUT_WR);
|
||||||
|
while (used + 1U < capacity) {
|
||||||
|
ssize_t count = recv(socket_fd, response + used, capacity - used - 1U, 0);
|
||||||
|
if (count == 0) break;
|
||||||
|
CHECK(count > 0);
|
||||||
|
used += (size_t)count;
|
||||||
|
}
|
||||||
|
response[used] = '\0';
|
||||||
|
(void)close(socket_fd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_http_contract(TrainlogDatabase *database)
|
||||||
|
{
|
||||||
|
char response[32768];
|
||||||
|
char large_request[12000];
|
||||||
|
uint16_t port;
|
||||||
|
pid_t child;
|
||||||
|
int status;
|
||||||
|
struct sigaction action;
|
||||||
|
CHECK(reserve_port(&port, 0) == -2);
|
||||||
|
child = fork();
|
||||||
|
CHECK(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
char diagnostic[256];
|
||||||
|
(void)memset(&action, 0, sizeof(action));
|
||||||
|
action.sa_handler = stop_child;
|
||||||
|
(void)sigemptyset(&action.sa_mask);
|
||||||
|
(void)sigaction(SIGTERM, &action, NULL);
|
||||||
|
child_stop = 0;
|
||||||
|
_exit(trainlog_web_server_run(database, port, &child_stop,
|
||||||
|
diagnostic, sizeof(diagnostic)) == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
CHECK(exchange(port,
|
||||||
|
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||||
|
response, sizeof(response)));
|
||||||
|
CHECK(strstr(response, "HTTP/1.1 200") != NULL);
|
||||||
|
CHECK(strstr(response, "Content-Type: application/json; charset=utf-8") != NULL);
|
||||||
|
CHECK(strstr(response, "X-Content-Type-Options: nosniff") != NULL);
|
||||||
|
CHECK(strstr(response, "Content-Security-Policy: default-src 'none'") != NULL);
|
||||||
|
CHECK(strstr(response, "Access-Control-Allow-Origin") == NULL);
|
||||||
|
CHECK(strstr(response,
|
||||||
|
"{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
|
||||||
|
"\"version\":\"0.1.2\"}") != NULL);
|
||||||
|
CHECK(exchange(port, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||||
|
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL);
|
||||||
|
CHECK(exchange(port,
|
||||||
|
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
|
||||||
|
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL);
|
||||||
|
CHECK(strstr(response, "Allow: GET") != NULL);
|
||||||
|
CHECK(exchange(port,
|
||||||
|
"GET /api/v1/health HTTP/1.1\r\nHost: foreign.example\r\n\r\n",
|
||||||
|
response, sizeof(response)) && strstr(response, "HTTP/1.1 400") != NULL);
|
||||||
|
(void)snprintf(large_request, sizeof(large_request),
|
||||||
|
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||||
|
"Content-Length: 5000\r\n\r\n%05000d", 0);
|
||||||
|
CHECK(exchange(port, large_request, response, sizeof(response)) &&
|
||||||
|
strstr(response, "HTTP/1.1 413") != NULL);
|
||||||
|
(void)memset(large_request, 'a', sizeof(large_request));
|
||||||
|
(void)snprintf(large_request, 128U,
|
||||||
|
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Large: ");
|
||||||
|
{
|
||||||
|
size_t prefix = strlen(large_request);
|
||||||
|
(void)memset(large_request + prefix, 'a', 8500U);
|
||||||
|
(void)memcpy(large_request + prefix + 8500U, "\r\n\r\n", 5U);
|
||||||
|
}
|
||||||
|
CHECK(exchange(port, large_request, response, sizeof(response)) &&
|
||||||
|
(strstr(response, "HTTP/1.1 431") != NULL ||
|
||||||
|
strstr(response, "HTTP/1.1 400") != NULL));
|
||||||
|
CHECK(kill(child, SIGTERM) == 0);
|
||||||
|
CHECK(waitpid(child, &status, 0) == child);
|
||||||
|
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_port_in_use(TrainlogDatabase *database)
|
||||||
|
{
|
||||||
|
volatile sig_atomic_t stop = 0;
|
||||||
|
char diagnostic[256];
|
||||||
|
uint16_t port;
|
||||||
|
int occupied = reserve_port(&port, 1);
|
||||||
|
CHECK(occupied >= 0);
|
||||||
|
CHECK(trainlog_web_server_run(database, port, &stop, diagnostic,
|
||||||
|
sizeof(diagnostic)) != 0);
|
||||||
|
CHECK(strstr(diagnostic, "127.0.0.1") != NULL);
|
||||||
|
(void)close(occupied);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
bool passed;
|
||||||
|
if (trainlog_database_open(":memory:", &database) != TRAINLOG_STATUS_OK)
|
||||||
|
return 1;
|
||||||
|
passed = test_http_contract(database) && test_port_in_use(database);
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return passed ? 0 : 1;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue