code wiki / _hdl_build / nx_adnet_slot.nx
nx_adnet_slot.nx source
↩ module page · 300 lines · 11829 B
1// nx_adnet_slot.nx -- LIB: the UNIVERSAL site-wide ad slot for the sovereign ad network (nx_adnet core).
2// One labeled, first-party, zero-third-party-JS banner slot rendered per page (injected by hr_serve3_slot),
3// plus the first-party /ad/click/<id> redirect responder. PRIVACY BY CONSTRUCTION: no cookie, no visitor id,
4// per-AD counters only; rotation is TIME-based (no per-user frequency state -- per-user tracking is REFUSED).
5// Inventory conf rows (nx_adnet contract): id<TAB>advertiser<TAB>img<TAB>clickurl<TAB>section<TAB>weight
6// plus directive rows: @host<TAB><allowed-vhost> (FAIL-CLOSED: no @host row for the vhost -> no slot ever).
7// All parsing here is BOUNDED (rule 12 defensive-at-boundaries); a row that fails validation is DROPPED,
8// never served. Section falls back to rows with section 'any'. license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "_hdl_build/nx_adnet.nx"
11
12func aslot_lc(c: u8) -> u8 { if c >= (65 as u8) { if c <= (90 as u8) { return (c + (32 as u8)) } } return c }
13
14func aslot_starts(s: *u8, pre: *u8) -> i64 {
15 var i: i64 = 0
16 while pre[i] != (0 as u8) { if s[i] != pre[i] { return 0 } i = i + 1 }
17 return 1
18}
19
20// BOUNDED field extract (nx_adnet ad_field has no cap -- this one refuses to overflow out).
21func aslot_field_b(row: *u8, rlen: i64, idx: i64, out: *u8, cap: i64) -> i64 {
22 var fs: i64 = 0
23 var f: i64 = 0
24 while f < idx { let t: i64 = ad_tab(row, fs, rlen); fs = t + 1; f = f + 1 }
25 let fe: i64 = ad_tab(row, fs, rlen)
26 var o: i64 = 0
27 var k: i64 = fs
28 while k < fe { if o < cap - 1 { out[o] = row[k]; o = o + 1 } k = k + 1 }
29 out[o] = 0 as u8
30 return o
31}
32
33// derive the section token from the request path: first path segment, lowercased ("/": home).
34func aslot_section_of(path: *u8, pn: i64, out: *u8, cap: i64) -> i64 {
35 var i: i64 = 0
36 if pn > 0 { if path[0] == (47 as u8) { i = 1 } }
37 var j: i64 = 0
38 while i < pn {
39 let c: u8 = path[i]
40 if c == (47 as u8) { break }
41 if c == (46 as u8) { break }
42 if c == (63 as u8) { break }
43 if j < cap - 1 { out[j] = aslot_lc(c); j = j + 1 }
44 i = i + 1
45 }
46 if j == 0 {
47 let h: *u8 = "home" as *u8
48 var k: i64 = 0
49 while h[k] != (0 as u8) { if k < cap - 1 { out[k] = h[k] } k = k + 1 }
50 j = k
51 }
52 out[j] = 0 as u8
53 return j
54}
55
56// directive scan: is this vhost explicitly ad-enabled? FAIL-CLOSED (no @host rows -> 0).
57func aslot_host_ok(inv: *u8, ilen: i64, host: *u8) -> i64 {
58 let f0: *u8 = sys_mmap(128)
59 let f1: *u8 = sys_mmap(256)
60 var ls: i64 = 0
61 while ls < ilen {
62 let le: i64 = ad_eol(inv, ls, ilen)
63 if le > ls {
64 let row: *u8 = ((inv as i64) + ls) as *u8
65 aslot_field_b(row, le - ls, 0, f0, 128)
66 if ad_streq(f0, "@host" as *u8) == 1 {
67 aslot_field_b(row, le - ls, 1, f1, 256)
68 if ad_streq(f1, host) == 1 { return 1 }
69 }
70 }
71 ls = le + 1
72 }
73 return 0
74}
75
76// id charset: [A-Za-z0-9_-] only, 1..64 chars.
77func aslot_id_ok(id: *u8) -> i64 {
78 var i: i64 = 0
79 while id[i] != (0 as u8) {
80 let c: u8 = id[i]
81 var ok: i64 = 0
82 if c >= (97 as u8) { if c <= (122 as u8) { ok = 1 } }
83 if c >= (65 as u8) { if c <= (90 as u8) { ok = 1 } }
84 if c >= (48 as u8) { if c <= (57 as u8) { ok = 1 } }
85 if c == (95 as u8) { ok = 1 }
86 if c == (45 as u8) { ok = 1 }
87 if ok == 0 { return 0 }
88 i = i + 1
89 if i > 64 { return 0 }
90 }
91 if i == 0 { return 0 }
92 return 1
93}
94
95// forbidden-byte scan shared by url validators: quote/angle/space/apostrophe kill the row.
96func aslot_url_chars_ok(u: *u8) -> i64 {
97 var i: i64 = 0
98 while u[i] != (0 as u8) {
99 let c: u8 = u[i]
100 if c == (34 as u8) { return 0 }
101 if c == (39 as u8) { return 0 }
102 if c == (60 as u8) { return 0 }
103 if c == (62 as u8) { return 0 }
104 if c == (32 as u8) { return 0 }
105 i = i + 1
106 if i > 900 { return 0 }
107 }
108 if i == 0 { return 0 }
109 return 1
110}
111
112// creative img: FIRST-PARTY ONLY -- absolute path, not scheme-relative, clean charset.
113func aslot_img_ok(u: *u8) -> i64 {
114 if u[0] != (47 as u8) { return 0 }
115 if u[1] == (47 as u8) { return 0 }
116 return aslot_url_chars_ok(u)
117}
118
119// click target: first-party path OR explicit http(s). javascript:/data: excluded by construction.
120func aslot_click_ok(u: *u8) -> i64 {
121 var sch: i64 = 0
122 if u[0] == (47 as u8) { if u[1] != (47 as u8) { sch = 1 } }
123 if aslot_starts(u, "https://" as *u8) == 1 { sch = 1 }
124 if aslot_starts(u, "http://" as *u8) == 1 { sch = 1 }
125 if sch == 0 { return 0 }
126 return aslot_url_chars_ok(u)
127}
128
129// HTML-escape append: < > & " escaped; apostrophe replaced with space (attribute-safe, no entity).
130func aslot_cat_esc(dst: *u8, off: i64, s: *u8, cap: i64) -> i64 {
131 var o: i64 = off
132 var i: i64 = 0
133 while s[i] != (0 as u8) {
134 if o + 8 >= cap { return o }
135 let c: u8 = s[i]
136 var lit: i64 = 1
137 if c == (60 as u8) { o = ad_cat(dst, o, "<" as *u8); lit = 0 }
138 if c == (62 as u8) { if lit == 1 { o = ad_cat(dst, o, ">" as *u8); lit = 0 } }
139 if c == (38 as u8) { if lit == 1 { o = ad_cat(dst, o, "&" as *u8); lit = 0 } }
140 if c == (34 as u8) { if lit == 1 { o = ad_cat(dst, o, """ as *u8); lit = 0 } }
141 if c == (39 as u8) { if lit == 1 { dst[o] = 32 as u8; o = o + 1; lit = 0 } }
142 if lit == 1 { dst[o] = c; o = o + 1 }
143 i = i + 1
144 }
145 return o
146}
147
148// count inventory rows whose SECTION (field 4) == section, skipping directive/oversize rows.
149func aslot_count(inv: *u8, ilen: i64, section: *u8) -> i64 {
150 let tmp: *u8 = sys_mmap(128)
151 var cnt: i64 = 0
152 var ls: i64 = 0
153 while ls < ilen {
154 let le: i64 = ad_eol(inv, ls, ilen)
155 if le > ls {
156 if le - ls < 1536 {
157 let row: *u8 = ((inv as i64) + ls) as *u8
158 if row[0] != (64 as u8) {
159 aslot_field_b(row, le - ls, 4, tmp, 128)
160 if ad_streq(tmp, section) == 1 { cnt = cnt + 1 }
161 }
162 }
163 }
164 ls = le + 1
165 }
166 return cnt
167}
168
169// BOUNDED rotate-pick: copy the (rot mod matches)-th section row into out (cap'd). 1 = found.
170func aslot_pick_b(inv: *u8, ilen: i64, section: *u8, rot: i64, out: *u8, cap: i64) -> i64 {
171 let m: i64 = aslot_count(inv, ilen, section)
172 if m == 0 { out[0] = 0 as u8; return 0 }
173 var r: i64 = rot
174 if r < 0 { r = 0 - r }
175 let target: i64 = r % m
176 let tmp: *u8 = sys_mmap(128)
177 var seen: i64 = 0
178 var ls: i64 = 0
179 while ls < ilen {
180 let le: i64 = ad_eol(inv, ls, ilen)
181 if le > ls {
182 if le - ls < 1536 {
183 let row: *u8 = ((inv as i64) + ls) as *u8
184 if row[0] != (64 as u8) {
185 aslot_field_b(row, le - ls, 4, tmp, 128)
186 if ad_streq(tmp, section) == 1 {
187 if seen == target {
188 var o: i64 = 0
189 var k: i64 = ls
190 while k < le { if o < cap - 1 { out[o] = inv[k]; o = o + 1 } k = k + 1 }
191 out[o] = 0 as u8
192 return 1
193 }
194 seen = seen + 1
195 }
196 }
197 }
198 }
199 ls = le + 1
200 }
201 return 0
202}
203
204// MAIN: build the labeled slot html for this request. Returns bytes written (0 = no slot).
205// idout (cap>=128) receives the picked ad id for the served-impression log. cap must be >= 6144.
206func aslot_html(inv: *u8, ilen: i64, path: *u8, pn: i64, rot: i64, out: *u8, cap: i64, idout: *u8) -> i64 {
207 idout[0] = 0 as u8
208 if cap < 6144 { return 0 }
209 if ilen <= 0 { return 0 }
210 let sec: *u8 = sys_mmap(128)
211 aslot_section_of(path, pn, sec, 128)
212 let row: *u8 = sys_mmap(2048)
213 var got: i64 = aslot_pick_b(inv, ilen, sec, rot, row, 2048)
214 if got == 0 { got = aslot_pick_b(inv, ilen, "any" as *u8, rot, row, 2048) }
215 if got == 0 { return 0 }
216 let rlen: i64 = ad_slen(row)
217 let id: *u8 = sys_mmap(128)
218 let adv: *u8 = sys_mmap(256)
219 let img: *u8 = sys_mmap(1024)
220 aslot_field_b(row, rlen, 0, id, 128)
221 aslot_field_b(row, rlen, 1, adv, 256)
222 aslot_field_b(row, rlen, 2, img, 1024)
223 let clk: *u8 = sys_mmap(1024)
224 aslot_field_b(row, rlen, 3, clk, 1024)
225 if aslot_id_ok(id) == 0 { return 0 }
226 if aslot_img_ok(img) == 0 { return 0 }
227 if aslot_click_ok(clk) == 0 { return 0 }
228 var o: i64 = 0
229 o = ad_cat(out, o, "<aside class=\"nx-ad-slot\" aria-label=\"Advertisement\" style=\"grid-column:1/-1;max-width:768px;margin:18px auto 10px;padding:4px;text-align:center\">" as *u8)
230 o = ad_cat(out, o, "<div style=\"font:600 10px system-ui,sans-serif;color:gray;letter-spacing:.6px;margin-bottom:3px\">AD · BROUGHT TO YOU BY " as *u8)
231 o = aslot_cat_esc(out, o, adv, cap)
232 o = ad_cat(out, o, "</div><a rel=\"noopener nofollow sponsored\" href=\"/ad/click/" as *u8)
233 o = ad_cat(out, o, id)
234 o = ad_cat(out, o, "\"><img src=\"" as *u8)
235 o = ad_cat(out, o, img)
236 o = ad_cat(out, o, "\" alt=\"" as *u8)
237 o = aslot_cat_esc(out, o, adv, cap)
238 o = ad_cat(out, o, "\" style=\"max-width:100%;height:auto;border:0\" width=\"728\" height=\"90\" loading=\"lazy\"></a></aside>" as *u8)
239 if o + 2 >= cap { idout[0] = 0 as u8; return 0 }
240 out[o] = 0 as u8
241 var k2: i64 = 0
242 while id[k2] != (0 as u8) { idout[k2] = id[k2]; k2 = k2 + 1 }
243 idout[k2] = 0 as u8
244 return o
245}
246
247// BOUNDED click-target resolve (nx_adnet contract field 3), validated separately by the caller.
248func aslot_click_target_b(inv: *u8, ilen: i64, ad_id: *u8, out: *u8, cap: i64) -> i64 {
249 let tmp: *u8 = sys_mmap(128)
250 var ls: i64 = 0
251 while ls < ilen {
252 let le: i64 = ad_eol(inv, ls, ilen)
253 if le > ls {
254 if le - ls < 1536 {
255 let row: *u8 = ((inv as i64) + ls) as *u8
256 if row[0] != (64 as u8) {
257 aslot_field_b(row, le - ls, 0, tmp, 128)
258 if ad_streq(tmp, ad_id) == 1 { aslot_field_b(row, le - ls, 3, out, cap); return 1 }
259 }
260 }
261 }
262 ls = le + 1
263 }
264 out[0] = 0 as u8
265 return 0
266}
267
268// If path is /ad/click/<id>: emit a FULL first-party 302 response into out; idout <- id when a real
269// target resolved (so the caller logs the click). Unknown/invalid id -> 302 to / (fail-closed, no 500).
270// Returns response bytes, or 0 when the path is not an ad click (caller continues its cascade).
271func aslot_click_resp(inv: *u8, ilen: i64, path: *u8, pn: i64, out: *u8, cap: i64, idout: *u8) -> i64 {
272 idout[0] = 0 as u8
273 if aslot_starts(path, "/ad/click/" as *u8) == 0 { return 0 }
274 if cap < 2048 { return 0 }
275 let id: *u8 = sys_mmap(128)
276 var i: i64 = 10
277 var j: i64 = 0
278 while i < pn { if j < 100 { id[j] = path[i]; j = j + 1 } i = i + 1 }
279 id[j] = 0 as u8
280 let url: *u8 = sys_mmap(1024)
281 var tgt: *u8 = "/" as *u8
282 if aslot_id_ok(id) == 1 {
283 if ilen > 0 {
284 let found: i64 = aslot_click_target_b(inv, ilen, id, url, 1024)
285 if found == 1 {
286 if aslot_click_ok(url) == 1 {
287 tgt = url
288 var k: i64 = 0
289 while id[k] != (0 as u8) { idout[k] = id[k]; k = k + 1 }
290 idout[k] = 0 as u8
291 }
292 }
293 }
294 }
295 var o: i64 = 0
296 o = ad_cat(out, o, "HTTP/1.1 302 Found\r\nLocation: " as *u8)
297 o = ad_cat(out, o, tgt)
298 o = ad_cat(out, o, "\r\nContent-Length: 0\r\nConnection: keep-alive\r\nCache-Control: no-store\r\nX-Robots-Tag: noindex\r\n\r\n" as *u8)
299 return o
300}