code wiki / _hdl_build / nx_content_publish_gate.nx
nx_content_publish_gate.nx source
↩ module page · 162 lines · 9125 B
1// nx_content_publish_gate.nx -- prove the OFF-LAN static-content publish rung on the mgmt API (2026-07-06):
2// the /synth page + STL/PNG assets can be STAGED (/api/upload) and PROMOTED (/api/promote_content) over the
3// authenticated public edge, never-brick, without SSH/LAN. In-process referee (no socket): crafts request BYTES
4// and feeds the pure handlers. Auth is the SAME ma_level_of gate as /api/deploy (proven in nx_mgmt_api_gate T6),
5// so this focuses on the NEW logic:
6// T1 md_content_target_ok fail-closed table (valid .stl/.html; reject traversal/subdir/wrong-prefix/bad-ext/service)
7// T2 /api/upload with a content target -> stages <target>.new (byte-exact)
8// T3 /api/promote_content -> live file == body, prev=0 (no prior live)
9// T4 second upload+promote -> live == new body AND .prev == old body (never-brick backup)
10// T5 NEG /api/promote_content without confirm=yes -> 400, live UNCHANGED
11// Self-cleaning (unlinks the gate_probe.* it writes under sites/nishifamily/synth/). license_tier: ORIGINAL
12// expect_exit: 0
13import "nx_mgmt_api.nx"
14import "nx_syscalls.nx"
15
16func w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
17func pn(v: i64) -> i64 {
18 let b: *u8 = sys_mmap(32) as *u8
19 var x: i64 = v; var neg: i64 = 0
20 if x < 0 { neg = 1; x = 0 - x }
21 var i: i64 = 31
22 if x == 0 { b[i] = 48 as u8; i = i - 1 }
23 while x > 0 { b[i] = (48 + x % 10) as u8; x = x / 10; i = i - 1 }
24 if neg == 1 { b[i] = 45 as u8; i = i - 1 }
25 sys_write(1, (b as i64 + i + 1) as *u8, 31 - i)
26 return 0
27}
28func glen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
29func gcat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){d[o+i]=s[i]; i=i+1} return o+i }
30func gcatn(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; while i<n {d[o+i]=s[i]; i=i+1} return o+n }
31func gcontains(hay: *u8, n: i64, needle: *u8) -> i64 {
32 let nn: i64 = glen(needle)
33 var i: i64 = 0
34 while i + nn <= n {
35 var m: i64 = 1; var j: i64 = 0
36 while j < nn { if (hay[i+j] as i64) != (needle[j] as i64) { m = 0; j = nn } else { j = j + 1 } }
37 if m == 1 { return 1 }
38 i = i + 1
39 }
40 return 0
41}
42// build "POST <line> HTTP/1.1\r\n...\r\n\r\n<body>" into dst; returns total length.
43func mkreq(dst: *u8, line: *u8, body: *u8) -> i64 {
44 let bn: i64 = glen(body)
45 var o: i64 = gcat(dst, 0, line)
46 o = gcat(dst, o, " HTTP/1.1\r\nHost: x\r\nContent-Type: application/octet-stream\r\nContent-Length: " as *u8)
47 // itoa bn
48 let t: *u8 = sys_mmap(24); var m: i64 = bn; var k: i64 = 0
49 if m == 0 { t[0]=48 as u8; k=1 }
50 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
51 var z: i64 = 0
52 while z < k { dst[o]=t[k-1-z]; o=o+1; z=z+1 }
53 o = gcat(dst, o, "\r\n\r\n" as *u8)
54 o = gcatn(dst, o, body, bn)
55 return o
56}
57// read a file; return 1 iff its bytes == want (NUL-terminated).
58func file_eq(path: *u8, want: *u8) -> i64 {
59 let szp: *i64 = sys_mmap(16) as *i64
60 let b: *u8 = sys_read_file(path, szp)
61 if (b as i64) == 0 { return 0 }
62 let n: i64 = szp[0]
63 let wn: i64 = glen(want)
64 if n != wn { return 0 }
65 var i: i64 = 0
66 while i < n { if (b[i] as i64) != (want[i] as i64) { return 0 } i = i + 1 }
67 return 1
68}
69func vcase(nm: *u8, expect: i64, fails: *i64) -> i64 {
70 let got: i64 = md_content_target_ok(nm, 0, glen(nm))
71 if got == expect { w(" ok " as *u8) } else { w(" BAD " as *u8); fails[0] = fails[0] + 1 }
72 w(nm); w(" ->" as *u8); pn(got); w("\n" as *u8)
73 return 0
74}
75
76const PROBE_A: *u8 = "<h1>PROBE-A synth</h1>" as *u8
77const PROBE_B: *u8 = "<h1>PROBE-B synth updated</h1>" as *u8
78const TGT: *u8 = "sites/nishifamily/synth/gate_probe.html" as *u8
79const TGT_NEW: *u8 = "sites/nishifamily/synth/gate_probe.html.new" as *u8
80const TGT_PREV: *u8 = "sites/nishifamily/synth/gate_probe.html.prev" as *u8
81
82func main() -> i64 {
83 w("=== nx_content_publish_gate -- publish-from-anywhere for STATIC site files (never-brick) ===\n" as *u8)
84 // run the file IO under a WSL-NATIVE dir: on /mnt/c (9p/DrvFs) a just-written file can be unreadable for
85 // a beat and renames half-visible -> false REDs. Production runs these handlers on the NAS ext4; the gate
86 // must test the LOGIC, not DrvFs. (T2-T4 flaked on /mnt/c; green from /tmp.)
87 sys_mkdir("/tmp/nxcontent_gate" as *u8, 0x1ed)
88 sys_chdir("/tmp/nxcontent_gate" as *u8)
89 sys_mkdir("sites" as *u8, 0x1ed)
90 sys_mkdir("sites/nishifamily" as *u8, 0x1ed)
91 sys_mkdir("sites/nishifamily/synth" as *u8, 0x1ed)
92 fio_unlink(TGT); fio_unlink(TGT_NEW); fio_unlink(TGT_PREV)
93 let out: *u8 = sys_mmap(262144)
94 let req: *u8 = sys_mmap(262144)
95 var fails: i64 = 0
96
97 // T1 validator table
98 w("T1 content-target validator (fail-closed):\n" as *u8)
99 let fp: *i64 = sys_mmap(8) as *i64; fp[0] = 0
100 vcase("sites/nishifamily/synth/wolf.stl" as *u8, 1, fp)
101 vcase("sites/nishifamily/synth/index.html" as *u8, 1, fp)
102 vcase("sites/nishifamily/synth/variants.png" as *u8, 1, fp)
103 vcase("sites/nishifamily/synth/../secret.html" as *u8, 0, fp) // leading '.' basename -> reject
104 vcase("sites/nishifamily/synth/sub/x.png" as *u8, 0, fp) // embedded '/' -> reject
105 vcase("sites/nishifamily/gallery/x.png" as *u8, 0, fp) // wrong prefix -> reject
106 vcase("sites/nishifamily/synth/evil.sh" as *u8, 0, fp) // bad extension -> reject
107 vcase("nx_mgmt_api.elf" as *u8, 0, fp) // service binary -> reject (not content)
108 // swgpu prefix row (2026-07-06 extension: the software-GPU showcase publishes off-LAN too)
109 vcase("sites/nishifamily/swgpu/index.html" as *u8, 1, fp)
110 vcase("sites/nishifamily/swgpu/../x.html" as *u8, 0, fp) // traversal in swgpu ns -> reject
111 vcase("sites/nishifamily/swgpux/index.html" as *u8, 0, fp) // prefix must match the FULL dir row
112 if fp[0] == 0 { w(" T1 PASS all 11 validator cases correct\n" as *u8) } else { fails = fails + 1; w(" T1 FAIL " as *u8); pn(fp[0]); w(" wrong\n" as *u8) }
113
114 // T2 upload (stage .new)
115 let n2: i64 = mkreq(req, "POST /api/upload?target=sites/nishifamily/synth/gate_probe.html&seq=0&final=1" as *u8, PROBE_A)
116 let r2: i64 = ma_do_upload(req, n2, out)
117 var t2: i64 = 1
118 if gcontains(out, r2, "\"final\":1" as *u8) == 0 { t2 = 0 }
119 if gcontains(out, r2, "200 OK" as *u8) == 0 { t2 = 0 }
120 if file_eq(TGT_NEW, PROBE_A) == 0 { t2 = 0 }
121 if t2 == 1 { w("T2 PASS /api/upload staged sites/nishifamily/synth/gate_probe.html.new (byte-exact)\n" as *u8) }
122 else { fails = fails + 1; w("T2 FAIL upload/stage\n" as *u8); sys_write(1, out, r2); w("\n" as *u8) }
123
124 // T3 promote (no prior live)
125 let n3: i64 = mkreq(req, "POST /api/promote_content" as *u8, "target=sites/nishifamily/synth/gate_probe.html&confirm=yes" as *u8)
126 let r3: i64 = ma_do_promote_content(req, n3, out)
127 var t3: i64 = 1
128 if gcontains(out, r3, "PROMOTE_CONTENT" as *u8) == 0 { t3 = 0 }
129 if gcontains(out, r3, "\"prev\":0" as *u8) == 0 { t3 = 0 }
130 if file_eq(TGT, PROBE_A) == 0 { t3 = 0 }
131 if t3 == 1 { w("T3 PASS /api/promote_content -> live == PROBE-A, prev=0\n" as *u8) }
132 else { fails = fails + 1; w("T3 FAIL promote\n" as *u8); sys_write(1, out, r3); w("\n" as *u8) }
133
134 // T4 second upload + promote (never-brick backup: .prev == old, live == new)
135 let n4: i64 = mkreq(req, "POST /api/upload?target=sites/nishifamily/synth/gate_probe.html&seq=0&final=1" as *u8, PROBE_B)
136 ma_do_upload(req, n4, out)
137 let n4b: i64 = mkreq(req, "POST /api/promote_content" as *u8, "target=sites/nishifamily/synth/gate_probe.html&confirm=yes" as *u8)
138 let r4: i64 = ma_do_promote_content(req, n4b, out)
139 var t4: i64 = 1
140 if gcontains(out, r4, "\"prev\":1" as *u8) == 0 { t4 = 0 }
141 if file_eq(TGT, PROBE_B) == 0 { t4 = 0 }
142 if file_eq(TGT_PREV, PROBE_A) == 0 { t4 = 0 }
143 if t4 == 1 { w("T4 PASS re-promote -> live == PROBE-B, .prev == PROBE-A (never-brick backup)\n" as *u8) }
144 else { fails = fails + 1; w("T4 FAIL backup\n" as *u8); sys_write(1, out, r4); w("\n" as *u8) }
145
146 // T5 NEG promote without confirm -> 400, live unchanged
147 let n5: i64 = mkreq(req, "POST /api/promote_content" as *u8, "target=sites/nishifamily/synth/gate_probe.html" as *u8)
148 let r5: i64 = ma_do_promote_content(req, n5, out)
149 var t5: i64 = 1
150 if gcontains(out, r5, "400" as *u8) == 0 { t5 = 0 }
151 if gcontains(out, r5, "confirm=yes" as *u8) == 0 { t5 = 0 }
152 if file_eq(TGT, PROBE_B) == 0 { t5 = 0 } // still PROBE-B
153 if t5 == 1 { w("T5 PASS NEG no-confirm -> 400, live unchanged\n" as *u8) }
154 else { fails = fails + 1; w("T5 FAIL no-confirm guard\n" as *u8) }
155
156 // cleanup
157 fio_unlink(TGT); fio_unlink(TGT_NEW); fio_unlink(TGT_PREV)
158
159 if fails == 0 { w("CONTENT-PUBLISH-GATE 5/5 GREEN -- static site files stage+promote over the authenticated API, never-brick, no SSH/LAN\n" as *u8); sys_exit(0); return 0 }
160 w("CONTENT-PUBLISH-GATE RED fails=" as *u8); pn(fails); w("\n" as *u8)
161 sys_exit(1); return 1
162}