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 &mdash; 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 &mdash; 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, "&times;" 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 &mdash; 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 &mdash; 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, "&times;" 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}