P4.1: distd — read-only HTTP API + downloads over the local store
dist server run binds 0.0.0.0:<port> (default 8787) and serves /healthz, /api/apps, /api/apps/<slug>, and /download/<sha256> (X-Checksum-SHA256, bytes verified content-identical). db.json reloads per request so CLI publishes/promotes are visible immediately. Errors reuse the CLI's JSON error shape with matching HTTP statuses. HTTP/1.1 is an in-repo shim over std.socket (src/server/http.sx), liftable to the sx stdlib later. Response buffers are heap slices: a 64K+ stack array in one frame segfaults the sx LLVM backend (DAGCombiner); 4-16K stack buffers are fine. Pinned in tests/server_http.sx including a freshness case.
This commit is contained in:
52
src/dist.sx
52
src/dist.sx
@@ -7,6 +7,7 @@
|
||||
// dist ci publish local publish pipeline (P3.4a/b) — see publish.sx
|
||||
// dist release promote point a channel at a release (P3.5) — see release/ops.sx
|
||||
// dist release rollback channel pointer to the previous release (P3.5)
|
||||
// dist server run read-only HTTP API over the store (P4.1) — see server/distd.sx
|
||||
//
|
||||
// EXIT-CODE CONTRACT (sysexits, via std.cli): success ends with
|
||||
// `exit_ok()` (EX_OK = 0); a no-command / unknown-or-missing
|
||||
@@ -29,6 +30,7 @@
|
||||
jout :: #import "json_out.sx";
|
||||
pl :: #import "publish/publish.sx";
|
||||
ops :: #import "release/ops.sx";
|
||||
srv :: #import "server/distd.sx";
|
||||
|
||||
// Direct stderr writer (fd 2), so human help/usage/progress never lands on
|
||||
// stdout's data stream. `out` (std builtin) targets stdout (fd 1).
|
||||
@@ -43,7 +45,7 @@ emit_human :: (s: string, json_mode: bool) {
|
||||
if json_mode { eputs(s); } else { out(s); }
|
||||
}
|
||||
|
||||
HELP :: "dist — application distribution CLI\n\nUsage:\n dist <group> <command> [flags]\n\nGroups & commands:\n ci\n ci publish publish a release from CI\n --manifest <path> publish manifest (dist.json) to read\n --local-store <dir> local artifact store + db.json directory\n release\n release promote point a channel at a release\n --app <slug> app the channel belongs to\n --channel <name> channel to move\n --release <id> release id to promote\n --local-store <dir> local artifact store + db.json directory\n release rollback move a channel back to its previous release\n --app <slug> app the channel belongs to\n --channel <name> channel to roll back\n --local-store <dir> local artifact store + db.json directory\n\nGlobal flags:\n --json emit machine-readable JSON on stdout; human text to stderr\n -h, --help show this help and exit\n\nExit codes:\n 0 success\n 1 command failed (publish/promote/rollback aborted; JSON error under --json)\n 64 usage error (no command, or an unknown/missing command or flag)\n";
|
||||
HELP :: "dist — application distribution CLI\n\nUsage:\n dist <group> <command> [flags]\n\nGroups & commands:\n ci\n ci publish publish a release from CI\n --manifest <path> publish manifest (dist.json) to read\n --local-store <dir> local artifact store + db.json directory\n release\n release promote point a channel at a release\n --app <slug> app the channel belongs to\n --channel <name> channel to move\n --release <id> release id to promote\n --local-store <dir> local artifact store + db.json directory\n release rollback move a channel back to its previous release\n --app <slug> app the channel belongs to\n --channel <name> channel to roll back\n --local-store <dir> local artifact store + db.json directory\n server\n server run serve the store read-only over HTTP (0.0.0.0)\n --local-store <dir> local artifact store + db.json directory\n --port <n> TCP port (default 8787)\n routes: /healthz, /api/apps, /api/apps/<slug>, /download/<sha256>\n\nGlobal flags:\n --json emit machine-readable JSON on stdout; human text to stderr\n -h, --help show this help and exit\n\nExit codes:\n 0 success\n 1 command failed (publish/promote/rollback aborted or server could not bind)\n 64 usage error (no command, or an unknown/missing command or flag)\n";
|
||||
|
||||
// True if `name` appears as a token in `args`.
|
||||
has_flag :: (args: []string, name: string) -> bool {
|
||||
@@ -180,12 +182,55 @@ handle_release_rollback :: (p: *Parsed, json_mode: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// `dist server run` — bind 0.0.0.0:<port> and serve the store read-only
|
||||
// (P4.1). Loops forever on success; a bind/listen failure reports through
|
||||
// `report_failure` like every other command.
|
||||
handle_server_run :: (p: *Parsed, json_mode: bool) {
|
||||
store_dir := p.value_of("local-store");
|
||||
port : s64 = 8787;
|
||||
if p.is_set("port") {
|
||||
pq := parse_port(p.value_of("port"));
|
||||
if pq == null {
|
||||
eputs(concat(concat("dist: --port must be 1..65535, got: ", p.value_of("port")), "\n"));
|
||||
exit_usage();
|
||||
}
|
||||
port = pq!;
|
||||
}
|
||||
|
||||
failed := false;
|
||||
srv.run_server(store_dir, port) catch { failed = true; };
|
||||
if failed {
|
||||
fail : jout.CliFailure = .{
|
||||
code = "server.bind",
|
||||
message = concat("could not bind/listen on 0.0.0.0:", int_to_string(port)),
|
||||
};
|
||||
report_failure("server run", fail, json_mode);
|
||||
}
|
||||
}
|
||||
|
||||
// Decimal port in 1..65535; anything else (empty, non-digits, out of
|
||||
// range) is null — a usage error at the call site.
|
||||
parse_port :: (s: string) -> ?s64 {
|
||||
if s.len == 0 or s.len > 5 { return null; }
|
||||
v : s64 = 0;
|
||||
i := 0;
|
||||
while i < s.len {
|
||||
c := s[i];
|
||||
if c < 48 or c > 57 { return null; } // '0'..'9'
|
||||
v = v * 10 + (c - 48);
|
||||
i += 1;
|
||||
}
|
||||
if v < 1 or v > 65535 { return null; }
|
||||
return v;
|
||||
}
|
||||
|
||||
// Route a parsed (group, command) to its handler. `parse` only returns a
|
||||
// (group, command) present in the table, so one arm always matches.
|
||||
dispatch :: (p: *Parsed, json_mode: bool) {
|
||||
if p.group == "ci" and p.command == "publish" { handle_ci_publish(p, json_mode); return; }
|
||||
if p.group == "release" and p.command == "promote" { handle_release_promote(p, json_mode); return; }
|
||||
if p.group == "release" and p.command == "rollback" { handle_release_rollback(p, json_mode); return; }
|
||||
if p.group == "server" and p.command == "run" { handle_server_run(p, json_mode); return; }
|
||||
eputs("dist: internal error: unrouted command\n");
|
||||
exit_usage();
|
||||
}
|
||||
@@ -231,10 +276,15 @@ main :: () -> ! {
|
||||
FlagSpec.{ name = "channel", takes_value = true, required = true },
|
||||
FlagSpec.{ name = "local-store", takes_value = true, required = true },
|
||||
];
|
||||
server_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "local-store", takes_value = true, required = true },
|
||||
FlagSpec.{ name = "port", takes_value = true, required = false },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
Command.{ group = "release", command = "promote", flags = promote_flags },
|
||||
Command.{ group = "release", command = "rollback", flags = rollback_flags },
|
||||
Command.{ group = "server", command = "run", flags = server_flags },
|
||||
];
|
||||
|
||||
diag : Diag = .{};
|
||||
|
||||
303
src/server/distd.sx
Normal file
303
src/server/distd.sx
Normal file
@@ -0,0 +1,303 @@
|
||||
// =====================================================================
|
||||
// distd.sx — the read-only distribution server over the local store
|
||||
// (subplan 04, Slices 1 + the read half of 3/4), run as `dist server run`.
|
||||
//
|
||||
// Serves the state the CLI publishes — db.json metadata and the
|
||||
// content-addressed objects — over HTTP (src/server/http.sx):
|
||||
//
|
||||
// GET /healthz {"status":"ok"} — no store access
|
||||
// GET /api/apps {"apps":[...]} every app in the store
|
||||
// GET /api/apps/<slug> {"app":..,"releases":[..],"channels":[..]}
|
||||
// GET /download/<sha256> the object's bytes (application/octet-stream,
|
||||
// X-Checksum-SHA256 header)
|
||||
//
|
||||
// Anything else is a JSON error in the CLI's error shape
|
||||
// (`{"status":"error","error":{code,message}}`) with the matching HTTP
|
||||
// status — the API and the CLI report failures identically.
|
||||
//
|
||||
// FRESHNESS: db.json is RELOADED on every /api request, so a `dist ci
|
||||
// publish` / `release promote` between requests is visible immediately —
|
||||
// the store on disk stays the single source of truth (no cache to
|
||||
// invalidate, LAN-scale traffic).
|
||||
//
|
||||
// MUTATION is the CLI's job for now: every route is GET; writes arrive
|
||||
// with token auth (subplan 04 Slice 2, deferred with the rest of the
|
||||
// upload/auth surface).
|
||||
//
|
||||
// RESPONSE BUFFERS are heap slices from the per-request arena, never big
|
||||
// stack arrays: a stack array of 64K+ in one frame crashes the sx LLVM
|
||||
// backend (DAGCombiner segfault). Small fixed buffers (4K) are fine.
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/json.sx";
|
||||
#import "modules/std/fs.sx";
|
||||
#import "../domain/platform.sx";
|
||||
#import "../domain/app.sx";
|
||||
#import "../domain/release.sx";
|
||||
#import "../domain/artifact.sx";
|
||||
#import "../domain/channel.sx";
|
||||
#import "../domain/audit.sx";
|
||||
#import "../repo/repo.sx";
|
||||
sock :: #import "modules/std/socket.sx";
|
||||
http :: #import "http.sx";
|
||||
db :: #import "../repo/db.sx";
|
||||
jout :: #import "../json_out.sx";
|
||||
|
||||
// Response-body capacity for the /api JSON renders (heap, per-request).
|
||||
RENDER_CAP :: 262144;
|
||||
|
||||
ServeErr :: error {
|
||||
Bind,
|
||||
}
|
||||
|
||||
// Server-side stderr log line (fd 2 via the socket module's write; stdout
|
||||
// stays clean for whoever launched the process).
|
||||
slog :: (s: string) {
|
||||
if s.len > 0 { sock.write(2, s.ptr, xx s.len); }
|
||||
}
|
||||
|
||||
// True iff `s` is exactly 64 lowercase-hex chars (a storage key).
|
||||
is_hex64 :: (s: string) -> bool {
|
||||
if s.len != 64 { return false; }
|
||||
i := 0;
|
||||
while i < s.len {
|
||||
c := s[i];
|
||||
digit := c >= 48 and c <= 57; // '0'..'9'
|
||||
lower := c >= 97 and c <= 102; // 'a'..'f'
|
||||
if !digit and !lower { return false; }
|
||||
i += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
starts_with :: (s: string, prefix: string) -> bool {
|
||||
if prefix.len > s.len { return false; }
|
||||
i := 0;
|
||||
while i < prefix.len {
|
||||
if s[i] != prefix[i] { return false; }
|
||||
i += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The path remainder after `prefix` (caller has checked starts_with).
|
||||
tail_after :: (s: string, prefix: string) -> string {
|
||||
return string.{ ptr = @s[prefix.len], len = s.len - prefix.len };
|
||||
}
|
||||
|
||||
// A `RENDER_CAP` heap slice from the per-request context allocator.
|
||||
render_buf :: () -> string {
|
||||
raw : [*]u8 = xx context.allocator.alloc_bytes(RENDER_CAP);
|
||||
return string.{ ptr = raw, len = RENDER_CAP };
|
||||
}
|
||||
|
||||
// ── responses ─────────────────────────────────────────────────────────
|
||||
|
||||
// JSON error body in the CLI's error shape, sent with the HTTP status.
|
||||
respond_error :: (client: s32, code: s64, fail_code: string, fail_message: string) {
|
||||
f : jout.CliFailure = .{ code = fail_code, message = fail_message };
|
||||
raw : [4096]u8 = ---;
|
||||
werr := false;
|
||||
n := jout.write_error(f, string.{ ptr = @raw[0], len = 4096 }) catch { werr = true; 0 };
|
||||
body := "{\"status\":\"error\"}";
|
||||
if !werr { body = string.{ ptr = @raw[0], len = n }; }
|
||||
http.respond(client, code, "application/json", "", body);
|
||||
}
|
||||
|
||||
// ── /api renders (builders own the `try`, callers catch) ─────────────
|
||||
|
||||
// Reload the persisted model. Null means the store has no readable
|
||||
// db.json — the 503 error response has already been sent.
|
||||
load_or_503 :: (client: s32, store_dir: string) -> ?Repo {
|
||||
if !exists(path_join(store_dir, "db.json")) {
|
||||
respond_error(client, 503, "store.load",
|
||||
concat("no db.json under the store (nothing published yet): ", store_dir));
|
||||
return null;
|
||||
}
|
||||
loaded, le := db.load(store_dir);
|
||||
if le {
|
||||
respond_error(client, 503, "store.load",
|
||||
concat("db.json under the store could not be loaded: ", store_dir));
|
||||
return null;
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
// `{"apps":[...]}` into `dst`, returning the bytes written.
|
||||
render_apps_json :: (repo: *Repo, dst: []u8) -> (s64, !JsonError) {
|
||||
alloc := context.allocator;
|
||||
root : Object = .{};
|
||||
arr : Array = .{};
|
||||
i := 0;
|
||||
while i < repo.apps.len {
|
||||
arr.add(db.app_to_json(repo.apps.items[i], alloc), alloc);
|
||||
i += 1;
|
||||
}
|
||||
root.put("apps", .array(arr), alloc);
|
||||
rootv : Value = .object(root);
|
||||
n := try write_to_buffer(rootv, dst);
|
||||
return n;
|
||||
}
|
||||
|
||||
// `{"app":..,"releases":[..],"channels":[..]}` for `app` into `dst`.
|
||||
render_app_detail_json :: (repo: *Repo, app: App, dst: []u8) -> (s64, !JsonError) {
|
||||
alloc := context.allocator;
|
||||
root : Object = .{};
|
||||
root.put("app", db.app_to_json(app, alloc), alloc);
|
||||
|
||||
rels : Array = .{};
|
||||
i := 0;
|
||||
while i < repo.releases.len {
|
||||
r := repo.releases.items[i];
|
||||
if r.app_id == app.id { rels.add(db.release_to_json(r, alloc), alloc); }
|
||||
i += 1;
|
||||
}
|
||||
root.put("releases", .array(rels), alloc);
|
||||
|
||||
chans : Array = .{};
|
||||
i = 0;
|
||||
while i < repo.channels.len {
|
||||
c := repo.channels.items[i];
|
||||
if c.app_id == app.id { chans.add(db.channel_to_json(c, alloc), alloc); }
|
||||
i += 1;
|
||||
}
|
||||
root.put("channels", .array(chans), alloc);
|
||||
|
||||
rootv : Value = .object(root);
|
||||
n := try write_to_buffer(rootv, dst);
|
||||
return n;
|
||||
}
|
||||
|
||||
// Send `n` rendered bytes of `buf` as 200 JSON — or the overflow error
|
||||
// when the render didn't fit RENDER_CAP.
|
||||
respond_render :: (client: s32, buf: string, n: s64, overflowed: bool) {
|
||||
if overflowed {
|
||||
respond_error(client, 500, "http.response_overflow",
|
||||
"response exceeded the server's render buffer");
|
||||
return;
|
||||
}
|
||||
http.respond(client, 200, "application/json", "", string.{ ptr = buf.ptr, len = n });
|
||||
}
|
||||
|
||||
// ── routes ────────────────────────────────────────────────────────────
|
||||
|
||||
handle_apps_index :: (client: s32, store_dir: string) {
|
||||
rq := load_or_503(client, store_dir);
|
||||
if rq == null { return; }
|
||||
repo := rq!;
|
||||
|
||||
buf := render_buf();
|
||||
werr := false;
|
||||
n := render_apps_json(@repo, buf) catch { werr = true; 0 };
|
||||
respond_render(client, buf, n, werr);
|
||||
}
|
||||
|
||||
handle_app_detail :: (client: s32, store_dir: string, slug: string) {
|
||||
rq := load_or_503(client, store_dir);
|
||||
if rq == null { return; }
|
||||
repo := rq!;
|
||||
|
||||
aq := repo.find_app_by_slug(slug);
|
||||
if aq == null {
|
||||
respond_error(client, 404, "api.unknown_app",
|
||||
concat("no app with that slug in the store: ", slug));
|
||||
return;
|
||||
}
|
||||
|
||||
buf := render_buf();
|
||||
werr := false;
|
||||
n := render_app_detail_json(@repo, aq!, buf) catch { werr = true; 0 };
|
||||
respond_render(client, buf, n, werr);
|
||||
}
|
||||
|
||||
handle_download :: (client: s32, store_dir: string, sha: string) {
|
||||
if !is_hex64(sha) {
|
||||
respond_error(client, 404, "download.bad_key",
|
||||
"download key must be a 64-char lowercase-hex sha256");
|
||||
return;
|
||||
}
|
||||
opath := path_join(store_dir, concat("objects/", sha));
|
||||
bq := read_file(opath);
|
||||
if bq == null {
|
||||
respond_error(client, 404, "download.unknown_object",
|
||||
concat("no object with that digest in the store: ", sha));
|
||||
return;
|
||||
}
|
||||
extra := concat("X-Checksum-SHA256: ", concat(sha, "\r\n"));
|
||||
http.respond(client, 200, "application/octet-stream", extra, bq!);
|
||||
}
|
||||
|
||||
// Route one parsed request. GET only; the path decides the handler.
|
||||
route :: (client: s32, store_dir: string, method: string, path: string) -> s64 {
|
||||
if method != "GET" {
|
||||
respond_error(client, 405, "http.method_not_allowed",
|
||||
"every distd route is GET for now (writes go through the dist CLI)");
|
||||
return 405;
|
||||
}
|
||||
if path == "/healthz" {
|
||||
http.respond(client, 200, "application/json", "", "{\"status\":\"ok\"}");
|
||||
return 200;
|
||||
}
|
||||
if path == "/api/apps" {
|
||||
handle_apps_index(client, store_dir);
|
||||
return 200;
|
||||
}
|
||||
if starts_with(path, "/api/apps/") {
|
||||
handle_app_detail(client, store_dir, tail_after(path, "/api/apps/"));
|
||||
return 200;
|
||||
}
|
||||
if starts_with(path, "/download/") {
|
||||
handle_download(client, store_dir, tail_after(path, "/download/"));
|
||||
return 200;
|
||||
}
|
||||
respond_error(client, 404, "http.not_found",
|
||||
concat("no route for ", path));
|
||||
return 404;
|
||||
}
|
||||
|
||||
// ── server loop ───────────────────────────────────────────────────────
|
||||
|
||||
// Read one request off `client`, route it, log the result line. All
|
||||
// allocations land in the pushed per-request context allocator.
|
||||
serve_one :: (client: s32, store_dir: string) {
|
||||
buf : [8192]u8 = ---;
|
||||
n := sock.read(client, @buf[0], 8192);
|
||||
if n <= 0 { return; }
|
||||
|
||||
raw := string.{ ptr = @buf[0], len = xx n };
|
||||
req : http.Request = .{};
|
||||
if !http.parse_request(raw, @req) {
|
||||
respond_error(client, 400, "http.bad_request",
|
||||
"request line did not parse as HTTP");
|
||||
return;
|
||||
}
|
||||
code := route(client, store_dir, req.method, req.path);
|
||||
line := concat("distd: ", concat(req.method, concat(" ", concat(req.path, concat(" -> ", concat(int_to_string(code), "\n"))))));
|
||||
slog(line);
|
||||
}
|
||||
|
||||
// Bind 0.0.0.0:<port> and serve forever (sequential, one connection at a
|
||||
// time — the deployment story is LAN/NAS-scale). Returns only when the
|
||||
// socket can't be opened. Per-request allocations live in an arena that
|
||||
// dies with the request.
|
||||
run_server :: (store_dir: string, port: s64) -> !ServeErr {
|
||||
fd, fe := http.listen_on(port);
|
||||
if fe { raise error.Bind; }
|
||||
|
||||
slog(concat("distd: serving store ", concat(store_dir, concat(" on http://0.0.0.0:", concat(int_to_string(port), "\n")))));
|
||||
|
||||
while true {
|
||||
client := sock.accept(fd, null, null);
|
||||
if client < 0 { continue; }
|
||||
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 65536);
|
||||
push Context.{ allocator = xx arena } {
|
||||
serve_one(client, store_dir);
|
||||
}
|
||||
arena.deinit();
|
||||
sock.close(client);
|
||||
}
|
||||
return;
|
||||
}
|
||||
88
src/server/http.sx
Normal file
88
src/server/http.sx
Normal file
@@ -0,0 +1,88 @@
|
||||
// =====================================================================
|
||||
// http.sx — minimal HTTP/1.1 over `std.socket` (subplan 04, Slice 1).
|
||||
//
|
||||
// The temporary in-repo boundary for the missing `std.http`, written so it
|
||||
// can be lifted into the sx stdlib later. Deliberately minimal — exactly
|
||||
// what a read-only JSON API + artifact download server needs:
|
||||
//
|
||||
// * `listen_on(port)` — a listening TCP socket on 0.0.0.0:<port>
|
||||
// (INADDR_ANY, so the server is reachable from the LAN);
|
||||
// * `parse_request` — method + path views off the request line;
|
||||
// * `respond` — one full response: status line, Content-Type/-Length,
|
||||
// `Connection: close`, optional extra header lines, body.
|
||||
//
|
||||
// NOT handled (v0, documented): request headers and bodies (the routes are
|
||||
// GET-only and need neither), keep-alive (every response closes), chunked
|
||||
// transfer, TLS (the deployment plan terminates TLS at a reverse proxy).
|
||||
// A request is read in ONE `read` — request lines of interest fit the
|
||||
// first segment; anything that doesn't parse is a 400.
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
sock :: #import "modules/std/socket.sx";
|
||||
|
||||
HttpError :: error {
|
||||
Socket,
|
||||
Bind,
|
||||
Listen,
|
||||
}
|
||||
|
||||
// Method + path of one request, as VIEWS into the caller's read buffer.
|
||||
Request :: struct {
|
||||
method: string = "";
|
||||
path: string = "";
|
||||
}
|
||||
|
||||
// Open a listening socket on 0.0.0.0:<port>. SO_REUSEADDR so a restarted
|
||||
// server can re-bind without waiting out TIME_WAIT.
|
||||
listen_on :: (port: s64) -> (s32, !HttpError) {
|
||||
fd := sock.socket(sock.AF_INET, sock.SOCK_STREAM, 0);
|
||||
if fd < 0 { raise error.Socket; }
|
||||
opt : s32 = 1;
|
||||
sock.setsockopt(fd, sock.SOL_SOCKET, sock.SO_REUSEADDR, @opt, 4);
|
||||
addr : sock.SockAddr = .{ sin_len = 16, sin_family = 2, sin_port = sock.htons(port) };
|
||||
if sock.bind(fd, @addr, 16) < 0 { sock.close(fd); raise error.Bind; }
|
||||
if sock.listen(fd, 16) < 0 { sock.close(fd); raise error.Listen; }
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Parse the request line `METHOD SP PATH SP HTTP/x.y` off the raw bytes.
|
||||
// False when the bytes don't look like an HTTP request line.
|
||||
parse_request :: (raw: string, req: *Request) -> bool {
|
||||
i := 0;
|
||||
while i < raw.len and raw[i] != 32 { i += 1; } // 32 = ' '
|
||||
if i == 0 or i >= raw.len { return false; }
|
||||
req.method = string.{ ptr = raw.ptr, len = i };
|
||||
|
||||
j := i + 1;
|
||||
k := j;
|
||||
while k < raw.len and raw[k] != 32 and raw[k] != 13 { k += 1; } // 13 = '\r'
|
||||
if k == j or k >= raw.len { return false; }
|
||||
if raw[k] != 32 { return false; } // no HTTP version after path
|
||||
req.path = string.{ ptr = @raw[j], len = k - j };
|
||||
return true;
|
||||
}
|
||||
|
||||
status_text :: (code: s64) -> string {
|
||||
if code == 200 { return "OK"; }
|
||||
if code == 400 { return "Bad Request"; }
|
||||
if code == 404 { return "Not Found"; }
|
||||
if code == 405 { return "Method Not Allowed"; }
|
||||
if code == 503 { return "Service Unavailable"; }
|
||||
return "Internal Server Error";
|
||||
}
|
||||
|
||||
// Write one complete response and leave the connection for the caller to
|
||||
// close. `extra` is zero or more pre-formatted header lines, each ending
|
||||
// in `\r\n` ("" for none).
|
||||
respond :: (client: s32, code: s64, content_type: string, extra: string, body: string) {
|
||||
h := concat("HTTP/1.1 ", concat(int_to_string(code), concat(" ", status_text(code))));
|
||||
h = concat(h, "\r\n");
|
||||
h = concat(h, concat("Content-Type: ", concat(content_type, "\r\n")));
|
||||
h = concat(h, concat("Content-Length: ", concat(int_to_string(body.len), "\r\n")));
|
||||
h = concat(h, "Connection: close\r\n");
|
||||
h = concat(h, extra);
|
||||
h = concat(h, "\r\n");
|
||||
sock.write(client, h.ptr, xx h.len);
|
||||
if body.len > 0 { sock.write(client, body.ptr, xx body.len); }
|
||||
}
|
||||
Reference in New Issue
Block a user