code wiki / _hdl_build / nx_ad_banner_gated_daemon.nx
nx_ad_banner_gated_daemon.nx source
↩ module page · 107 lines · 10162 B
1// nx_ad_banner_gated_daemon.nx -- the BANNER BUILDER served BEHIND THE OPAQUE FAMILY GATE. The builder form
2// page loads, but GENERATION (POST /ad/banner) is gated: it requires a valid no-cookie OPAQUE family session
3// (X-Nishi-Session header, validated via olg_whoami against the SAME realm/keys as /login). A first-party
4// inline script attaches the session token (sessionStorage 'nsess', set by the login page) on submit and
5// injects the returned banner -- the banner OUTPUT stays 0-third-party-JS. Unauthenticated -> 401 + a login
6// hint, so a non-family visitor can see the tool but cannot generate. The clean live wiring (a /ad proxy route
7// on sites.elf -> this daemon) is handed to the publisher/hosting. argv: [1]=port [2]=keys [3]=store
8// [4]=argon2_m(opt). Composes nx_opaque_login (session) + nx_ad_banner_builder (abb_render). license_tier: ORIGINAL
9import "nx_opaque_login.nx" // olg_ctx_setup / olg_whoami / NxAuthContext / NX_MAUTH_OK
10import "nx_ad_banner_builder.nx" // abb_render
11import "nx_syscalls.nx"
12const GBB_MAGIC_262144: i64 = 262144
13const GBB_MAGIC_65536: i64 = 65536
14const GBB_MAGIC_65535: i64 = 65535
15const GBB_MAGIC_1024: i64 = 1024
16const GBB_MAGIC_32768: i64 = 32768
17
18// The builder page: public shell + first-party session-aware generate (no third-party scripts).
19const GBB_PAGE: *u8 = "<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Nishi Banner Builder</title><style>body{font-family:system-ui,sans-serif;max-width:760px;margin:4vh auto;padding:0 18px;color:#16202e}h1{font-size:1.3rem}form{display:grid;gap:10px}label{display:grid;gap:3px;font-size:.85rem;color:#445}input,select{padding:8px;border:1px solid #b9c2d6;border-radius:6px;font-size:.95rem}button{padding:10px 18px;background:#0b2545;color:#fff;border:0;border-radius:6px;font-size:1rem;cursor:pointer;justify-self:start}.prev{margin:18px 0;padding:20px;background:#eef2f7;border-radius:10px;display:flex;justify-content:center;min-height:60px}textarea{width:100%;min-height:90px;font-family:ui-monospace,monospace;font-size:.8rem;border:1px solid #b9c2d6;border-radius:6px;padding:8px;box-sizing:border-box}.note{color:#667;font-size:.9rem}</style></head><body><h1>Nishi Banner Builder</h1><p class='note'>Generate a static or animated banner for the one-ad-per-page slot. No third-party scripts, no trackers, no external images — ever. Generating requires a Nishi Family sign-in.</p><form id='f'><label>Business / sponsor<input name='business' required></label><label>Headline<input name='headline'></label><label>Body line<input name='body'></label><label>Call-to-action<input name='cta'></label><label>Click link (https:// or /path)<input name='link'></label><label>Size<select name='size'><option>rectangle 300x250</option><option>leaderboard 728x90</option><option>mobile 320x50</option><option>skyscraper 160x600</option><option>halfpage 300x600</option><option>billboard 970x250</option></select></label><label>Theme<select name='theme'><option>light</option><option>dark</option><option>warm</option></select></label><label>Kind<select name='kind'><option>static</option><option>dynamic (animated)</option></select></label><button type='submit'>Generate banner</button></form><h2 style='font-size:1.05rem'>Preview</h2><div class='prev' id='prev'>Fill the form and Generate.</div><h2 style='font-size:1.05rem'>Banner HTML (copy into the ad slot)</h2><textarea id='out' readonly></textarea><script>var f=document.getElementById('f'),pv=document.getElementById('prev'),ot=document.getElementById('out');f.addEventListener('submit',function(e){e.preventDefault();var b=new URLSearchParams(new FormData(f)).toString();pv.textContent='Generating\\u2026';fetch('/ad/banner',{method:'POST',headers:{'X-Nishi-Session':sessionStorage.getItem('nsess')||'','Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(r.status===401){pv.innerHTML='Please <a href=\"/login\">sign in</a> as Nishi Family to generate.';return null}return r.text()}).then(function(t){if(t){pv.innerHTML=t;ot.value=t}}).catch(function(err){pv.textContent='error: '+err})});</script></body></html>"
20
21func gbb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
22func gbb_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c < 48 { return v } if c > 57 { return v } v = v * 10 + (c - 48); i = i + 1 } return v }
23func gbb_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o }
24func gbb_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64 = off; var i: i64 = 0; while i < n { dst[o] = src[i]; o = o + 1; i = i + 1 } return o }
25func gbb_itoa(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8 = sys_mmap(28); var m: i64 = v; var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var o: i64 = off; var q: i64 = k - 1; while q >= 0 { dst[o] = t[q]; o = o + 1; q = q - 1 } return o }
26func gbb_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { if nl == 0 { return 0 } var i: i64 = 0; while i + nl <= n { var j: i64 = 0; var ok: i64 = 1; while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } } if ok == 1 { return i } i = i + 1 } return 0 - 1 }
27func gbb_starts(buf: *u8, n: i64, pre: *u8) -> i64 { var i: i64 = 0; while pre[i] != (0 as u8) { if i >= n { return 0 } if buf[i] != pre[i] { return 0 } i = i + 1 } return 1 }
28// extract a request header value (req[0..hend]) for `name` (incl trailing ':'), trimming a leading space.
29func gbb_hdr_val(req: *u8, hend: i64, name: *u8, nl: i64, out: *u8, cap: i64) -> i64 {
30 let p: i64 = gbb_find(req, hend, name, nl)
31 if p < 0 { out[0] = 0 as u8; return 0 }
32 var i: i64 = p + nl
33 if i < hend { if req[i] == (32 as u8) { i = i + 1 } }
34 var o: i64 = 0
35 while i < hend { let c: u8 = req[i]; if c == (13 as u8) { i = hend } else { if c == (10 as u8) { i = hend } else { if o < cap - 1 { out[o] = c; o = o + 1 } i = i + 1 } } }
36 out[o] = 0 as u8; return o
37}
38func gbb_send(cfd: i64, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
39 let buf: *u8 = sys_mmap(GBB_MAGIC_262144); var o: i64 = 0
40 o = gbb_cat(buf, o, "HTTP/1.1 " as *u8); o = gbb_cat(buf, o, status)
41 o = gbb_cat(buf, o, "\r\nContent-Type: " as *u8); o = gbb_cat(buf, o, ctype)
42 o = gbb_cat(buf, o, "\r\nContent-Length: " as *u8); o = gbb_itoa(buf, o, blen)
43 o = gbb_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\nReferrer-Policy: no-referrer\r\n\r\n" as *u8)
44 o = gbb_catb(buf, o, body, blen)
45 sys_write(cfd, buf, o); return 0
46}
47
48func main(argc: i64, argv: *i64) -> i64 {
49 if argc < 4 { sys_write(1, "usage: nx_ad_banner_gated_daemon <port> <keys> <store> [argon2_m]\n" as *u8, 64); sys_exit(2); return 2 }
50 let port: i64 = gbb_atoi(argv[1] as *u8)
51 let keys: *u8 = argv[2] as *u8
52 let store: *u8 = argv[3] as *u8
53 var m_cost: i64 = GBB_MAGIC_65536
54 if argc > 4 { m_cost = gbb_atoi(argv[4] as *u8) }
55
56 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
57 if olg_ctx_setup(ctx, keys, store, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, m_cost, 3, 4) != 0 {
58 sys_write(1, "CTX-INIT-FAIL\n" as *u8, 14); sys_exit(1); return 1
59 }
60
61 let addr: *u8 = sys_mmap(16)
62 addr[0] = 2 as u8; addr[1] = 0 as u8; addr[2] = ((port >> 8) & 0xff) as u8; addr[3] = (port & 0xff) as u8
63 var zi: i64 = 4; while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 }
64 let lfd: i64 = sys_socket(2, 1, 0)
65 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
66 let optv: *u8 = sys_mmap(4); optv[0] = 1 as u8
67 sys_setsockopt(lfd, 1, 2, optv, 4)
68 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 }
69 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
70 sys_write(1, "BANNER-BUILDER-GATED-UP\n" as *u8, 24)
71
72 while 1 == 1 {
73 let cfd: i64 = sys_accept(lfd)
74 if cfd >= 0 {
75 sys_set_socket_timeout(cfd, 5)
76 let req: *u8 = sys_mmap(GBB_MAGIC_65536)
77 let rn: i64 = sys_read(cfd, req, GBB_MAGIC_65535)
78 if rn > 0 {
79 let he: i64 = gbb_find(req, rn, "\r\n\r\n" as *u8, 4)
80 if gbb_starts(req, rn, "POST /ad/banner" as *u8) == 1 {
81 // GATED: require a valid family OPAQUE session.
82 let tb: *u8 = sys_mmap(GBB_MAGIC_1024)
83 let tl: i64 = gbb_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, GBB_MAGIC_1024)
84 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64
85 let now: i64 = sys_now_realtime_sec()
86 var authed: i64 = 0
87 if tl > 0 { if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { authed = 1 } }
88 if authed == 1 {
89 var body: *u8 = req; var bn: i64 = 0
90 if he >= 0 { body = ((req as i64) + he + 4) as *u8; bn = rn - he - 4 }
91 let out: *u8 = sys_mmap(GBB_MAGIC_32768)
92 let glen: i64 = abb_render(body, bn, out)
93 if glen > 0 { gbb_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, out, glen) }
94 else { gbb_send(cfd, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"missing business\"}" as *u8, 27) }
95 } else {
96 gbb_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 25)
97 }
98 } else {
99 // the builder page (public shell; generation is gated above)
100 gbb_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, GBB_PAGE, gbb_slen(GBB_PAGE))
101 }
102 }
103 sys_close(cfd)
104 }
105 }
106 sys_close(lfd); sys_exit(0); return 0
107}