code wiki / _hdl_build / nx_adnet_apply.nx
nx_adnet_apply.nx source
↩ module page · 237 lines · 13111 B
1// nx_adnet_apply.nx -- LIB: the PUBLIC "advertise with us" surface. The last mile of the ad network.
2// THE GAP: the lane can serve a slot, measure a VIEWABLE impression, price it, invoice it, enforce a
3// campaign and record an advertiser -- and a prospective client had NO WAY TO APPLY. The catalog page
4// showed what was for sale with no door next to it. Machinery nobody can reach is not a business.
5//
6// WHAT THIS PAGE SAYS IS THE PRODUCT. Three claims almost no network makes, all of them true here and all
7// of them checkable by the client, which is why they are stated in public:
8// * WE BILL ON MRC-VIEWABLE IMPRESSIONS, NOT SERVED. 50 percent of the creative's pixels for one
9// CONTINUOUS second, or you are not charged for it.
10// * WE SHOW YOU THE DELIVERY RATIO. served vs viewable on every invoice line. If we deliver badly you
11// will see it before you have to ask.
12// * NO COOKIE, NO VISITOR ID, NO CROSS-SITE ANYTHING. Placement is contextual -- by page section and a
13// time rotation. There is no per-user profile to buy because there is none to build.
14// A refusal is also public: an ad with no rate row is REFUSED at invoice time rather than billed at zero.
15//
16// PRIVACY BOUNDARY, unchanged from nx_adnet_selfserve: this is a B2B intake. The form collects an
17// ADVERTISER contact, never a visitor. The schema HAS NO VISITOR FIELD by construction.
18// license_tier: ORIGINAL
19import "nx_syscalls.nx"
20import "_hdl_build/nx_adnet_slot.nx"
21
22// The creative spec the intake actually enforces (nx_adnet_creative). Stated here so an applicant reads
23// the SAME numbers the validator uses -- a spec page that drifts from the validator is how you get a
24// support queue full of rejected uploads.
25const AAP_W: i64 = 728
26const AAP_H: i64 = 90
27const AAP_MAXKB: i64 = 64
28
29func aap_catd(dst: *u8, off: i64, v: i64) -> i64 {
30 var o: i64 = off
31 var m: i64 = v
32 let t: *u8 = sys_mmap(24)
33 var k: i64 = 0
34 if m == 0 { t[0] = 48 as u8; k = 1 }
35 while m > 0 { t[k] = (48 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 }
36 var i: i64 = k - 1
37 while i >= 0 { dst[o] = t[i]; o = o + 1; i = i - 1 }
38 return o
39}
40
41// Emit the public page. Returns bytes written, 0 = refused (buffer too small).
42// Deliberately styleless beyond a little inline CSS: it inherits the site chrome when embedded, and a
43// page that needs an external stylesheet is a page that breaks under the site's own CSP.
44func aap_page(out: *u8, cap: i64) -> i64 {
45 if cap < 4096 { return 0 }
46 var o: i64 = 0
47 o = ad_cat(out, o, "<h1>Advertise on Nishi</h1>\n" as *u8)
48 o = ad_cat(out, o, "<p class=lede>One labelled banner per page. First-party, no third-party JavaScript, and billed only for impressions a human could actually see.</p>\n" as *u8)
49
50 o = ad_cat(out, o, "<h2>What you are buying</h2>\n<ul>\n" as *u8)
51 o = ad_cat(out, o, "<li><b>Billed on viewable impressions, not served ones.</b> An impression counts when at least 50% of your creative is in view for one continuous second — the MRC display standard. If it never rendered, you are not charged for it.</li>\n" as *u8)
52 o = ad_cat(out, o, "<li><b>You see the delivery ratio.</b> Every invoice line shows served and viewable side by side. If we deliver your campaign badly, you will see it before you have to ask.</li>\n" as *u8)
53 o = ad_cat(out, o, "<li><b>No cookie, no visitor id, no cross-site tracking.</b> Placement is contextual — by page section and a time rotation. There is no per-user profile for sale because there is none being built.</li>\n" as *u8)
54 o = ad_cat(out, o, "<li><b>Flight dates and a budget ceiling are enforced at serve time.</b> When a campaign ends or exhausts its budget it stops being shown, not merely stops being billed.</li>\n" as *u8)
55 o = ad_cat(out, o, "</ul>\n" as *u8)
56
57 o = ad_cat(out, o, "<h2>Creative spec</h2>\n<p>PNG, exactly " as *u8)
58 o = aap_catd(out, o, AAP_W)
59 o = ad_cat(out, o, "×" as *u8)
60 o = aap_catd(out, o, AAP_H)
61 o = ad_cat(out, o, ", at most " as *u8)
62 o = aap_catd(out, o, AAP_MAXKB)
63 o = ad_cat(out, o, "KB. The size ceiling is enforced at upload, not requested politely: a banner heavy enough to slow the host page reflects on the advertiser who paid for it. Your creative is hosted first-party at a content-addressed url, so replacing the artwork mints a new url and never serves a stale image.</p>\n" as *u8)
64
65 o = ad_cat(out, o, "<h2>Apply</h2>\n" as *u8)
66 o = ad_cat(out, o, "<form method=post action=\"/advertise/apply\">\n" as *u8)
67 o = ad_cat(out, o, "<p><label>Company<br><input name=company maxlength=120 required></label></p>\n" as *u8)
68 o = ad_cat(out, o, "<p><label>Contact email<br><input name=email type=email maxlength=160 required></label></p>\n" as *u8)
69 o = ad_cat(out, o, "<p><label>Where should the ad click through to?<br><input name=clickurl type=url maxlength=400 required></label></p>\n" as *u8)
70 o = ad_cat(out, o, "<p><label>Anything we should know (optional)<br><textarea name=note maxlength=600 rows=3></textarea></label></p>\n" as *u8)
71 o = ad_cat(out, o, "<p><button type=submit>Send application</button></p>\n" as *u8)
72 o = ad_cat(out, o, "</form>\n" as *u8)
73 o = ad_cat(out, o, "<p class=muted>Applications are staged for review and never go live automatically. We will reply with upload instructions and a rate quote. Nothing on this form is a visitor record — it is a business contact and is stored as one.</p>\n" as *u8)
74 out[o] = 0 as u8
75 return o
76}
77
78// The response to a submitted application. Staged-never-live is the whole safety property, so the
79// acknowledgement says so plainly rather than implying the ad is running.
80func aap_ack(out: *u8, cap: i64) -> i64 {
81 if cap < 512 { return 0 }
82 var o: i64 = 0
83 o = ad_cat(out, o, "<h1>Application received</h1>\n" as *u8)
84 o = ad_cat(out, o, "<p>Thank you — your application is <b>staged for review</b>. Nothing goes live automatically; an operator approves each advertiser by hand.</p>\n" as *u8)
85 o = ad_cat(out, o, "<p>Next: we reply with a rate quote and instructions for uploading your " as *u8)
86 o = aap_catd(out, o, AAP_W)
87 o = ad_cat(out, o, "×" as *u8)
88 o = aap_catd(out, o, AAP_H)
89 o = ad_cat(out, o, " creative.</p>\n" as *u8)
90 out[o] = 0 as u8
91 return o
92}
93
94// ---- APPLICATION PERSISTENCE ----------------------------------------------------------------------
95// An application is NOT an inventory row. ass_submit (nx_adnet_selfserve) validates a COMPLETE nx_adnet
96// row including a first-party creative url -- which an applicant does not have yet, because the creative
97// upload is what we send them AFTER we accept them. Feeding an application into that validator would
98// reject every genuine applicant. So applications get their own append-only journal.
99//
100// FIELD DISCIPLINE, all bounded: a TAB or newline injected by a submitter would forge a second row, so
101// both are stripped rather than escaped. Length caps mirror the form's maxlength attributes.
102// B2B ONLY: company / email / clickurl / note. No visitor field exists here, by construction.
103func aap_clean(src: *u8, out: *u8, cap: i64) -> i64 {
104 var o: i64 = 0
105 var i: i64 = 0
106 while src[i] != (0 as u8) {
107 let c: u8 = src[i]
108 var ok: i64 = 1
109 if c == (9 as u8) { ok = 0 }
110 if c == (10 as u8) { ok = 0 }
111 if c == (13 as u8) { ok = 0 }
112 if c < (32 as u8) { ok = 0 }
113 if ok == 1 { if o < cap - 1 { out[o] = c; o = o + 1 } }
114 i = i + 1
115 if i > 2048 { break }
116 }
117 out[o] = 0 as u8
118 return o
119}
120
121// Build one application row. Returns bytes, 0 = REFUSED (a required field was empty after cleaning).
122// received_at is passed IN, never read from a clock here, so a gate can assert the row exactly.
123func aap_row(company: *u8, email: *u8, clickurl: *u8, note: *u8, received_at: i64, out: *u8, cap: i64) -> i64 {
124 if cap < 1024 { return 0 }
125 let c1: *u8 = sys_mmap(256)
126 let c2: *u8 = sys_mmap(256)
127 let c3: *u8 = sys_mmap(512)
128 let c4: *u8 = sys_mmap(1024)
129 if aap_clean(company, c1, 256) == 0 { return 0 }
130 if aap_clean(email, c2, 256) == 0 { return 0 }
131 if aap_clean(clickurl, c3, 512) == 0 { return 0 }
132 aap_clean(note, c4, 1024)
133 var o: i64 = ad_cat(out, 0, "application\t" as *u8)
134 o = aap_catd(out, o, received_at)
135 out[o] = 9 as u8; o = o + 1
136 o = ad_cat(out, o, c1)
137 out[o] = 9 as u8; o = o + 1
138 o = ad_cat(out, o, c2)
139 out[o] = 9 as u8; o = o + 1
140 o = ad_cat(out, o, c3)
141 out[o] = 9 as u8; o = o + 1
142 o = ad_cat(out, o, c4)
143 out[o] = 10 as u8; o = o + 1
144 out[o] = 0 as u8
145 return o
146}
147
148// ---- FORM FIELD EXTRACTION ------------------------------------------------------------------------
149// The daemon has no form parser (sd_form_field lives in nx_status_daemon, a whole daemon we are not
150// importing for one function), so the apply lib owns its own -- bounded, and gated alongside everything
151// else here. application/x-www-form-urlencoded: name=value&name=value, '+' is space, %XX is a byte.
152//
153// MATCHES ON A FIELD BOUNDARY, not a substring: scanning for "email=" would also match "notemail=".
154// A parser that matches loosely is how a submitter puts their value in someone else's field.
155func aap_hexv(c: u8) -> i64 {
156 if c >= (48 as u8) { if c <= (57 as u8) { return (c as i64) - 48 } }
157 if c >= (97 as u8) { if c <= (102 as u8) { return (c as i64) - 87 } }
158 if c >= (65 as u8) { if c <= (70 as u8) { return (c as i64) - 55 } }
159 return 0 - 1
160}
161
162func aap_field(body: *u8, blen: i64, name: *u8, out: *u8, cap: i64) -> i64 {
163 out[0] = 0 as u8
164 var nl: i64 = 0
165 while name[nl] != (0 as u8) { nl = nl + 1 }
166 if nl == 0 { return 0 }
167 var i: i64 = 0
168 while i < blen {
169 // a field starts at offset 0 or immediately after '&'
170 var at_start: i64 = 0
171 if i == 0 { at_start = 1 }
172 if i > 0 { if body[i - 1] == (38 as u8) { at_start = 1 } }
173 if at_start == 1 {
174 var m: i64 = 1
175 var k: i64 = 0
176 while k < nl { if i + k >= blen { m = 0; break } if body[i + k] != name[k] { m = 0; break } k = k + 1 }
177 if m == 1 {
178 if i + nl < blen {
179 if body[i + nl] == (61 as u8) {
180 var o: i64 = 0
181 var j: i64 = i + nl + 1
182 while j < blen {
183 let c: u8 = body[j]
184 if c == (38 as u8) { break }
185 if o >= cap - 1 { break }
186 if c == (43 as u8) { out[o] = 32 as u8; o = o + 1; j = j + 1 }
187 else {
188 if c == (37 as u8) {
189 if j + 2 < blen {
190 let h1: i64 = aap_hexv(body[j + 1])
191 let h2: i64 = aap_hexv(body[j + 2])
192 if h1 >= 0 { if h2 >= 0 {
193 out[o] = (h1 * 16 + h2) as u8; o = o + 1; j = j + 3
194 } else { out[o] = c; o = o + 1; j = j + 1 } }
195 else { out[o] = c; o = o + 1; j = j + 1 }
196 } else { out[o] = c; o = o + 1; j = j + 1 }
197 } else { out[o] = c; o = o + 1; j = j + 1 }
198 }
199 }
200 out[o] = 0 as u8
201 return o
202 }
203 }
204 }
205 }
206 i = i + 1
207 }
208 return 0
209}
210
211// ---- ABUSE BOUNDS FOR A PUBLIC, UNAUTHENTICATED ROUTE ---------------------------------------------
212// /advertise/apply faces the open internet with no auth, so the two cheap exhaustion attacks have to be
213// closed by construction rather than by hoping nobody tries.
214//
215// A rate limit is deliberately NOT attempted here: this daemon is fork-per-connection, so children share
216// no memory, and a file-backed counter would be a racy lie that reads as protection. These two bounds
217// are stateless and therefore actually hold under concurrency.
218//
219// (1) BODY CAP -- four short form fields cannot legitimately exceed this. A multi-megabyte POST would
220// otherwise be parsed and copied per request.
221// (2) JOURNAL CEILING -- an append-only file on a public route is a disk-exhaustion primitive. Past the
222// ceiling we REFUSE THE APPEND but still ack, so a spammer learns nothing from the response and a
223// real applicant during a flood is not shown an error we cannot explain. The operator sees a journal
224// pinned at the ceiling, which is the signal to go look.
225const AAP_MAX_BODY: i64 = 8192
226const AAP_MAX_JOURNAL: i64 = 4194304
227
228func aap_body_ok(blen: i64) -> i64 {
229 if blen <= 0 { return 0 }
230 if blen > AAP_MAX_BODY { return 0 }
231 return 1
232}
233
234func aap_journal_ok(cur_bytes: i64) -> i64 {
235 if cur_bytes < 0 { return 1 }
236 if cur_bytes >= AAP_MAX_JOURNAL { return 0 }
237 return 1
238}