code wiki / (root) / nx_math_serve.nx

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}