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}