code wiki / _hdl_build / nx_cms_analytics_gate.nx
nx_cms_analytics_gate.nx source
↩ module page · 278 lines · 12734 B
1// nx_cms_analytics_gate.nx -- CMS W5 GATE (re-runnable, evidence-driven): FIRST-PARTY ANALYTICS
2// (the Site Kit / Jetpack class, minus the third-party beacon). Proves: each public page view is
3// counted server-side; counts are exact; counts SURVIVE a daemon restart (additive, file-backed);
4// /admin/stats is session-only and shows the count; the served page carries NO third-party analytics
5// beacon (first-party by construction). Appends "CMSGATE row=nx_cms_analytics ... verdict=PASS|FAIL".
6// Exit 0 iff all rows pass. license_tier: ORIGINAL
7import "nx_cms_store.nx"
8import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
9import "nx_sha256.nx"
10import "nx_syscalls.nx"
11import "_hdl_build/nx_kill_portable.nx"
12
13const AN_PORT: i64 = 8089
14const AN_SITE: *u8 = "/tmp/_cms_an_site"
15
16func an_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 an_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
18func an_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var k: i64=0; while s[k]!=(0 as u8){dst[o]=s[k];o=o+1;k=k+1} return o }
19func an_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { var o: i64=off; var k: i64=0; while k<n{dst[o]=s[k];o=o+1;k=k+1} return o }
20func an_index(hay: *u8, n: i64, needle: *u8) -> i64 {
21 let nl: i64 = an_len(needle)
22 var i: i64 = 0
23 while i + nl <= n {
24 var q: i64 = 0
25 var ok: i64 = 1
26 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 }
27 if ok == 1 { return i }
28 i = i + 1
29 }
30 return 0 - 1
31}
32func an_has(hay: *u8, n: i64, needle: *u8) -> i64 { if an_index(hay, n, needle) >= 0 { return 1 } return 0 }
33
34func an_http(req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 {
35 let fd: i64 = sys_socket(2, 1, 0)
36 if fd < 0 { return 0 - 1 }
37 let addr: *u8 = sys_mmap(16)
38 addr[0] = 2 as u8; addr[1] = 0 as u8
39 addr[2] = ((AN_PORT >> 8) & 0xff) as u8
40 addr[3] = (AN_PORT & 0xff) as u8
41 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8
42 var zi: i64 = 8
43 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 }
44 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
45 var off: i64 = 0
46 var go: i64 = 1
47 while go == 1 { go = 0; if off < rl { let w: i64 = sys_write(fd, (req + off) as *u8, rl - off); if w > 0 { off = off + w; go = 1 } } }
48 sys_set_socket_timeout(fd, 10)
49 var total: i64 = 0
50 go = 1
51 while go == 1 { go = 0; if total < cap { let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total); if r > 0 { total = total + r; go = 1 } } }
52 sys_close(fd)
53 return total
54}
55func an_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 {
56 var o: i64 = an_cat(req, 0, "POST " as *u8)
57 o = an_cat(req, o, path)
58 o = an_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8)
59 if (cookie[0] as i64) != 0 { o = an_cat(req, o, "Cookie: nsess=" as *u8); o = an_catn(req, o, cookie, 32); o = an_cat(req, o, "\r\n" as *u8) }
60 o = an_cat(req, o, "Content-Length: " as *u8)
61 let bl: i64 = an_len(body)
62 let t: *u8 = sys_mmap(24)
63 var m: i64 = bl
64 var k: i64 = 0
65 if m == 0 { t[0] = 48 as u8; k = 1 }
66 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
67 var i: i64 = 0
68 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 }
69 o = an_cat(req, o, "\r\n\r\n" as *u8)
70 o = an_cat(req, o, body)
71 return o
72}
73func an_get(req: *u8, path: *u8, cookie: *u8) -> i64 {
74 var o: i64 = an_cat(req, 0, "GET " as *u8)
75 o = an_cat(req, o, path)
76 o = an_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8)
77 if (cookie[0] as i64) != 0 { o = an_cat(req, o, "Cookie: nsess=" as *u8); o = an_catn(req, o, cookie, 32); o = an_cat(req, o, "\r\n" as *u8) }
78 o = an_cat(req, o, "\r\n" as *u8)
79 return o
80}
81func an_row(id: i64, ok: i64, what: *u8) -> i64 {
82 an_w("ANROW " as *u8)
83 let d: *u8 = sys_mmap(8); d[0] = (48 + id/10) as u8; d[1] = (48 + id%10) as u8; d[2] = 32 as u8; d[3] = 0 as u8
84 an_w(d)
85 if ok == 1 { an_w("PASS " as *u8) }
86 if ok == 0 { an_w("FAIL " as *u8) }
87 an_w(what); an_w("\n" as *u8)
88 return ok
89}
90func an_mkdir(path: *u8, mode: i64) -> i64 { let nb: *i64 = sys_mmap(8) as *i64; nb[0] = 258; return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) }
91func an_writefile(path: *u8, buf: *u8, n: i64) -> i64 { let fd: i64 = sys_openat_wr(path, 0x1a4); if fd < 0 { return 0 } sys_write(fd, buf, n); sys_close(fd); return 1 }
92
93// CALLER-GAP FIX 2026-08-01 (seq1267 class, 11th cms instance): the runner emits
94// buildroot/_build/<t>.sov.elf under the no-/tmp doctrine while this gate probed /tmp.
95// The exec site lives in an_boot -- a DIFFERENT function from the availability probe in main --
96// so the path is re-derived here rather than smuggled across as a local (the compiler correctly
97// refused that in nx_cms_multisite_gate with UNRESOLVED identifier).
98func an_admin_path() -> *u8 {
99 let f: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8)
100 if f >= 0 { sys_close(f); return "buildroot/_build/nx_cms_admin.sov.elf" as *u8 }
101 return "/tmp/nx_cms_admin.sov.elf" as *u8
102}
103
104// boot the admin on (port,site); returns pid
105func an_boot(site: *u8, logp: *u8) -> i64 {
106 let pid: i64 = sys_fork()
107 if pid == 0 {
108 let lg: i64 = sys_openat_wr(logp, 0x1a4)
109 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) }
110 let argvv: *i64 = sys_mmap(8*6) as *i64
111 argvv[0] = an_admin_path() as i64
112 argvv[1] = "8089" as *u8 as i64
113 argvv[2] = site as i64
114 argvv[3] = "99" as *u8 as i64
115 argvv[4] = 0
116 let envp: *i64 = sys_mmap(16) as *i64; envp[0] = 0
117 sys_execve(an_admin_path(), argvv, envp)
118 sys_exit(127)
119 }
120 return pid
121}
122// poll until the daemon serves GET /
123func an_wait(req: *u8, resp: *u8) -> i64 {
124 var rn: i64 = 0 - 1
125 var tries: i64 = 0
126 while tries < 50 {
127 let rl: i64 = an_get(req, "/" as *u8, "" as *u8)
128 rn = an_http(req, rl, resp, 524287)
129 if rn > 0 { tries = 50 }
130 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 }
131 }
132 return rn
133}
134
135func main() -> i64 {
136 an_mkdir(AN_SITE, 0x1ed)
137 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_an_site/template.html" as *u8)
138 cst_copy("knowledge/cms/andelinwest/content.txt" as *u8, "/tmp/_cms_an_site/content.txt" as *u8)
139 sys_renameat("/tmp/_cms_an_site/draft.txt" as *u8, "/tmp/_cms_an_site/draft.stale" as *u8)
140 sys_renameat("/tmp/_cms_an_site/analytics.txt" as *u8, "/tmp/_cms_an_site/analytics.stale" as *u8)
141 let dig: *u8 = sys_mmap(32)
142 sha256_digest("an-gate-pw-1" as *u8, 12, dig)
143 let hexs: *u8 = sys_mmap(80)
144 let hxc: *u8 = "0123456789abcdef" as *u8
145 var i: i64 = 0
146 while i < 32 { hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15]; hexs[i*2+1] = hxc[(dig[i] as i64) & 15]; i = i + 1 }
147 an_writefile("/tmp/_cms_an_site/admin_pw.sha256" as *u8, hexs, 64)
148
149 // rebuild admin once
150 let bpid: i64 = sys_fork()
151 if bpid == 0 {
152 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
153 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
154 let bargv: *i64 = sys_mmap(32) as *i64
155 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64
156 bargv[1] = "nx_cms_admin" as *u8 as i64
157 bargv[2] = 0
158 let benvp: *i64 = sys_mmap(16) as *i64; benvp[0] = 0
159 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127)
160 }
161 let bst: *i64 = sys_mmap(16) as *i64
162 sys_wait4(bpid, bst, 0)
163 let bchk: i64 = sys_openat_rd(an_admin_path())
164 if bchk < 0 { an_w("CMSGATE row=nx_cms_analytics ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) }
165 sys_close(bchk)
166
167 let req: *u8 = sys_mmap(262144)
168 let resp: *u8 = sys_mmap(524288)
169 let nocookie: *u8 = sys_mmap(8)
170 nocookie[0] = 0 as u8
171 let sess: *u8 = sys_mmap(64)
172 sess[0] = 0 as u8
173
174 var pid: i64 = an_boot(AN_SITE as *u8, "/tmp/_cms_an_gate1.log" as *u8)
175 var rn: i64 = an_wait(req, resp) // this first GET / is hit #1
176 var pass: i64 = 0
177 var rows: i64 = 0
178 var ok: i64 = 0
179 if rn > 0 { ok = 1 }
180 rows = rows + 1; pass = pass + an_row(0, ok, "daemon serves the page" as *u8)
181
182 // A1: the served page carries NO third-party analytics beacon (first-party by construction)
183 ok = 0
184 if rn > 0 { if an_has(resp, rn, "google-analytics" as *u8) == 0 { if an_has(resp, rn, "googletagmanager" as *u8) == 0 { if an_has(resp, rn, "gtag(" as *u8) == 0 { ok = 1 } } } }
185 rows = rows + 1; pass = pass + an_row(1, ok, "no third-party analytics beacon in page" as *u8)
186
187 // A2: hit GET / four more times (total 5), then login + /admin/stats shows views = 5
188 var h: i64 = 0
189 while h < 4 {
190 let rlh: i64 = an_get(req, "/" as *u8, nocookie)
191 an_http(req, rlh, resp, 524287)
192 h = h + 1
193 }
194 var rl: i64 = an_post(req, "/admin/login" as *u8, nocookie, "pw=an-gate-pw-1" as *u8)
195 rn = an_http(req, rl, resp, 524287)
196 var have: i64 = 0
197 if rn > 0 { if an_has(resp, rn, "303" as *u8) == 1 {
198 let sp: i64 = an_index(resp, rn, "nsess=" as *u8)
199 if sp >= 0 { var m2: i64 = 0; while m2 < 32 { sess[m2] = resp[sp+6+m2]; m2 = m2 + 1 } sess[32] = 0 as u8; have = 1 }
200 } }
201 ok = 0
202 if have == 1 {
203 rl = an_get(req, "/admin/stats" as *u8, sess)
204 rn = an_http(req, rl, resp, 524287)
205 if rn > 0 { if an_has(resp, rn, "<td>/</td><td>5</td>" as *u8) == 1 { ok = 1 } }
206 }
207 rows = rows + 1; pass = pass + an_row(2, ok, "5 views counted exactly in stats" as *u8)
208
209 // A3: stats is session-only (unauth -> 403)
210 rl = an_get(req, "/admin/stats" as *u8, nocookie)
211 rn = an_http(req, rl, resp, 524287)
212 ok = 0
213 if rn > 0 { if an_has(resp, rn, "403" as *u8) == 1 { if an_has(resp, rn, "<td>5</td>" as *u8) == 0 { ok = 1 } } }
214 rows = rows + 1; pass = pass + an_row(3, ok, "stats refused without session 403" as *u8)
215
216 // A4: counts SURVIVE a restart (kill, reboot the daemon, stats still shows 5+)
217 nxk_kill(pid, 9)
218 let st4: *i64 = sys_mmap(16) as *i64
219 sys_wait4(pid, st4, 0)
220 pid = an_boot(AN_SITE as *u8, "/tmp/_cms_an_gate2.log" as *u8)
221 rn = an_wait(req, resp) // this GET / is hit #6
222 sess[0] = 0 as u8
223 rl = an_post(req, "/admin/login" as *u8, nocookie, "pw=an-gate-pw-1" as *u8)
224 rn = an_http(req, rl, resp, 524287)
225 ok = 0
226 if rn > 0 { if an_has(resp, rn, "303" as *u8) == 1 {
227 let sp2: i64 = an_index(resp, rn, "nsess=" as *u8)
228 if sp2 >= 0 {
229 var m3: i64 = 0
230 while m3 < 32 { sess[m3] = resp[sp2+6+m3]; m3 = m3 + 1 }
231 sess[32] = 0 as u8
232 rl = an_get(req, "/admin/stats" as *u8, sess)
233 rn = an_http(req, rl, resp, 524287)
234 // after restart the wait-poll added hit #6 -> count is 6
235 if rn > 0 { if an_has(resp, rn, "<td>/</td><td>6</td>" as *u8) == 1 { ok = 1 } }
236 }
237 } }
238 rows = rows + 1; pass = pass + an_row(4, ok, "counts survive restart (5 -> 6)" as *u8)
239
240 // ---- teardown ----
241 nxk_kill(pid, 9)
242 let stbuf: *i64 = sys_mmap(16) as *i64
243 sys_wait4(pid, stbuf, 0)
244
245 var verdict: *u8 = "FAIL" as *u8
246 if pass == rows { verdict = "PASS" as *u8 }
247 let line: *u8 = sys_mmap(512)
248 var lo: i64 = an_cat(line, 0, "CMSGATE row=nx_cms_analytics rows=" as *u8)
249 let t2: *u8 = sys_mmap(24)
250 var mm: i64 = rows
251 var kk: i64 = 0
252 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
253 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
254 var ii: i64 = 0
255 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
256 lo = an_cat(line, lo, " pass=" as *u8)
257 mm = pass; kk = 0
258 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
259 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
260 ii = 0
261 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
262 lo = an_cat(line, lo, " verdict=" as *u8)
263 lo = an_cat(line, lo, verdict)
264 lo = an_cat(line, lo, " epoch=" as *u8)
265 mm = sys_now_realtime_sec(); kk = 0
266 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
267 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
268 ii = 0
269 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
270 line[lo] = 10 as u8
271 lo = lo + 1
272 sys_write(1, line, lo)
273 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4)
274 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) }
275 if pass == rows { sys_exit(0) }
276 sys_exit(1)
277 return 1
278}