nx_sites_daemon.nx source
↩ module page · 714 lines · 42666 B
1// nx_sites_daemon.nx -- sovereign multi-vhost HTTPS daemon (pure Nishi,
2// silicon-up). Serves andelinwest.com + nishifamily.com (+ /wiki) by
3// Host-header / path dispatch over the PROVEN TLS 1.3 + Ed25519 path.
4//
5// Operator 2026-05-28 (full autonomy): "get andelinwest.com and
6// nishifamily.com/wiki up ... near hundred percent site uptime."
7//
8// Reuses only the proven primitives (run_ed25519 handshake + app_recv +
9// app_send) the ed25519 test daemon validated end-to-end. Avoids the
10// mv daemon's nx_wiki_route_dispatch (which returns empty on the NAS).
11// Dispatch is simple substring matching on the decrypted request --
12// reliable for placeholders. Real wiki engine swaps in later behind
13// the same vhost routing.
14//
15// M6 anti-pretend-stub: each page SELF-LABELS as a launch placeholder
16// and shows real structure (andelinwest names its compliance blockers;
17// the wiki lists the actual shipped substrate components).
18
19import "nx_syscalls.nx"
20import "nx_csprng.nx"
21import "nx_http_server.nx"
22import "nx_tls13_server_session.nx"
23import "nx_tls13_server_session_run.nx"
24import "nx_tls13_server_session_app_data.nx"
25import "nx_sites_telemetry.nx"
26import "wiki/nx_wiki_status.nx"
27import "wiki/nx_wiki_article.nx"
28import "wiki/nx_wiki_queue.nx"
29import "_hdl_build/nx_host_router.nx"
30// Route B: SNI -> per-domain wildcard-cert selection (shared with the gate).
31import "nx_sni_cert_select.nx"
32// L5 dynamic: reverse-proxy /research -> the sovereign library daemon (:8095).
33import "nx_http_proxy.nx"
34
35const NX_SD_PORT: i64 = 8443
36const NX_SD_BUDGET: i64 = 100000000
37// Fork-per-connection concurrency cap. Bounds live handler children so a
38// scanner flood can't fork-bomb the box; the parent block-reaps one child
39// when at the cap before forking again.
40const NX_SD_MAX_CHILDREN: i64 = 64
41// Browser-trusted Let's Encrypt ECDSA P-256 chain (SAN andelinwest.com +
42// nishifamily.com) + its P-256 private key. Replaces the Ed25519
43// self-signed cert that all browsers reject (Ed25519 server certs are
44// unsupported by Chrome/Firefox/Safari -> "SSL protocol error").
45// CERT_PATH is the CONCATENATED-DER fullchain (leaf + LE intermediates),
46// leaf first; the TLS run splits it into one CertificateEntry per cert
47// so clients can build a path to a root in their trust store.
48const NX_SD_CERT_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_fullchain.der" as *u8
49const NX_SD_PRIV_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_ecdsa_key.bin" as *u8
50// Route B (SNI-aware TLS): TWO per-domain wildcard chains loaded at startup, one chosen
51// per handshake by the ClientHello SNI. Each is a concat-DER fullchain (leaf-first) + a
52// 32-byte P-256 ECDSA private scalar -- same on-disk format as the legacy pair above.
53// nishifamily = the DEFAULT (SAN *.nishifamily.com + nishifamily.com)
54// andelinwest = SAN *.andelinwest.com + andelinwest.com
55// Operator deploys the four files at these paths; absent files degrade fail-safe (see main).
56const NX_SD_CERT_NISHI: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_nishifamily_fullchain.der" as *u8
57const NX_SD_PRIV_NISHI: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_nishifamily_ecdsa_key.bin" as *u8
58const NX_SD_CERT_ANDELIN: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_andelinwest_fullchain.der" as *u8
59const NX_SD_PRIV_ANDELIN: *u8 = "/volume1/homes/elderwesto/nishihost/certs/le_andelinwest_ecdsa_key.bin" as *u8
60// MSG_PEEK (recvfrom flag 0x2): read the cleartext ClientHello WITHOUT consuming it, so the
61// proven handshake re-reads the same bytes -> the SNI peek is invisible to nx_tls13_server_session_run.
62const NX_SD_MSG_PEEK: i64 = 2
63const NX_SD_SNI_PEEK_CAP: i64 = 8192
64// Sovereign hosting sites table (Host -> doc root). Per-request file serve = HOT content.
65const NX_SD_SITES_CONF: *u8 = "/volume1/homes/elderwesto/nishihost/sites.conf" as *u8
66// SENSE layer: append-only per-connection telemetry log (self-improving
67// system). Opened once at startup; written fire-and-forget per request.
68const NX_SD_TELEMETRY_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/sites_telemetry.log" as *u8
69// Keep-alive: max requests served on one TLS session before we close it
70// (bounds a single client's hold on a forked child); response build cap.
71const NX_SD_MAX_REQ_PER_CONN: i64 = 64
72const NX_SD_OUTCAP: i64 = 16384
73// Hoisted buffer for the live /wiki/status scorecard response (rendered
74// once at startup by the real wiki engine; served verbatim).
75const NX_SD_STATUS_CAP: i64 = 65536
76// Hoisted buffers for the Gitea x Wikipedia component articles + index.
77const NX_SD_ART_CAP: i64 = 65536
78// Per-request buffer for the DYNAMIC routes (work queue) rendered in the child.
79const NX_SD_DYN_CAP: i64 = 65536
80// Favicon: answer instantly with 204 (no body) so the browser's parallel
81// favicon fetch doesn't cost a second handshake / hang.
82const RESP_FAVICON: *u8 = "HTTP/1.1 204 No Content\r\nConnection: keep-alive\r\n\r\n" as *u8
83
84const BODY_ANDELIN: *u8 = "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Andelin West Law - Coming Soon</title><style>body{font-family:Georgia,serif;max-width:680px;margin:8vh auto;padding:0 24px;color:#1a1a2e;line-height:1.6}h1{font-size:2rem}.tag{color:#555}.note{margin-top:2rem;padding:16px 20px;background:#f4f4f8;border-left:4px solid #2a4d8f;font-size:.95rem}.foot{margin-top:3rem;font-size:.8rem;color:#888}</style></head><body><h1>Andelin West Law</h1><p class=\"tag\">A modern legal practice. Site launching soon.</p><div class=\"note\"><strong>Placeholder page.</strong> Served by the sovereign Nishi substrate. The full site is pending attorney review of state-bar advertising rules, ADA accessibility conformance, and the privacy notice before any client-facing or intake content goes live.</div><p class=\"foot\">Served bits-up by the Nishi stack — TLS 1.3 + Ed25519, no third-party web server.</p></body></html>\n" as *u8
85
86const BODY_WIKI: *u8 = "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Nishi Ecosystem Wiki</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:760px;margin:6vh auto;padding:0 24px;color:#161622;line-height:1.65}h1{font-size:2.1rem}h2{font-size:1.1rem;margin-top:2rem;color:#2a4d8f}ul{padding-left:1.2rem}li{margin:.35rem 0}.note{padding:14px 18px;background:#eef2fb;border-left:4px solid #2a4d8f;border-radius:4px;font-size:.95rem}code{background:#f0f0f4;padding:1px 5px;border-radius:3px;font-size:.9em}.foot{margin-top:3rem;font-size:.8rem;color:#888}</style></head><body><h1>Nishi Ecosystem Wiki</h1><p class=\"note\"><strong>Launch placeholder — the live wiki engine is coming online.</strong> What you are reading is itself served bits-up by the substrate below: no nginx, no OpenSSL, no libc.</p><p style=\"margin:1.2rem 0\"><a href=\"/wiki/status\"><strong>▸ Live stack scorecard →</strong></a> <a href=\"/wiki/components\"><strong>▸ Component articles (Gitea×Wikipedia) →</strong></a></p><h2>Shipped substrate</h2><ul><li>Self-hosting NishiLang compiler (RV64 + x86_64), bits-up from C bootstrap</li><li>TLS 1.3 server + Ed25519 signatures (RFC 8446 / 8032) — serving this page now</li><li>SHA-256/512, ChaCha20-Poly1305, AES-GCM, HKDF, Argon2id — all KAT-verified</li><li>Sovereign secrets vault (HashiCorp-class, ChaCha20-Poly1305 at rest)</li><li>Multi-vhost HTTPS daemon with DHCP-stable routing + auto-healing watchdog</li></ul><h2>In progress</h2><ul><li>Full wiki engine (search index, doc store, markdown render)</li><li>Modern auth (OPAQUE PAKE + BIP39 recovery, no cookies)</li><li>Multi-agent concurrent editing (CRDT, sovereign)</li></ul><p class=\"foot\">nishifamily.com/wiki — bits-up sovereign hosting on the west NAS.</p></body></html>\n" as *u8
87
88const BODY_FAMILY: *u8 = "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Nishi Family</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:680px;margin:8vh auto;padding:0 24px;color:#161622;line-height:1.65;text-align:center}h1{font-size:2.3rem}.tag{color:#555;font-size:1.05rem}a{color:#2a4d8f}.foot{margin-top:3rem;font-size:.8rem;color:#888}</style></head><body><h1>Nishi Family</h1><p class=\"tag\">The family hub — home of the Nishi sovereign ecosystem.</p><p><a href=\"/wiki\">Explore the Nishi Ecosystem Wiki →</a></p><p class=\"foot\">Served bits-up by the Nishi stack — TLS 1.3 + Ed25519, no third-party web server.</p></body></html>\n" as *u8
89
90func sd_strlen(s: *u8) -> i64 {
91 var n: i64 = 0
92 while s[n] != 0 { n = n + 1 }
93 return n
94}
95
96// substring search: returns 1 if needle (nlen) occurs in hay[0..hlen).
97func sd_contains(hay: *u8, hlen: i64, needle: *u8, nlen: i64) -> i64 {
98 if nlen <= 0 { return 0 }
99 if hlen < nlen { return 0 }
100 var i: i64 = 0
101 let last: i64 = hlen - nlen
102 while i <= last {
103 var j: i64 = 0
104 var ok: i64 = 1
105 while j < nlen {
106 if hay[i + j] != needle[j] { ok = 0; j = nlen } else { j = j + 1 }
107 }
108 if ok == 1 { return 1 }
109 i = i + 1
110 }
111 return 0
112}
113
114// Return the index just past the first CRLFCRLF (HTTP header/body split), or
115// hlen if absent. Used to locate a POST body in the decrypted request.
116func sd_body_start(hay: *u8, hlen: i64) -> i64 {
117 if hlen < 4 { return hlen }
118 var i: i64 = 0
119 let last: i64 = hlen - 4
120 while i <= last {
121 if hay[i] == (0x0D as u8) { if hay[i + 1] == (0x0A as u8) { if hay[i + 2] == (0x0D as u8) { if hay[i + 3] == (0x0A as u8) {
122 return i + 4
123 } } } }
124 i = i + 1
125 }
126 return hlen
127}
128
129// --- HTTP/1.1 response construction (keep-alive requires Content-Length
130// so the client knows where the body ends and can reuse the connection) ---
131func sd_u64_to_dec(n: i64, buf: *u8) -> i64 {
132 if n == 0 { buf[0] = 48; return 1 }
133 var d: i64 = 0
134 var x: i64 = n
135 while x > 0 { d = d + 1; x = x / 10 }
136 var i: i64 = d - 1
137 x = n
138 while i >= 0 {
139 buf[i] = (48 + (x % 10)) as u8
140 x = x / 10
141 i = i - 1
142 }
143 return d
144}
145
146func sd_append(dst: *u8, off: i64, src: *u8, n: i64) -> i64 {
147 var i: i64 = 0
148 while i < n { dst[off + i] = src[i]; i = i + 1 }
149 return off + n
150}
151
152func sd_append_str(dst: *u8, off: i64, s: *u8) -> i64 {
153 return sd_append(dst, off, s, sd_strlen(s))
154}
155
156// Build a complete HTTP/1.1 keep-alive response (status + headers +
157// Content-Length + body) into out; returns total length. Built ONCE per
158// page at startup into a hoisted buffer, then sent verbatim per request.
159func sd_build_resp(out: *u8, ctype: *u8, body: *u8, body_len: i64) -> i64 {
160 var w: i64 = 0
161 w = sd_append_str(out, w, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8)
162 w = sd_append_str(out, w, ctype)
163 w = sd_append_str(out, w, "\r\nContent-Length: " as *u8)
164 let dbuf: *u8 = sys_mmap(24)
165 let dn: i64 = sd_u64_to_dec(body_len, dbuf)
166 w = sd_append(out, w, dbuf, dn)
167 w = sd_append_str(out, w, "\r\nConnection: keep-alive\r\nX-Served-By: nishi-substrate\r\n\r\n" as *u8)
168 w = sd_append(out, w, body, body_len)
169 return w
170}
171
172// Load a concatenated-DER fullchain; returns the buffer (or 0 on failure) and writes its
173// length into len_box. Validates a sane minimum size (a real LE leaf is ~900+ bytes).
174func sd_load_chain(path: *u8, len_box: *i64) -> *u8 {
175 len_box[0] = 0
176 let p: *u8 = sys_read_file(path, len_box)
177 if (p as i64) == 0 { return 0 as *u8 }
178 if len_box[0] < 100 { return 0 as *u8 }
179 return p
180}
181
182// Load a 32-byte P-256 ECDSA private scalar; returns the buffer or 0 if absent / wrong size.
183func sd_load_key(path: *u8) -> *u8 {
184 let lb: *i64 = (sys_mmap(8)) as *i64
185 lb[0] = 0
186 let p: *u8 = sys_read_file(path, lb)
187 if (p as i64) == 0 { return 0 as *u8 }
188 if lb[0] != 32 { return 0 as *u8 }
189 return p
190}
191
192// ---- DATA-DRIVEN SNI cert table (R1b 2026-07-15) -------------------------------------------------
193// Every domain we serve HTTPS for is one manifest row (suffix + fullchain-der + ecdsa-key), so adding
194// a domain = drop its le_<domain>_* + a line, NEVER an edge recompile. Row 0 = the DEFAULT cert.
195const NX_SD_CERT_MANIFEST: *u8 = "/volume1/homes/elderwesto/nishihost/certs/sni_certs.manifest" as *u8
196const NX_SD_CERT_MAX: i64 = 32
197
198// Parse a positive decimal from a NUL-terminated string (stops at first non-digit). Used for argv port.
199func sd_atoi(s: *u8) -> i64 {
200 var v: i64 = 0
201 var i: i64 = 0
202 var go: i64 = 1
203 while go == 1 {
204 let c: i64 = s[i] as i64
205 if c < 48 { go = 0 }
206 else { if c > 57 { go = 0 }
207 else { v = v * 10 + (c - 48); i = i + 1 } }
208 }
209 return v
210}
211
212// Copy one whitespace-delimited field from buf[pos..end) into out (NUL-terminated). Skips leading
213// spaces/tabs. Returns the position just past the field, or -1 if the line has no field (blank/'#'/EOL).
214func sd_field(buf: *u8, pos: i64, end: i64, out: *u8, out_cap: i64) -> i64 {
215 var p: i64 = pos
216 var skip: i64 = 1
217 while skip == 1 {
218 if p >= end { skip = 0 }
219 else {
220 let c: i64 = buf[p] as i64
221 if c == 32 { p = p + 1 }
222 else { if c == 9 { p = p + 1 } else { skip = 0 } }
223 }
224 }
225 if p >= end { return 0 - 1 }
226 let c0: i64 = buf[p] as i64
227 if c0 == 10 { return 0 - 1 }
228 if c0 == 13 { return 0 - 1 }
229 if c0 == 35 { return 0 - 1 }
230 var o: i64 = 0
231 var cp: i64 = 1
232 while cp == 1 {
233 if p >= end { cp = 0 }
234 else {
235 let c: i64 = buf[p] as i64
236 if c == 32 { cp = 0 }
237 else { if c == 9 { cp = 0 }
238 else { if c == 10 { cp = 0 }
239 else { if c == 13 { cp = 0 }
240 else {
241 if o < out_cap - 1 { out[o] = buf[p] as u8; o = o + 1 }
242 p = p + 1
243 } } } }
244 }
245 }
246 out[o] = 0 as u8
247 return p
248}
249
250// Advance past the current line (to the byte after the next '\n', or to end).
251func sd_next_line(buf: *u8, pos: i64, end: i64) -> i64 {
252 var p: i64 = pos
253 var go: i64 = 1
254 while go == 1 {
255 if p >= end { go = 0 }
256 else { if (buf[p] as i64) == 10 { p = p + 1; go = 0 } else { p = p + 1 } }
257 }
258 return p
259}
260
261// Parse the manifest into parallel arrays; loads each row's cert+key. Rows whose cert/key fail to load
262// are skipped (fail-safe). Returns the number of rows loaded (0 => caller uses the legacy fallback).
263func sd_load_cert_table(man: *u8, man_n: i64,
264 suf_ptrs: *i64, suf_lens: *i64,
265 cert_ptrs: *i64, cert_lens: *i64, key_ptrs: *i64,
266 max: i64) -> i64 {
267 var count: i64 = 0
268 var pos: i64 = 0
269 while pos < man_n {
270 if count >= max { pos = man_n }
271 else {
272 let sufb: *u8 = sys_mmap(256)
273 let p1: i64 = sd_field(man, pos, man_n, sufb, 256)
274 if p1 < 0 { pos = sd_next_line(man, pos, man_n) }
275 else {
276 let certp: *u8 = sys_mmap(512)
277 let p2: i64 = sd_field(man, p1, man_n, certp, 512)
278 if p2 < 0 { pos = sd_next_line(man, pos, man_n) }
279 else {
280 let keyp: *u8 = sys_mmap(512)
281 let p3: i64 = sd_field(man, p2, man_n, keyp, 512)
282 if p3 < 0 { pos = sd_next_line(man, pos, man_n) }
283 else {
284 let clb: *i64 = (sys_mmap(8)) as *i64
285 clb[0] = 0
286 let cbuf: *u8 = sd_load_chain(certp, clb)
287 let kbuf: *u8 = sd_load_key(keyp)
288 if (cbuf as i64) == 0 { pos = sd_next_line(man, pos, man_n) }
289 else { if (kbuf as i64) == 0 { pos = sd_next_line(man, pos, man_n) }
290 else {
291 var sl: i64 = 0
292 while sufb[sl] != (0 as u8) { sl = sl + 1 }
293 suf_ptrs[count] = sufb as i64
294 suf_lens[count] = sl
295 cert_ptrs[count] = cbuf as i64
296 cert_lens[count] = clb[0]
297 key_ptrs[count] = kbuf as i64
298 count = count + 1
299 pos = sd_next_line(man, pos, man_n)
300 } }
301 }
302 }
303 }
304 }
305 }
306 return count
307}
308
309func main(argc: i64, argv: *i64) -> i64 {
310 // ---- Route B: load BOTH per-domain wildcard chains at startup ----
311 // The NISHI (nishifamily) pair is the DEFAULT served for any host that does not match a
312 // known domain (and whenever the SNI is absent/garbled). Fail-safe: if the new
313 // nishifamily files are not deployed yet, fall back to the LEGACY single chain so the
314 // existing single-domain handshake never regresses (rule 20 fail-fast only if NOTHING loads).
315 let cert_len_box: *i64 = (sys_mmap(8)) as *i64
316 var cert_nishi: *u8 = sd_load_chain(NX_SD_CERT_NISHI, cert_len_box)
317 var ecdsa_nishi: *u8 = sd_load_key(NX_SD_PRIV_NISHI)
318 if (cert_nishi as i64) == 0 { cert_nishi = sd_load_chain(NX_SD_CERT_PATH, cert_len_box) }
319 if (ecdsa_nishi as i64) == 0 { ecdsa_nishi = sd_load_key(NX_SD_PRIV_PATH) }
320 if (cert_nishi as i64) == 0 { return 2 }
321 if (ecdsa_nishi as i64) == 0 { return 3 }
322 let cert_nishi_len: i64 = cert_len_box[0]
323
324 // The ANDELIN (andelinwest) pair. Fail-safe: if its files are absent, serve the default
325 // pair instead (a valid TLS handshake still completes -- the cert just won't cover an
326 // andelinwest host -- rather than crash or drop the connection).
327 let andelin_len_box: *i64 = (sys_mmap(8)) as *i64
328 var cert_andelin: *u8 = sd_load_chain(NX_SD_CERT_ANDELIN, andelin_len_box)
329 var ecdsa_andelin: *u8 = sd_load_key(NX_SD_PRIV_ANDELIN)
330 var cert_andelin_len: i64 = andelin_len_box[0]
331 if (cert_andelin as i64) == 0 { cert_andelin = cert_nishi; cert_andelin_len = cert_nishi_len; ecdsa_andelin = ecdsa_nishi }
332 if (ecdsa_andelin as i64) == 0 { cert_andelin = cert_nishi; cert_andelin_len = cert_nishi_len; ecdsa_andelin = ecdsa_nishi }
333
334 // ---- DATA-DRIVEN cert table: load the manifest (row 0 = default). Fall back to the legacy
335 // nishi+andelin pair if the manifest is absent/empty so a fresh box never regresses (rule 20). ----
336 let cert_suf_ptrs: *i64 = (sys_mmap(NX_SD_CERT_MAX * 8)) as *i64
337 let cert_suf_lens: *i64 = (sys_mmap(NX_SD_CERT_MAX * 8)) as *i64
338 let cert_buf_ptrs: *i64 = (sys_mmap(NX_SD_CERT_MAX * 8)) as *i64
339 let cert_buf_lens: *i64 = (sys_mmap(NX_SD_CERT_MAX * 8)) as *i64
340 let cert_key_ptrs: *i64 = (sys_mmap(NX_SD_CERT_MAX * 8)) as *i64
341 let man_box: *i64 = (sys_mmap(8)) as *i64
342 man_box[0] = 0
343 let man_buf: *u8 = sys_read_file(NX_SD_CERT_MANIFEST, man_box)
344 var cert_count: i64 = 0
345 if (man_buf as i64) != 0 {
346 cert_count = sd_load_cert_table(man_buf, man_box[0], cert_suf_ptrs, cert_suf_lens, cert_buf_ptrs, cert_buf_lens, cert_key_ptrs, NX_SD_CERT_MAX)
347 }
348 if cert_count == 0 {
349 cert_suf_ptrs[0] = ("nishifamily.com" as *u8) as i64
350 cert_suf_lens[0] = 15
351 cert_buf_ptrs[0] = cert_nishi as i64
352 cert_buf_lens[0] = cert_nishi_len
353 cert_key_ptrs[0] = ecdsa_nishi as i64
354 cert_suf_ptrs[1] = ("andelinwest.com" as *u8) as i64
355 cert_suf_lens[1] = 15
356 cert_buf_ptrs[1] = cert_andelin as i64
357 cert_buf_lens[1] = cert_andelin_len
358 cert_key_ptrs[1] = ecdsa_andelin as i64
359 cert_count = 2
360 }
361 sys_write(1, "nishi sites daemon: SNI cert table rows=" as *u8, 40)
362 let ccb: *u8 = sys_mmap(24)
363 let ccn: i64 = sd_u64_to_dec(cert_count, ccb)
364 sys_write(1, ccb, ccn)
365 sys_write(1, "\n" as *u8, 1)
366
367 // Load the sovereign hosting sites table once at startup. Graceful: if absent,
368 // sites_cfg_n stays 0 and every host uses the legacy baked routing (no regression).
369 let sd_cfg_box: *i64 = (sys_mmap(8)) as *i64
370 sd_cfg_box[0] = 0
371 let sites_cfg: *u8 = sys_read_file(NX_SD_SITES_CONF, sd_cfg_box)
372 var sites_cfg_n: i64 = 0
373 if (sites_cfg as i64) != 0 { sites_cfg_n = sd_cfg_box[0] }
374
375 // Bind port: argv[1] overrides the default (lets a test instance run on :8543 beside the live :8443).
376 var port: i64 = NX_SD_PORT
377 if argc > 1 { let ap: i64 = sd_atoi(argv[1] as *u8); if ap > 0 { port = ap } }
378 let addr_buf: *u8 = sys_mmap(16)
379 nx_http_server_addr_any(addr_buf, port)
380 let lv: *i64 = (sys_mmap(8)) as *i64
381 let lfd: i64 = nx_http_server_listen(addr_buf, 16, lv)
382 if lfd < 0 { return 4 }
383 sys_write(1, "nishi sites daemon: data-driven SNI cert table + sites.conf docroot, listening\n" as *u8, 78)
384
385 // Build full HTTP/1.1 keep-alive responses ONCE into hoisted buffers
386 // (constant memory; sent verbatim for every matching request).
387 let ctype_html: *u8 = "text/html; charset=utf-8" as *u8
388 let nb_andelin: i64 = sd_strlen(BODY_ANDELIN)
389 let nb_wiki: i64 = sd_strlen(BODY_WIKI)
390 let nb_family: i64 = sd_strlen(BODY_FAMILY)
391 let resp_andelin: *u8 = sys_mmap(nb_andelin + 256)
392 let resp_wiki: *u8 = sys_mmap(nb_wiki + 256)
393 let resp_family: *u8 = sys_mmap(nb_family + 256)
394 let n_andelin: i64 = sd_build_resp(resp_andelin, ctype_html, BODY_ANDELIN, nb_andelin)
395 let n_wiki: i64 = sd_build_resp(resp_wiki, ctype_html, BODY_WIKI, nb_wiki)
396 let n_family: i64 = sd_build_resp(resp_family, ctype_html, BODY_FAMILY, nb_family)
397 let n_favicon: i64 = sd_strlen(RESP_FAVICON)
398
399 // /wiki/status -- the LIVE sovereign scorecard, rendered ONCE at startup
400 // by the real wiki engine (nx_wiki_status_handle) into a hoisted buffer
401 // (constant memory; served verbatim per request). Self-contained: needs
402 // NO doc-store, so it works on the NAS where the content store is empty
403 // (the reason the full dispatcher returned empty here before). Graceful:
404 // on render failure, falls back to the static wiki placeholder.
405 var resp_wstatus: *u8 = sys_mmap(NX_SD_STATUS_CAP)
406 let wstatus_box: *i64 = (sys_mmap(8)) as *i64
407 wstatus_box[0] = 0
408 let wstatus_rc: i64 = nx_wiki_status_handle(resp_wstatus, NX_SD_STATUS_CAP, wstatus_box)
409 var n_wstatus: i64 = wstatus_box[0]
410 if wstatus_rc != 0 { resp_wstatus = resp_wiki; n_wstatus = n_wiki }
411 if n_wstatus <= 0 { resp_wstatus = resp_wiki; n_wstatus = n_wiki }
412
413 // Component articles (Gitea x Wikipedia) + index, each rendered ONCE at
414 // startup by the wiki engine into a hoisted buffer (constant memory;
415 // served verbatim). Self-contained -- no doc-store needed. Graceful
416 // fallback to the wiki placeholder on any render error.
417 var resp_art_crawler: *u8 = sys_mmap(NX_SD_ART_CAP)
418 let acr_box: *i64 = (sys_mmap(8)) as *i64
419 acr_box[0] = 0
420 let acr_rc: i64 = nx_wiki_article_handle("crawler" as *u8, 7, resp_art_crawler, NX_SD_ART_CAP, acr_box)
421 var n_art_crawler: i64 = acr_box[0]
422 if acr_rc != 0 { resp_art_crawler = resp_wiki; n_art_crawler = n_wiki }
423 if n_art_crawler <= 0 { resp_art_crawler = resp_wiki; n_art_crawler = n_wiki }
424
425 var resp_art_x25519: *u8 = sys_mmap(NX_SD_ART_CAP)
426 let axx_box: *i64 = (sys_mmap(8)) as *i64
427 axx_box[0] = 0
428 let axx_rc: i64 = nx_wiki_article_handle("x25519" as *u8, 6, resp_art_x25519, NX_SD_ART_CAP, axx_box)
429 var n_art_x25519: i64 = axx_box[0]
430 if axx_rc != 0 { resp_art_x25519 = resp_wiki; n_art_x25519 = n_wiki }
431 if n_art_x25519 <= 0 { resp_art_x25519 = resp_wiki; n_art_x25519 = n_wiki }
432
433 var resp_art_ranking: *u8 = sys_mmap(NX_SD_ART_CAP)
434 let ark_box: *i64 = (sys_mmap(8)) as *i64
435 ark_box[0] = 0
436 let ark_rc: i64 = nx_wiki_article_handle("ranking-math" as *u8, 12, resp_art_ranking, NX_SD_ART_CAP, ark_box)
437 var n_art_ranking: i64 = ark_box[0]
438 if ark_rc != 0 { resp_art_ranking = resp_wiki; n_art_ranking = n_wiki }
439 if n_art_ranking <= 0 { resp_art_ranking = resp_wiki; n_art_ranking = n_wiki }
440
441 var resp_art_wiki: *u8 = sys_mmap(NX_SD_ART_CAP)
442 let awe_box: *i64 = (sys_mmap(8)) as *i64
443 awe_box[0] = 0
444 let awe_rc: i64 = nx_wiki_article_handle("wiki-engine" as *u8, 11, resp_art_wiki, NX_SD_ART_CAP, awe_box)
445 var n_art_wiki: i64 = awe_box[0]
446 if awe_rc != 0 { resp_art_wiki = resp_wiki; n_art_wiki = n_wiki }
447 if n_art_wiki <= 0 { resp_art_wiki = resp_wiki; n_art_wiki = n_wiki }
448
449 var resp_components: *u8 = sys_mmap(NX_SD_ART_CAP)
450 let aci_box: *i64 = (sys_mmap(8)) as *i64
451 aci_box[0] = 0
452 let aci_rc: i64 = nx_wiki_components_handle(resp_components, NX_SD_ART_CAP, aci_box)
453 var n_components: i64 = aci_box[0]
454 if aci_rc != 0 { resp_components = resp_wiki; n_components = n_wiki }
455 if n_components <= 0 { resp_components = resp_wiki; n_components = n_wiki }
456
457 // /wiki/projects is discovered live by the standalone nx_pm_discover tool
458 // (the HTTP client hangs the daemon's compile graph). The daemon will
459 // serve the tool's emitted page from a file; until that file-serve is
460 // wired, /wiki/projects falls back to the wiki placeholder.
461 var resp_projects: *u8 = resp_wiki
462 var n_projects: i64 = n_wiki
463
464 // CONSTANT MEMORY: every per-connection buffer is allocated ONCE
465 // here and reused for every connection. NishiLang is GC-free + has
466 // no hidden allocator -- so allocating inside the accept loop would
467 // be a programmer-side leak (the language won't reclaim it). Hoist
468 // everything; the daemon's RSS is then fixed for its whole lifetime
469 // and cannot grow with request count.
470 let server_random: *u8 = sys_mmap(32)
471 let server_x25519_priv: *u8 = sys_mmap(32)
472 let rec_buf: *u8 = sys_mmap(20480)
473 let plain: *u8 = sys_mmap(20480)
474 let out_rec: *u8 = sys_mmap(NX_SD_OUTCAP)
475 // Route B: hoisted SNI-peek buffers (constant memory; children get a COW copy on write).
476 let sni_peek: *u8 = sys_mmap(NX_SD_SNI_PEEK_CAP)
477 let sni_host: *u8 = sys_mmap(256)
478 // Dynamic-route render buffer (work queue / request 303). Hoisted in the
479 // parent; each forked child gets its own COW copy.
480 let dyn_buf: *u8 = sys_mmap(NX_SD_DYN_CAP)
481 // SENSE layer (self-improving system): peer-addr buffer for
482 // LAN/internet classification, a hoisted telemetry line buffer, and
483 // one append-mode log fd held open for the daemon's whole lifetime
484 // (no per-connection open/close in the hot path). A failed open
485 // (tel_fd < 0) simply disables telemetry -- it never blocks serving.
486 let sock_addr: *u8 = sys_mmap(64)
487 let sock_len: *i64 = (sys_mmap(8)) as *i64
488 let tel_buf: *u8 = sys_mmap(256)
489 let tel_fd: i64 = sys_openat_append(NX_SD_TELEMETRY_PATH, 420)
490
491 // FORK-PER-CONNECTION: each accepted connection is handled in its own
492 // child, which exits when done. The OS reclaims everything the child
493 // allocated -- so the P-256 field ops' per-operation allocations cannot
494 // accumulate in a long-lived process (the prior OOM -> supervisor-
495 // restart cycle). Bonus: connections are served CONCURRENTLY rather
496 // than one-at-a-time. csprng is fork-safe (getrandom / urandom per
497 // call), so children never share key material. Concurrency is bounded
498 // by NX_SD_MAX_CHILDREN; the parent reaps exited children every accept.
499 // Listen-socket timeout so accept() returns periodically when idle --
500 // lets the reap loop clear finished children even with no new traffic
501 // (otherwise the parent blocks in accept() while children zombie).
502 sys_set_socket_timeout(lfd, 5)
503 let reap_status: *i64 = (sys_mmap(8)) as *i64
504 var served: i64 = 0
505 var live: i64 = 0
506 while served < NX_SD_BUDGET {
507 // Reap exited children (non-blocking) every iteration -- including
508 // idle accept-timeouts -- so zombies never accumulate.
509 while sys_wait4(0 - 1, reap_status, 1) > 0 { live = live - 1 }
510
511 sock_len[0] = 16
512 let cfd: i64 = sys_accept_with_addr(lfd, sock_addr, sock_len)
513 if cfd < 0 { continue } // accept timeout (idle) or transient error
514
515 // At the cap: block-reap one child before forking another (throttle).
516 if live >= NX_SD_MAX_CHILDREN {
517 if sys_wait4(0 - 1, reap_status, 0) > 0 { live = live - 1 }
518 }
519
520 let pid: i64 = sys_fork()
521 if pid == 0 {
522 // ---- CHILD: handle exactly this connection, then exit ----
523 sys_close(lfd)
524 // SENSE: classify the access path (observational only).
525 let acc_lan: i64 = nx_access_is_lan(sock_addr)
526 // PUBLIC-EXPOSURE HARDENING: 10s cap on every blocking read/write
527 // so a silent scanner can't pin this child forever.
528 sys_set_socket_timeout(cfd, 10)
529 // TCP_NODELAY (IPPROTO_TCP=6, TCP_NODELAY=1): disable Nagle on the
530 // connection. MEASURED: the PK crypto is only ~9ms/handshake yet
531 // curl saw ~150ms -- the ~140ms gap is Nagle holding our multi-
532 // record handshake flight until the client's delayed ACK (~40ms
533 // each). Without this, every page pays that stall. Per-socket,
534 // not inherited from the listener, so it must be set on cfd.
535 let nodelay: *u8 = sys_mmap(4)
536 nodelay[0] = 1 as u8; nodelay[1] = 0 as u8; nodelay[2] = 0 as u8; nodelay[3] = 0 as u8
537 sys_setsockopt(cfd, 6, 1, nodelay, 4)
538 nx_csprng_fill(server_random, 32)
539 nx_csprng_fill(server_x25519_priv, 32)
540 // ---- Route B: SNI-aware cert selection ----
541 // Peek the cleartext ClientHello WITHOUT consuming it (MSG_PEEK), pick the
542 // per-domain wildcard pair by the SNI, then run the UNCHANGED handshake serving
543 // that pair. The peek never consumes bytes, so nx_tls13_server_session_run reads
544 // the exact same ClientHello it always did -> zero change to the proven handshake.
545 // Any peek failure / absent / unknown SNI -> the default (nishifamily) pair.
546 let pk_n: i64 = sys_recvfrom(cfd, sni_peek, NX_SD_SNI_PEEK_CAP, NX_SD_MSG_PEEK, 0 as *u8, 0 as *i64)
547 // DATA-DRIVEN: pick the cert-table row from the SNI (row 0 = default; unknown/absent -> 0).
548 var cert_idx: i64 = 0
549 if pk_n > 0 { cert_idx = scs_pick_from_clienthello_table(sni_peek, pk_n, sni_host, 256, cert_suf_ptrs, cert_suf_lens, cert_count) }
550 if cert_idx < 0 { cert_idx = 0 }
551 if cert_idx >= cert_count { cert_idx = 0 }
552 var sel_cert: *u8 = cert_buf_ptrs[cert_idx] as *u8
553 var sel_cert_len: i64 = cert_buf_lens[cert_idx]
554 var sel_key: *u8 = cert_key_ptrs[cert_idx] as *u8
555 let t_hs0: i64 = sys_now_us()
556 let hs_rc: i64 = nx_tls13_server_session_run(
557 cfd, server_random, server_x25519_priv,
558 sel_cert, sel_cert_len, sel_key)
559 let hs_us: i64 = sys_now_us() - t_hs0
560 var vhost_id: i64 = 0
561 var served_bytes: i64 = 0
562 if hs_rc > 0 {
563 let s: *Tls13ServerSession = hs_rc as *Tls13ServerSession
564 // KEEP-ALIVE: serve multiple requests on this one TLS session
565 // (one handshake per page, not one per file). Loop until the
566 // client closes / idle-times-out (cfd has a 10s timeout) or we
567 // hit the per-connection request cap.
568 var keep: i64 = 1
569 var nreq: i64 = 0
570 while keep == 1 {
571 if nreq >= NX_SD_MAX_REQ_PER_CONN {
572 keep = 0
573 } else {
574 let rec_n: i64 = sys_read(cfd, rec_buf, 20480)
575 if rec_n <= 0 {
576 keep = 0
577 } else {
578 let plain_n: i64 = nx_tls13_server_session_app_recv(s, rec_buf, rec_n, plain, 20480)
579 if plain_n <= 0 {
580 keep = 0
581 } else {
582 // SAME EXPERIENCE LAN+INTERNET: dispatch by
583 // Host/path ONLY (never by acc_lan). Favicon
584 // first -- a favicon req to andelinwest.com also
585 // contains "andelinwest".
586 var resp: *u8 = resp_family
587 var resp_n: i64 = n_family
588 let is_favicon: i64 = sd_contains(plain, plain_n, "favicon" as *u8, 7)
589 if is_favicon == 1 {
590 resp = RESP_FAVICON; resp_n = n_favicon; vhost_id = 9
591 } else {
592 // DYNAMIC routes (rendered per-request in this child): the work
593 // queue read + the POST request append. These read/write the
594 // sovereign journal at request time, so they cannot be hoisted.
595 var dyn_done: i64 = 0
596 let is_postreq: i64 = sd_contains(plain, plain_n, "POST /wiki/request" as *u8, 18)
597 let is_queue: i64 = sd_contains(plain, plain_n, "/wiki/queue" as *u8, 11)
598 if is_postreq == 1 {
599 let bstart: i64 = sd_body_start(plain, plain_n)
600 let dnq: *i64 = (sys_mmap(8)) as *i64
601 dnq[0] = 0
602 let prc: i64 = nx_wiki_request_handle((plain as i64 + bstart) as *u8, plain_n - bstart, dyn_buf, NX_SD_DYN_CAP, dnq)
603 if prc == 0 { resp = dyn_buf; resp_n = dnq[0]; vhost_id = 10; dyn_done = 1 }
604 }
605 if dyn_done == 0 { if is_queue == 1 {
606 let dnq2: *i64 = (sys_mmap(8)) as *i64
607 dnq2[0] = 0
608 let qrc: i64 = nx_wiki_queue_handle(dyn_buf, NX_SD_DYN_CAP, dnq2)
609 if qrc == 0 { resp = dyn_buf; resp_n = dnq2[0]; vhost_id = 11; dyn_done = 1 }
610 } }
611 // SOVEREIGN LIBRARY: /research(/*) -> reverse-proxy to the library
612 // daemon (:8095, 16K-taxon full-text). Dynamic backend, not a docroot.
613 // Additive + fail-safe: any non-/research path falls through unchanged.
614 if dyn_done == 0 {
615 let rpath: *u8 = sys_mmap(2048)
616 hr_req_path(plain, plain_n, rpath, 2048)
617 let rpre: *u8 = "/research" as *u8
618 var is_research: i64 = 1
619 var ri: i64 = 0
620 while ri < 9 { if rpath[ri] != rpre[ri] { is_research = 0; ri = 9 } else { ri = ri + 1 } }
621 if is_research == 1 { let rc9: i64 = rpath[9] as i64; if rc9 == 0 { is_research = 1 } else { if rc9 == 47 { is_research = 1 } else { is_research = 0 } } }
622 if is_research == 1 {
623 resp = dyn_buf
624 resp_n = nx_http_proxy_relay("127.0.0.1" as *u8, 9, 8095, "/research" as *u8, 9, plain, plain_n, dyn_buf, NX_SD_DYN_CAP)
625 vhost_id = 25
626 dyn_done = 1
627 }
628 }
629 // SOVEREIGN HOSTING: any host in sites.conf is served file-based
630 // (hot content -- push a file, live next request). Hosts NOT in the
631 // table fall through to the legacy baked routing below (the wiki).
632 if dyn_done == 0 {
633 if hr_known_host(sites_cfg, sites_cfg_n, plain, plain_n) == 1 {
634 resp = dyn_buf
635 resp_n = hr_serve(sites_cfg, sites_cfg_n, plain, plain_n, dyn_buf, NX_SD_DYN_CAP)
636 vhost_id = 20
637 dyn_done = 1
638 }
639 }
640 // HOT wiki dashboard: /wiki/roadmap served from a FILE per-request (living --
641 // re-push the file -> live next request). Checked before the baked /wiki route so it wins.
642 if dyn_done == 0 {
643 if sd_contains(plain, plain_n, "/wiki/roadmap" as *u8, 13) == 1 {
644 let rlb: *i64 = (sys_mmap(8)) as *i64; rlb[0] = 0
645 let rbuf: *u8 = sys_read_file("/volume1/homes/elderwesto/nishihost/wiki/roadmap.html" as *u8, rlb)
646 if (rbuf as i64) != 0 { resp = dyn_buf; resp_n = sd_build_resp(dyn_buf, ctype_html, rbuf, rlb[0]); vhost_id = 30; dyn_done = 1 }
647 }
648 }
649 // HOT /maps: nishifamily.com/maps -- the sovereign map surface (nx_geo_mapsvg
650 // renders the SVG world map; no third-party map lib). Served from a FILE per
651 // request (re-render -> live next request), same pattern as /wiki/roadmap.
652 if dyn_done == 0 {
653 if sd_contains(plain, plain_n, "/maps" as *u8, 5) == 1 {
654 let mlb: *i64 = (sys_mmap(8)) as *i64; mlb[0] = 0
655 let mbuf: *u8 = sys_read_file("/volume1/homes/elderwesto/nishihost/sites/nishifamily/maps/index.html" as *u8, mlb)
656 if (mbuf as i64) != 0 { resp = dyn_buf; resp_n = sd_build_resp(dyn_buf, ctype_html, mbuf, mlb[0]); vhost_id = 31; dyn_done = 1 }
657 }
658 }
659 if dyn_done == 0 {
660 let is_andelin: i64 = sd_contains(plain, plain_n, "andelinwest" as *u8, 11)
661 let is_wiki: i64 = sd_contains(plain, plain_n, "/wiki" as *u8, 5)
662 let is_wstatus: i64 = sd_contains(plain, plain_n, "/wiki/status" as *u8, 12)
663 let is_cidx: i64 = sd_contains(plain, plain_n, "/wiki/components" as *u8, 16)
664 let is_proj: i64 = sd_contains(plain, plain_n, "/wiki/projects" as *u8, 14)
665 let is_cr: i64 = sd_contains(plain, plain_n, "/wiki/component/crawler" as *u8, 23)
666 let is_xx: i64 = sd_contains(plain, plain_n, "/wiki/component/x25519" as *u8, 22)
667 let is_rk: i64 = sd_contains(plain, plain_n, "/wiki/component/ranking-math" as *u8, 28)
668 let is_we: i64 = sd_contains(plain, plain_n, "/wiki/component/wiki-engine" as *u8, 27)
669 // Same-experience LAN+internet: dispatch by Host/path only. The
670 // sequential ifs run least-specific first so the most-specific
671 // path (a component slug) wins the overwrite.
672 if is_andelin == 1 {
673 resp = resp_andelin; resp_n = n_andelin; vhost_id = 1
674 } else {
675 if is_wiki == 1 { resp = resp_wiki; resp_n = n_wiki; vhost_id = 2 }
676 if is_wstatus == 1 { resp = resp_wstatus; resp_n = n_wstatus; vhost_id = 3 }
677 if is_cidx == 1 { resp = resp_components; resp_n = n_components; vhost_id = 8 }
678 if is_proj == 1 { resp = resp_projects; resp_n = n_projects; vhost_id = 12 }
679 if is_cr == 1 { resp = resp_art_crawler; resp_n = n_art_crawler; vhost_id = 4 }
680 if is_xx == 1 { resp = resp_art_x25519; resp_n = n_art_x25519; vhost_id = 5 }
681 if is_rk == 1 { resp = resp_art_ranking; resp_n = n_art_ranking; vhost_id = 6 }
682 if is_we == 1 { resp = resp_art_wiki; resp_n = n_art_wiki; vhost_id = 7 }
683 }
684 }
685 }
686 let out_n: i64 = nx_tls13_server_session_app_send(s, resp, resp_n, out_rec, NX_SD_OUTCAP)
687 if out_n > 0 {
688 sys_write(cfd, out_rec, out_n)
689 served_bytes = served_bytes + resp_n
690 nreq = nreq + 1
691 } else {
692 keep = 0
693 }
694 }
695 }
696 }
697 }
698 }
699 sys_close(cfd)
700 // SENSE: append one additive telemetry record (O_APPEND -> atomic
701 // across concurrent children; fire-and-forget).
702 nx_telemetry_emit(tel_fd, tel_buf, sys_now_us(), acc_lan, vhost_id,
703 hs_us, served_bytes, hs_rc)
704 sys_exit(0)
705 }
706
707 // ---- PARENT: drop our copy of the conn fd; children reaped at loop top ----
708 sys_close(cfd)
709 if pid > 0 { live = live + 1 }
710 served = served + 1
711 }
712 sys_close(lfd)
713 return 0
714}