nx_math_serve.nx source
↩ module page · 179 lines · 7508 B
1// nx_math_serve.nx -- /math AS A TOOL, NOT A BROCHURE.
2//
3// WHY THIS EXISTS. The operator asked for a UI where you can DO mathematics; what shipped first was a page
4// whose formulas were typeset at publish time and whose worked solutions were three hardcoded equations.
5// Their words: "/math is just static output how does that match what i asked for". It did not. A page that
6// DEMONSTRATES a capability is a different artifact from one that PROVIDES it, and only the second is a UI.
7//
8// WHAT MAKES IT INTERACTIVE, AND WHY THIS SHAPE. A visitor types an equation into a PLAIN GET FORM. There
9// is no script, no fetch and no client runtime: the estate's document portal reached the same conclusion
10// (portal actions as plain forms), and a GET form has three properties a script cannot match here -- it
11// works with JavaScript off, every solved equation gets a SHAREABLE URL a teacher can hand to a class, and
12// there is no third-party anything in the path.
13//
14// ONE RENDERER, TWO ENTRY POINTS. This daemon calls mp_render from nx_math_page, the SAME function the
15// static build calls. Two renderers for one surface is the duplicate-ruler defect wearing HTML, and it
16// would drift the moment either was edited. The static file remains as the fallback a reader gets if this
17// daemon is down, and it is byte-identical to what this serves for an empty form.
18//
19// ⚠BINDS LOOPBACK, DELIBERATELY. A daemon binding INADDR_ANY is on the LAN the instant it starts; the
20// estate has measured that. The edge proxies to it, so it is never directly exposed.
21//
22// license_tier: ORIGINAL
23import "syscalls.nx"
24import "nx_http_server.nx"
25import "nx_math_page.nx"
26
27const MS_PORT_DEFAULT: i64 = 18131
28const MS_REQ_CAP: i64 = 16384
29const MS_EQ_CAP: i64 = 1024
30const MS_BACKLOG: i64 = 64
31// Response header + the rendered body. MP_CAP is the renderer's own ceiling, so the response buffer is
32// DERIVED from it rather than guessed -- a second independent constant here could only ever be wrong.
33const MS_HDR_RESERVE: i64 = 512
34
35func ms_slen(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { i = i + 1 } return i }
36
37func ms_hexval(c: i64) -> i64 {
38 if c >= 48 { if c <= 57 { return c - 48 } }
39 if c >= 97 { if c <= 102 { return c - 87 } }
40 if c >= 65 { if c <= 70 { return c - 55 } }
41 return 0 - 1
42}
43
44// URL-DECODE a query value in place into `out`. Returns its length, or -1 on a malformed escape.
45// A malformed escape is REFUSED rather than passed through: half-decoded input is how a value that looks
46// safe reaches a consumer that treats it otherwise.
47func ms_urldecode(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
48 var i: i64 = 0
49 var o: i64 = 0
50 while i < n {
51 if o >= cap { return 0 - 1 }
52 let c: i64 = src[i] as i64
53 if c == 43 { out[o] = 32 as u8; o = o + 1; i = i + 1 } else {
54 if c == 37 {
55 if i + 2 >= n { return 0 - 1 }
56 let h: i64 = ms_hexval(src[i+1] as i64)
57 let l: i64 = ms_hexval(src[i+2] as i64)
58 if h < 0 { return 0 - 1 }
59 if l < 0 { return 0 - 1 }
60 out[o] = (h * 16 + l) as u8
61 o = o + 1
62 i = i + 3
63 } else {
64 out[o] = src[i]
65 o = o + 1
66 i = i + 1
67 } }
68 }
69 return o
70}
71
72// Find "eq=" in the path's query string and decode its value. Returns the decoded length, 0 if absent.
73func ms_extract_eq(path: *u8, plen: i64, out: *u8, cap: i64) -> i64 {
74 var q: i64 = 0 - 1
75 var i: i64 = 0
76 while i < plen { if path[i] == (63 as u8) { q = i; i = plen } else { i = i + 1 } }
77 if q < 0 { return 0 }
78 var p: i64 = q + 1
79 while p < plen {
80 // a parameter runs to '&' or the end
81 var e: i64 = p
82 while e < plen { if path[e] == (38 as u8) { break } e = e + 1 }
83 // does it start with "eq=" ?
84 if e - p > 3 {
85 if path[p] == (101 as u8) { if path[p+1] == (113 as u8) { if path[p+2] == (61 as u8) {
86 return ms_urldecode(((path as i64) + p + 3) as *u8, e - p - 3, out, cap)
87 } } }
88 }
89 p = e + 1
90 }
91 return 0
92}
93
94func ms_put(b: *u8, off: i64, s: *u8) -> i64 {
95 let n: i64 = ms_slen(s)
96 var i: i64 = 0
97 while i < n { b[off + i] = s[i]; i = i + 1 }
98 return off + n
99}
100
101func ms_num(b: *u8, off: i64, v: i64) -> i64 {
102 let t: *u8 = sys_mmap(32)
103 var m: i64 = v
104 var k: i64 = 0
105 if m == 0 { t[0] = 48 as u8; k = 1 }
106 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
107 var o: i64 = off
108 var i: i64 = 0
109 while i < k { b[o] = t[k - 1 - i]; o = o + 1; i = i + 1 }
110 sys_munmap(t, 32)
111 return o
112}
113
114func main(argc: i64, argv: *i64) -> i64 {
115 var port: i64 = MS_PORT_DEFAULT
116 if argc >= 2 {
117 var v: i64 = 0
118 let a: *u8 = argv[1] as *u8
119 var i: i64 = 0
120 while a[i] != (0 as u8) { v = v * 10 + ((a[i] as i64) - 48); i = i + 1 }
121 if v > 0 { port = v }
122 }
123
124 let addr: *u8 = sys_mmap(64)
125 nx_http_server_addr_loopback(addr, port)
126 let vd: *i64 = (sys_mmap(8)) as *i64
127 let lfd: i64 = nx_http_server_listen(addr, MS_BACKLOG, vd)
128 if lfd < 0 {
129 sys_write(2, "[nx_math_serve] LISTEN-FAILED verdict=" as *u8, 38)
130 sys_write(2, nxs_verdict_name(vd[0]), ms_slen(nxs_verdict_name(vd[0])))
131 sys_write(2, "\n" as *u8, 1)
132 sys_exit(1)
133 return 1
134 }
135 sys_write(1, "[nx_math_serve] listening on loopback\n" as *u8, 38)
136
137 let req: *u8 = sys_mmap(MS_REQ_CAP)
138 let eq: *u8 = sys_mmap(MS_EQ_CAP)
139 let body: *u8 = sys_mmap(MP_CAP)
140 let resp: *u8 = sys_mmap(MP_CAP + MS_HDR_RESERVE)
141 let mth: *i64 = (sys_mmap(8)) as *i64
142 let poff: *i64 = (sys_mmap(8)) as *i64
143 let plen: *i64 = (sys_mmap(8)) as *i64
144 let clen: *i64 = (sys_mmap(8)) as *i64
145 let boff: *i64 = (sys_mmap(8)) as *i64
146 let rn: *i64 = (sys_mmap(8)) as *i64
147
148 var alive: i64 = 1
149 while alive == 1 {
150 let cfd: i64 = nx_http_server_accept_one(lfd, vd)
151 if cfd >= 0 {
152 let rr: i64 = nx_http_server_read_request(cfd, req, MS_REQ_CAP, mth, poff, plen, clen, boff, rn)
153 var eqn: i64 = 0
154 if rr == NXS_OK {
155 let d: i64 = ms_extract_eq(((req as i64) + poff[0]) as *u8, plen[0], eq, MS_EQ_CAP - 1)
156 if d > 0 { eqn = d }
157 }
158 eq[eqn] = 0 as u8
159
160 // THE SAME RENDERER THE STATIC BUILD USES, with whatever the visitor typed.
161 let n: i64 = mp_render(body, eq, eqn)
162 var o: i64 = 0
163 if n > 0 {
164 o = ms_put(resp, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8)
165 o = ms_num(resp, o, n)
166 o = ms_put(resp, o, "\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: strict-origin-when-cross-origin\r\nConnection: close\r\n\r\n" as *u8)
167 var i: i64 = 0
168 while i < n { resp[o + i] = body[i]; i = i + 1 }
169 o = o + n
170 } else {
171 // The renderer refused (buffer exhausted). Say so with a status, never a blank 200.
172 o = ms_put(resp, o, "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 44\r\nConnection: close\r\n\r\nthe page renderer refused: buffer exhausted\n" as *u8)
173 }
174 nx_http_server_send_response(cfd, resp, o)
175 }
176 }
177 sys_exit(0)
178 return 0
179}