code wiki / _hdl_build / nx_cms_admin.nx

nx_cms_admin.nx source

↩ module page · 1716 lines · 94554 B

1// nx_cms_admin.nx -- CMS C3+C4+C5: the WYSIWYG (field-based v1) ADMIN DAEMON. The human-engagement 2// surface: legal staff edit labelled fields in the browser; publish is atomic + revisioned; the 3// rendered page goes hot. Composes the session's proven CMS rungs: 4// nx_cms_store (C2: content as data, atomic + .prev revision) [gated] 5// nx_cms_render (C2: render-from-data, escape-on-render) [gated] 6// nx_html_sanitize(step 9: allowlist sanitize ON WRITE, 19/19 XSS) [gated] 7// nx_secure_token (step 3: 128-bit urandom session + CSRF, ct-eq) 8// _pe_pub (step 12: TEAM-AUTHORED publish state machine, CAPREG241) 9// nx_http_form (step 6: urlencoded body parse -- existing runtime rung) 10// nx_http_server (listen helper -- existing runtime rung) 11// Routes (the ONLY untrusted-input surface, rule 12 defensive-at-boundary): 12// GET / serve <site>/page.html (the published render) 13// GET /admin login form (no session) | field editor w/ CSRF (session) 14// POST /admin/login pw -> argon2id vs <site>/admin_pw.ar2 (C10, params live IN the file; 15// sha256 file = legacy migration fallback ONLY, .ar2 always wins); 16// 5-strike lockout; session cookie HttpOnly+SameSite=Strict (Secure = TLS rung) 17// POST /admin/save session + CSRF -> _html fields SANITIZED, store ATOMIC, render, publish; 18// mode=draft -> _pe_draft guard, draft.txt only, public page UNTOUCHED (C9) 19// GET /admin/preview session -> render of the PENDING DRAFT (no-store); 404 when no draft 20// POST /admin/publish session + CSRF -> _pe_draft guard (409 from CLEAN), draft promoted atomic 21// POST /admin/discard session + CSRF -> _pe_draft guard (409 from CLEAN), draft set aside additive 22// POST /admin/rollback session + CSRF -> _pe_pub guard (refuses from DRAFT), .prev swap, render 23// argv: [1]=port [2]=site dir [3]=request budget (exit 0 after N requests -- deterministic gates, 24// no orphan daemons). Plain HTTP loopback v1; TLS termination = the sites-daemon integration rung. 25// license_tier: ORIGINAL 26import "nx_cms_render.nx" 27import "nx_secure_token.nx" 28import "_pe_pub.nx" 29import "_pe_draft.nx" 30import "_pe_blockr.nx" 31import "_pe_seohead.nx" 32import "nx_cms_pw.nx" 33import "nx_session_ttl.nx" // #8: idle + absolute session timeout (sess_issue on login, sess_verify on check) 34import "nx_mfa.nx" // #12: TOTP second factor (mfa_verify after the password, when admin_totp.b32 exists) 35import "nx_http_form.nx" 36import "nx_cms_multipart.nx" 37import "nx_cms_snapshot.nx" 38import "nx_sha256.nx" 39const CA_MAGIC_65536: i64 = 65536 40const CA_MAGIC_65535: i64 = 65535 41const CA_MAGIC_8192: i64 = 8192 42const CA_MAGIC_8191: i64 = 8191 43const CA_MAGIC_131072: i64 = 131072 44const CA_MAGIC_131071: i64 = 131071 45const CA_MAGIC_200000: i64 = 200000 46const CA_MAGIC_4096: i64 = 4096 47const CA_MAGIC_3600: i64 = 3600 48const CA_MAGIC_2048: i64 = 2048 49const CA_MAGIC_2047: i64 = 2047 50const CA_MAGIC_4095: i64 = 4095 51const CA_MAGIC_131080: i64 = 131080 52// (WEBDEV STUDIO imports removed -- the studio is now its own OPAQUE-gated daemon, nx_sitegen_studio_daemon) 53 54const CA_REQCAP: i64 = 262144 55const CA_OUTCAP: i64 = 524288 56const CA_LOCKOUT: i64 = 5 57 58func ca_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 59func ca_cat(dst: *u8, off: i64, s: *u8) -> i64 { 60 var o: i64 = off 61 var k: i64 = 0 62 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 63 return o 64} 65func ca_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { 66 var o: i64 = off 67 var k: i64 = 0 68 while k < n { dst[o] = s[k]; o = o + 1; k = k + 1 } 69 return o 70} 71func ca_catnum(dst: *u8, off: i64, v: i64) -> i64 { 72 let t: *u8 = sys_mmap(24) 73 var m: i64 = v 74 var k: i64 = 0 75 if m == 0 { t[0] = 48 as u8; k = 1 } 76 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 77 var o: i64 = off 78 var i: i64 = 0 79 while i < k { dst[o] = t[k-1-i]; o = o + 1; i = i + 1 } 80 return o 81} 82 83// first index of needle (nl bytes) in hay[0..n), or -1 84func ca_index(hay: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 85 var i: i64 = 0 86 while i + nl <= n { 87 var q: i64 = 0 88 var ok: i64 = 1 89 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 } 90 if ok == 1 { return i } 91 i = i + 1 92 } 93 return 0 - 1 94} 95 96func ca_atoi(s: *u8) -> i64 { 97 var v: i64 = 0 98 var i: i64 = 0 99 while s[i] != (0 as u8) { 100 let c: i64 = s[i] as i64 101 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 102 i = i + 1 103 } 104 return v 105} 106 107// read "key=<int>" out of a small config buffer (rule 11/17); returns the value or dflt if absent. 108func ca_conf_int(buf: *u8, n: i64, key: *u8, klen: i64, dflt: i64) -> i64 { 109 let at: i64 = ca_index(buf, n, key, klen) 110 if at < 0 { return dflt } 111 var v: i64 = 0 112 var i: i64 = at + klen 113 var seen: i64 = 0 114 var go: i64 = 1 115 while go == 1 { 116 go = 0 117 if i < n { 118 let c: i64 = buf[i] as i64 119 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = seen + 1; i = i + 1; go = 1 } } 120 } 121 } 122 if seen == 0 { return dflt } 123 return v 124} 125 126// build <sitedir>/<leaf> into dst 127func ca_path(dst: *u8, sitedir: *u8, leaf: *u8) -> i64 { 128 var o: i64 = ca_cat(dst, 0, sitedir) 129 o = ca_cat(dst, o, "/" as *u8) 130 o = ca_cat(dst, o, leaf) 131 dst[o] = 0 as u8 132 return o 133} 134 135// ---- response writers ---- 136func ca_send(cfd: i64, buf: *u8, n: i64) -> i64 { 137 var off: i64 = 0 138 var go: i64 = 1 139 while go == 1 { 140 go = 0 141 if off < n { 142 let w: i64 = sys_write(cfd, (buf + off) as *u8, n - off) 143 if w > 0 { off = off + w; go = 1 } 144 } 145 } 146 return off 147} 148func ca_resp(cfd: i64, status: *u8, extra: *u8, body: *u8, bn: i64) -> i64 { 149 let h: *u8 = sys_mmap(CA_OUTCAP) 150 var o: i64 = ca_cat(h, 0, "HTTP/1.1 " as *u8) 151 o = ca_cat(h, o, status) 152 o = ca_cat(h, o, "\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nContent-Length: " as *u8) 153 o = ca_catnum(h, o, bn) 154 o = ca_cat(h, o, "\r\n" as *u8) 155 o = ca_cat(h, o, extra) 156 o = ca_cat(h, o, "\r\n" as *u8) 157 o = ca_catn(h, o, body, bn) 158 ca_send(cfd, h, o) 159 return 0 160} 161 162// ---- request reading (headers + Content-Length body) ---- 163func ca_content_length(buf: *u8, hend: i64) -> i64 { 164 var p: i64 = ca_index(buf, hend, "Content-Length:" as *u8, 15) 165 if p < 0 { p = ca_index(buf, hend, "content-length:" as *u8, 15) } 166 if p < 0 { return 0 } 167 var v: i64 = 0 168 var i: i64 = p + 15 169 var go: i64 = 1 170 while go == 1 { 171 go = 0 172 if i < hend { 173 let c: i64 = buf[i] as i64 174 if c == 32 { i = i + 1; go = 1 } 175 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); i = i + 1; go = 1 } } 176 } 177 } 178 return v 179} 180func ca_read_req(cfd: i64, buf: *u8, cap: i64, hend_out: *i64) -> i64 { 181 var total: i64 = 0 182 var hend: i64 = 0 - 1 183 var go: i64 = 1 184 while go == 1 { 185 go = 0 186 if total < cap { 187 if hend < 0 { 188 let r: i64 = sys_read(cfd, (buf + total) as *u8, cap - total) 189 if r > 0 { 190 total = total + r 191 hend = ca_index(buf, total, "\r\n\r\n" as *u8, 4) 192 if hend < 0 { go = 1 } 193 } 194 } 195 } 196 } 197 hend_out[0] = hend 198 if hend < 0 { return total } 199 let cl: i64 = ca_content_length(buf, hend) 200 var need: i64 = hend + 4 + cl 201 if need > cap { need = cap } 202 go = 1 203 while go == 1 { 204 go = 0 205 if total < need { 206 let r2: i64 = sys_read(cfd, (buf + total) as *u8, need - total) 207 if r2 > 0 { total = total + r2; go = 1 } 208 } 209 } 210 return total 211} 212 213// ---- auth ---- 214// session cookie present + matches? sess[0]==0 means no session issued yet. 215func ca_session_ok(buf: *u8, hend: i64, sess: *u8) -> i64 { 216 if (sess[0] as i64) == 0 { return 0 } 217 let p: i64 = ca_index(buf, hend, "nsess=" as *u8, 6) 218 if p < 0 { return 0 } 219 if p + 6 + 32 > hend { return 0 } 220 if st_eq_ct((buf + p + 6) as *u8, sess, 32) == 0 { return 0 } 221 // #8: token matches -> ALSO enforce idle + absolute timeout (sess_verify slides the idle window on VALID) 222 if sess_verify(sess, sys_now_realtime_ms()) != SESS_VALID { return 0 } 223 return 1 224} 225// #12: MFA satisfied? -> 1 if NOT enrolled (password-only, graceful) OR enrolled with a valid TOTP code in 226// the body. 0 ONLY when enrolled and the code is missing/wrong. now_unix = sys_now_realtime_sec() at login. 227func ca_mfa_ok(body: *u8, bn: i64, totpsec: *u8, totpseclen: i64, now_unix: i64) -> i64 { 228 if mfa_enrolled(totpsec, totpseclen) == 0 { return 1 } 229 let code: *u8 = sys_mmap(32); let cl: *i64 = sys_mmap(16) as *i64 230 if nx_http_form_get_field(body, bn, "code" as *u8, 4, code, 31, cl) != 0 { return 0 } 231 return mfa_verify(totpsec, totpseclen, code, cl[0], now_unix, MFA_WINDOW) 232} 233// csrf form field matches? 234func ca_csrf_ok(body: *u8, bn: i64, csrf: *u8) -> i64 { 235 let v: *u8 = sys_mmap(128) 236 let vl: *i64 = sys_mmap(16) as *i64 237 if nx_http_form_get_field(body, bn, "csrf" as *u8, 4, v, 127, vl) != 0 { return 0 } 238 if vl[0] != 32 { return 0 } 239 return st_eq_ct(v, csrf, 32) 240} 241 242// ---- page assembly ---- 243// escape a store value into an editor form (attribute/textarea safe) 244func ca_esc_into(dst: *u8, off: i64, val: *u8, vl: i64) -> i64 { 245 return off + hs_escape(val, vl, (dst + off) as *u8, CA_OUTCAP - off) 246} 247 248// the field editor: one labelled input per content key, textarea for _html, CSRF hidden field. 249func ca_editor_page(out: *u8, store: *u8, sn: i64, csrf: *u8) -> i64 { 250 let v: *u8 = sys_mmap(CA_MAGIC_65536) 251 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Andelin &amp; West &mdash; Site Manager</title><style>*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#eef1f6;color:#16202e}header{background:#0b2545;color:#fff;padding:16px 24px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}header h1{font-size:1.15rem;margin:0;font-weight:700}nav a{color:#cdd9ee;margin-left:16px;text-decoration:none;font-size:.88rem}nav a:hover{color:#fff}main{max-width:760px;margin:24px auto;padding:0 16px}section{background:#fff;border:1px solid #dde3ee;border-radius:10px;padding:16px 22px 20px;margin:16px 0}section h2{margin:0 0 4px;font-size:1.02rem;color:#0b2545;border-bottom:2px solid #eef1f6;padding-bottom:8px}label{display:block;margin-top:14px;font-weight:600;font-size:.86rem}input,textarea{width:100%;padding:9px 11px;font-size:.95rem;border:1px solid #c3ccdd;border-radius:6px;margin-top:5px;font-family:inherit}textarea{min-height:84px}.hint{font-weight:400;color:#7a8699;font-size:.8rem}.bar{display:flex;gap:12px;margin-top:4px;flex-wrap:wrap}button{padding:11px 22px;font-size:.95rem;background:#2a4d8f;color:#fff;border:0;border-radius:6px;cursor:pointer}button:hover{background:#21407a}.ops form{display:inline-block;margin:4px 8px 4px 0}.ops button{background:#5b6b85;font-size:.84rem;padding:8px 14px}.roll button{background:#8f2a2a}a.dl{font-size:.84rem;color:#2a4d8f}</style></head><body><header><h1>Andelin &amp; West &mdash; Site Manager</h1><nav><a href=\"/\">View site</a><a href=\"/admin/blocks\">Blocks</a><a href=\"/admin/stats\">Stats</a><a href=\"/admin/submissions\">Submissions</a></nav></header><main><form method=\"POST\" action=\"/admin/save\">" as *u8) 252 // ---- Firm Identity ---- 253 o = ca_cat(out, o, "<section><h2>Firm Identity</h2><label>Firm name</label><input name=\"hero_title\" value=\"" as *u8) 254 var vl: i64 = cst_get(store, sn, "hero_title" as *u8, v, CA_MAGIC_65535) 255 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 256 o = ca_cat(out, o, "\"><label>Tagline</label><input name=\"hero_tag\" value=\"" as *u8) 257 vl = cst_get(store, sn, "hero_tag" as *u8, v, CA_MAGIC_65535) 258 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 259 o = ca_cat(out, o, "\"></section>" as *u8) 260 // ---- Contact ---- 261 o = ca_cat(out, o, "<section><h2>Contact</h2><label>Phone</label><input name=\"phone\" value=\"" as *u8) 262 vl = cst_get(store, sn, "phone" as *u8, v, CA_MAGIC_65535) 263 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 264 o = ca_cat(out, o, "\"><label>Email</label><input name=\"email\" value=\"" as *u8) 265 vl = cst_get(store, sn, "email" as *u8, v, CA_MAGIC_65535) 266 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 267 o = ca_cat(out, o, "\"><label>Address <span class=\"hint\">(one line per row)</span></label><textarea name=\"address\">" as *u8) 268 vl = cst_get(store, sn, "address" as *u8, v, CA_MAGIC_65535) 269 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 270 o = ca_cat(out, o, "</textarea><label>Office hours <span class=\"hint\">(one line per row)</span></label><textarea name=\"hours\">" as *u8) 271 vl = cst_get(store, sn, "hours" as *u8, v, CA_MAGIC_65535) 272 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 273 o = ca_cat(out, o, "</textarea></section>" as *u8) 274 // ---- Practice Areas ---- 275 o = ca_cat(out, o, "<section><h2>Practice Areas <span class=\"hint\">(one per line)</span></h2><textarea name=\"practice_areas\">" as *u8) 276 vl = cst_get(store, sn, "practice_areas" as *u8, v, CA_MAGIC_65535) 277 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 278 o = ca_cat(out, o, "</textarea></section>" as *u8) 279 // ---- About (rich text) ---- 280 o = ca_cat(out, o, "<section><h2>About <span class=\"hint\">(rich text: b i strong em p br ul ol li a h2 h3)</span></h2><textarea name=\"note_html\">" as *u8) 281 vl = cst_get(store, sn, "note_html" as *u8, v, CA_MAGIC_65535) 282 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 283 o = ca_cat(out, o, "</textarea></section>" as *u8) 284 // ---- Footer ---- 285 o = ca_cat(out, o, "<section><h2>Footer</h2><label>Footer text</label><input name=\"foot\" value=\"" as *u8) 286 vl = cst_get(store, sn, "foot" as *u8, v, CA_MAGIC_65535) 287 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 288 o = ca_cat(out, o, "\"></section>" as *u8) 289 // ---- SEO & Social ---- 290 o = ca_cat(out, o, "<section><h2>SEO &amp; Social</h2><label>SEO title <span class=\"hint\">(blank = firm name)</span></label><input name=\"seo_title\" value=\"" as *u8) 291 vl = cst_get(store, sn, "seo_title" as *u8, v, CA_MAGIC_65535) 292 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 293 o = ca_cat(out, o, "\"><label>SEO description <span class=\"hint\">(blank = tagline)</span></label><input name=\"seo_desc\" value=\"" as *u8) 294 vl = cst_get(store, sn, "seo_desc" as *u8, v, CA_MAGIC_65535) 295 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 296 o = ca_cat(out, o, "\"><label>Site URL <span class=\"hint\">(for canonical + sitemap)</span></label><input name=\"site_url\" value=\"" as *u8) 297 vl = cst_get(store, sn, "site_url" as *u8, v, CA_MAGIC_65535) 298 if vl > 0 { o = ca_esc_into(out, o, v, vl) } 299 o = ca_cat(out, o, "\"></section>" as *u8) 300 o = ca_cat(out, o, "<input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 301 o = ca_catn(out, o, csrf, 32) 302 o = ca_cat(out, o, "\"><section><div class=\"bar\"><button type=\"submit\">Save &amp; Publish</button><button type=\"submit\" name=\"mode\" value=\"draft\">Save as draft</button></div></section></form><section class=\"ops\"><h2>Publishing &amp; Backup</h2><p class=\"hint\"><a href=\"/admin/preview\">Preview pending draft &rarr;</a></p><form method=\"POST\" action=\"/admin/publish\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 303 o = ca_catn(out, o, csrf, 32) 304 o = ca_cat(out, o, "\"><button type=\"submit\">Publish pending draft</button></form><form method=\"POST\" action=\"/admin/discard\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 305 o = ca_catn(out, o, csrf, 32) 306 o = ca_cat(out, o, "\"><button type=\"submit\">Discard pending draft</button></form><form class=\"roll\" method=\"POST\" action=\"/admin/rollback\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 307 o = ca_catn(out, o, csrf, 32) 308 o = ca_cat(out, o, "\"><button type=\"submit\">Roll back to previous publish</button></form>" as *u8) 309 // W4 backup/restore (UpdraftPlus-class): snapshot the whole site, restore byte-exact 310 o = ca_cat(out, o, "<fieldset><legend>Backup</legend><form method=\"POST\" action=\"/admin/snapshot\" style=\"display:inline\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 311 o = ca_catn(out, o, csrf, 32) 312 o = ca_cat(out, o, "\"><button type=\"submit\">Take snapshot</button></form> <a href=\"/admin/snapshot.nxsnap\">download</a> <form method=\"POST\" action=\"/admin/restore\" style=\"display:inline\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 313 o = ca_catn(out, o, csrf, 32) 314 o = ca_cat(out, o, "\"><button type=\"submit\">Restore last snapshot</button></form></fieldset></section></main></body></html>" as *u8) 315 return o 316} 317 318func ca_login_page(out: *u8, locked: i64) -> i64 { 319 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Editor Login</title><style>body{font-family:Georgia,serif;max-width:420px;margin:10vh auto;padding:0 24px}input{width:100%;padding:8px;margin-top:8px;box-sizing:border-box}button{margin-top:14px;padding:10px 22px;background:#2a4d8f;color:#fff;border:0;border-radius:4px}</style></head><body><h1>Editor Login</h1>" as *u8) 320 if locked == 1 { o = ca_cat(out, o, "<p><b>Locked: too many failed attempts. Restart the editor service to retry.</b></p></body></html>" as *u8); return o } 321 o = ca_cat(out, o, "<form method=\"POST\" action=\"/admin/login\"><label>Password</label><input type=\"password\" name=\"pw\"><label>Authenticator code <span style=\"font-weight:400;color:#888\">(if MFA enabled)</span></label><input name=\"code\" inputmode=\"numeric\" autocomplete=\"one-time-code\" maxlength=\"6\" placeholder=\"6-digit code\"><button type=\"submit\">Sign in</button></form></body></html>" as *u8) 322 return o 323} 324 325// ---- CLIENT PORTAL (role 2: clients sign in to their own portal; the lawyer/staff role is /admin) ---- 326// client session cookie present + matches? csess[0]==0 means no client session issued yet. 327func ca_client_ok(buf: *u8, hend: i64, csess: *u8) -> i64 { 328 if (csess[0] as i64) == 0 { return 0 } 329 let p: i64 = ca_index(buf, hend, "nclient=" as *u8, 8) 330 if p < 0 { return 0 } 331 if p + 8 + 32 > hend { return 0 } 332 return st_eq_ct((buf + p + 8) as *u8, csess, 32) 333} 334func ca_client_login_page(out: *u8, bad: i64) -> i64 { 335 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Client Portal &mdash; Andelin &amp; West</title><style>body{font-family:Georgia,serif;max-width:420px;margin:10vh auto;padding:0 24px;color:#16202e}h1{color:#0b2545}input{width:100%;padding:8px;margin-top:8px;box-sizing:border-box;border:1px solid #aab;border-radius:4px}button{margin-top:14px;padding:10px 22px;background:#0b2545;color:#fff;border:0;border-radius:4px;cursor:pointer}a{color:#2a4d8f}</style></head><body><h1>Client Portal</h1><p>Andelin &amp; West clients &mdash; sign in to your secure portal.</p>" as *u8) 336 if bad == 1 { o = ca_cat(out, o, "<p style=\"color:#a11\"><b>Incorrect credentials.</b></p>" as *u8) } 337 o = ca_cat(out, o, "<form method=\"POST\" action=\"/portal/login\"><label>Password</label><input type=\"password\" name=\"pw\"><button type=\"submit\">Sign in</button></form><p style=\"margin-top:1.5rem\"><a href=\"/\">&larr; Back to site</a></p></body></html>" as *u8) 338 return o 339} 340func ca_portal_page(out: *u8) -> i64 { 341 return ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Your Client Portal</title><style>body{font-family:Georgia,serif;max-width:680px;margin:6vh auto;padding:0 24px;color:#16202e;line-height:1.6}h1{color:#0b2545}.card{background:#f4f4f8;border-left:4px solid #0b2545;padding:16px 20px;margin:16px 0}button{padding:8px 18px;background:#0b2545;color:#fff;border:0;border-radius:4px;cursor:pointer}</style></head><body><h1>Welcome to your Client Portal</h1><p>You are securely signed in.</p><div class=\"card\"><strong>Your matters</strong><p>Secure documents, invoices, and messaging with your attorney will appear here.</p></div><form method=\"POST\" action=\"/portal/logout\"><button type=\"submit\">Sign out</button></form></body></html>" as *u8) 342} 343 344// load store + render template -> atomic page.html publish. returns 1/0. 345func ca_render_publish(sitedir: *u8) -> i64 { 346 let cpath: *u8 = sys_mmap(512) 347 let tpath: *u8 = sys_mmap(512) 348 let ppath: *u8 = sys_mmap(512) 349 ca_path(cpath, sitedir, "content.txt" as *u8) 350 ca_path(tpath, sitedir, "template.html" as *u8) 351 ca_path(ppath, sitedir, "page.html" as *u8) 352 let store: *u8 = sys_mmap(CA_REQCAP) 353 var sn: i64 = cst_read(cpath, store, CA_REQCAP - 1) 354 if sn < 0 { return 0 } 355 let tpl: *u8 = sys_mmap(CA_REQCAP) 356 let tn: i64 = cst_read(tpath, tpl, CA_REQCAP - 1) 357 if tn < 0 { return 0 } 358 // C11: blocks -> blocks_html (team-authored renderer) injected before template render 359 let bh: *u8 = sys_mmap(CA_OUTCAP) 360 let bhn: i64 = ca_blocks_html(store, sn, bh, CA_OUTCAP - 1) 361 var rstore: *u8 = store 362 if bhn >= 0 { 363 let s2b: *u8 = sys_mmap(CA_REQCAP) 364 sn = cst_set(store, sn, "blocks_html" as *u8, bh, bhn, s2b, CA_REQCAP - 1) 365 rstore = s2b 366 } 367 // W2: SEO head -> seo_head_html (team-authored renderer); _html key inserts raw (self-escaped) 368 let sh: *u8 = sys_mmap(CA_OUTCAP) 369 let shn: i64 = ca_seo_head(rstore, sn, sh, CA_OUTCAP - 1) 370 if shn > 0 { 371 let s3b: *u8 = sys_mmap(CA_REQCAP) 372 sn = cst_set(rstore, sn, "seo_head_html" as *u8, sh, shn, s3b, CA_REQCAP - 1) 373 rstore = s3b 374 } 375 let page: *u8 = sys_mmap(CA_OUTCAP) 376 let pn: i64 = crd_render(tpl, tn, rstore, sn, page, CA_OUTCAP - 1) 377 return cst_write_atomic(ppath, page, pn) 378} 379 380// ---- C11: blocks + uploads ---- 381// like ca_resp but with a caller-chosen Content-Type (binary-safe image serving) 382func ca_resp_ct(cfd: i64, status: *u8, ctype: *u8, body: *u8, bn: i64) -> i64 { 383 let h: *u8 = sys_mmap(CA_OUTCAP) 384 var o: i64 = ca_cat(h, 0, "HTTP/1.1 " as *u8) 385 o = ca_cat(h, o, status) 386 o = ca_cat(h, o, "\r\nContent-Type: " as *u8) 387 o = ca_cat(h, o, ctype) 388 o = ca_cat(h, o, "\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nContent-Length: " as *u8) 389 o = ca_catnum(h, o, bn) 390 o = ca_cat(h, o, "\r\n\r\n" as *u8) 391 o = ca_catn(h, o, body, bn) 392 ca_send(cfd, h, o) 393 return 0 394} 395 396// filename sanity: 1..100 chars of [A-Za-z0-9._-], no leading dot, no ".." (traversal-proof) 397func ca_sane_name(s: *u8, n: i64) -> i64 { 398 if n < 1 { return 0 } 399 if n > 100 { return 0 } 400 if (s[0] as i64) == 46 { return 0 } 401 var i: i64 = 0 402 while i < n { 403 let c: i64 = s[i] as i64 404 var ok: i64 = 0 405 if c >= 97 { if c <= 122 { ok = 1 } } 406 if c >= 65 { if c <= 90 { ok = 1 } } 407 if c >= 48 { if c <= 57 { ok = 1 } } 408 if c == 46 { ok = 1 } 409 if c == 95 { ok = 1 } 410 if c == 45 { ok = 1 } 411 if ok == 0 { return 0 } 412 if c == 46 { if i + 1 < n { if (s[i+1] as i64) == 46 { return 0 } } } 413 i = i + 1 414 } 415 return 1 416} 417 418func ca_lc(c: i64) -> i64 { 419 if c >= 65 { if c <= 90 { return c + 32 } } 420 return c 421} 422 423// image type by extension (the allowed-upload table, DATA): 0=png 1=jpeg 2=gif, -1 = not allowed 424func ca_img_type(name: *u8, n: i64) -> i64 { 425 var d: i64 = 0 - 1 426 var i: i64 = 0 427 while i < n { if (name[i] as i64) == 46 { d = i } i = i + 1 } 428 if d < 0 { return 0 - 1 } 429 let e: i64 = n - d - 1 430 if e == 3 { 431 let a: i64 = ca_lc(name[d+1] as i64) 432 let b: i64 = ca_lc(name[d+2] as i64) 433 let c: i64 = ca_lc(name[d+3] as i64) 434 if a == 112 { if b == 110 { if c == 103 { return 0 } } } // png 435 if a == 106 { if b == 112 { if c == 103 { return 1 } } } // jpg 436 if a == 103 { if b == 105 { if c == 102 { return 2 } } } // gif 437 } 438 if e == 4 { 439 let a2: i64 = ca_lc(name[d+1] as i64) 440 let b2: i64 = ca_lc(name[d+2] as i64) 441 let c2: i64 = ca_lc(name[d+3] as i64) 442 let d2: i64 = ca_lc(name[d+4] as i64) 443 if a2 == 106 { if b2 == 112 { if c2 == 101 { if d2 == 103 { return 1 } } } } // jpeg 444 } 445 return 0 - 1 446} 447 448// magic bytes must MATCH the claimed extension (a .png with non-PNG bytes is refused) 449func ca_magic_ok(t: i64, body: *u8, n: i64) -> i64 { 450 if t == 0 { 451 if n < 4 { return 0 } 452 if (body[0] as i64) != 0x89 { return 0 } 453 if (body[1] as i64) != 0x50 { return 0 } 454 if (body[2] as i64) != 0x4e { return 0 } 455 if (body[3] as i64) != 0x47 { return 0 } 456 return 1 457 } 458 if t == 1 { 459 if n < 3 { return 0 } 460 if (body[0] as i64) != 0xff { return 0 } 461 if (body[1] as i64) != 0xd8 { return 0 } 462 if (body[2] as i64) != 0xff { return 0 } 463 return 1 464 } 465 if t == 2 { 466 if n < 4 { return 0 } 467 if (body[0] as i64) != 71 { return 0 } 468 if (body[1] as i64) != 73 { return 0 } 469 if (body[2] as i64) != 70 { return 0 } 470 if (body[3] as i64) != 56 { return 0 } 471 return 1 472 } 473 return 0 474} 475 476func ca_img_ctype(t: i64) -> *u8 { 477 if t == 0 { return "image/png" as *u8 } 478 if t == 1 { return "image/jpeg" as *u8 } 479 return "image/gif" as *u8 480} 481 482// W2 SEO: build the <head> meta block from store keys via the TEAM-AUTHORED renderer (_pe_seohead), 483// with field fallbacks (seo_title->hero_title, seo_desc->hero_tag). Each tag is emitted only when its 484// source value is present; the renderer escapes every value. Returns head length. 485func ca_seo_get(store: *u8, sn: i64, k1: *u8, k2: *u8, out: *u8, cap: i64) -> i64 { 486 var vl: i64 = cst_get(store, sn, k1, out, cap) 487 if vl > 0 { return vl } 488 if (k2[0] as i64) != 0 { vl = cst_get(store, sn, k2, out, cap) } 489 if vl > 0 { return vl } 490 return 0 491} 492func ca_seo_head(store: *u8, sn: i64, out: *u8, cap: i64) -> i64 { 493 var o: i64 = 0 494 let v: *u8 = sys_mmap(CA_MAGIC_8192) 495 // title (kind 1) + og:title (kind 3): seo_title || hero_title 496 var tl: i64 = ca_seo_get(store, sn, "seo_title" as *u8, "hero_title" as *u8, v, CA_MAGIC_8191) 497 if tl > 0 { 498 o = o + _pe_seohead_one(1, v, tl, ((out as i64) + o) as *u8, cap - o) 499 o = o + _pe_seohead_one(3, v, tl, ((out as i64) + o) as *u8, cap - o) 500 } 501 // description (kind 2) + og:description (kind 4): seo_desc || hero_tag 502 var dl: i64 = ca_seo_get(store, sn, "seo_desc" as *u8, "hero_tag" as *u8, v, CA_MAGIC_8191) 503 if dl > 0 { 504 o = o + _pe_seohead_one(2, v, dl, ((out as i64) + o) as *u8, cap - o) 505 o = o + _pe_seohead_one(4, v, dl, ((out as i64) + o) as *u8, cap - o) 506 } 507 // og:image (kind 5): only if set 508 let il: i64 = cst_get(store, sn, "og_image" as *u8, v, CA_MAGIC_8191) 509 if il > 0 { o = o + _pe_seohead_one(5, v, il, ((out as i64) + o) as *u8, cap - o) } 510 // canonical (kind 6): site_url (+ "/" if bare host) 511 let ul: i64 = cst_get(store, sn, "site_url" as *u8, v, CA_MAGIC_8191) 512 if ul > 0 { o = o + _pe_seohead_one(6, v, ul, ((out as i64) + o) as *u8, cap - o) } 513 // og:type website (kind 7), constant 514 o = o + _pe_seohead_one(7, "website" as *u8, 7, ((out as i64) + o) as *u8, cap - o) 515 return o 516} 517 518// W5: extract the GET path token (after "GET ", up to space or '?') into key, sanitized to a 519// safe store-key charset ([A-Za-z0-9/._-]); returns key length or -1. The path IS the analytics key 520// so analytics is genuinely per-page (the site currently serves one page, "/"). 521func ca_path_key(req: *u8, rn: i64, key: *u8, cap: i64) -> i64 { 522 if ca_index(req, rn, "GET " as *u8, 4) != 0 { return 0 - 1 } 523 var i: i64 = 4 524 var o: i64 = 0 525 while i < rn { 526 let c: i64 = req[i] as i64 527 if c == 32 { i = rn } 528 if c == 63 { i = rn } 529 if i < rn { 530 var okc: i64 = 0 531 if c >= 97 { if c <= 122 { okc = 1 } } 532 if c >= 65 { if c <= 90 { okc = 1 } } 533 if c >= 48 { if c <= 57 { okc = 1 } } 534 if c == 47 { okc = 1 } 535 if c == 46 { okc = 1 } 536 if c == 95 { okc = 1 } 537 if c == 45 { okc = 1 } 538 if okc == 1 { if o < cap { key[o] = c as u8; o = o + 1 } } 539 i = i + 1 540 } 541 } 542 if o == 0 { return 0 - 1 } 543 key[o] = 0 as u8 544 return o 545} 546 547// W5: record one page view for key in the analytics store (read-modify-write; the accept loop is 548// sequential so this is race-free). additive: counts only ever increase. 549func ca_analytics_hit(apath: *u8, key: *u8) -> i64 { 550 let st: *u8 = sys_mmap(CA_REQCAP) 551 var sn: i64 = cst_read(apath, st, CA_REQCAP - 1) 552 if sn < 0 { sn = 0 } 553 let cv: *u8 = sys_mmap(32) 554 let cl: i64 = cst_get(st, sn, key, cv, 31) 555 var count: i64 = 0 556 if cl > 0 { 557 var i: i64 = 0 558 while i < cl { let c: i64 = cv[i] as i64; if c >= 48 { if c <= 57 { count = count * 10 + (c - 48) } } i = i + 1 } 559 } 560 count = count + 1 561 let nv: *u8 = sys_mmap(32) 562 var no: i64 = 0 563 let tmp: *u8 = sys_mmap(32) 564 var m: i64 = count 565 var k: i64 = 0 566 if m == 0 { tmp[0] = 48 as u8; k = 1 } 567 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 568 while no < k { nv[no] = tmp[k-1-no]; no = no + 1 } 569 let s2: *u8 = sys_mmap(CA_REQCAP) 570 let n2: i64 = cst_set(st, sn, key, nv, no, s2, CA_REQCAP - 1) 571 cst_write_atomic(apath, s2, n2) 572 return count 573} 574 575// W5: render the analytics store as a session-only stats table (path -> count). Returns length. 576func ca_stats_page(out: *u8, apath: *u8) -> i64 { 577 let st: *u8 = sys_mmap(CA_REQCAP) 578 var sn: i64 = cst_read(apath, st, CA_REQCAP - 1) 579 if sn < 0 { sn = 0 } 580 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Stats</title><style>body{font-family:Georgia,serif;max-width:640px;margin:4vh auto}table{width:100%;border-collapse:collapse}td,th{border-bottom:1px solid #ccc;text-align:left;padding:6px}</style></head><body><h1>Site analytics <span style=\"font-size:.6em;color:#888\">(first-party, no third-party beacon)</span></h1><p><a href=\"/admin\">&larr; editor</a></p><table><tr><th>path</th><th>views</th></tr>" as *u8) 581 // each record: @key\n value\n ; walk the store lines 582 var i: i64 = 0 583 var total: i64 = 0 584 while i < sn { 585 if (st[i] as i64) == 64 { 586 // key from i+1 to newline 587 var ke: i64 = i + 1 588 while ke < sn { if (st[ke] as i64) == 10 { break } ke = ke + 1 } 589 // value from ke+1 to next newline 590 var ve: i64 = ke + 1 591 while ve < sn { if (st[ve] as i64) == 10 { break } ve = ve + 1 } 592 o = ca_cat(out, o, "<tr><td>" as *u8) 593 o = o + hs_escape(((st as i64) + i + 1) as *u8, ke - i - 1, ((out as i64) + o) as *u8, CA_OUTCAP - o) 594 o = ca_cat(out, o, "</td><td>" as *u8) 595 o = ca_catn(out, o, ((st as i64) + ke + 1) as *u8, ve - ke - 1) 596 o = ca_cat(out, o, "</td></tr>" as *u8) 597 var cc: i64 = 0 598 var q: i64 = ke + 1 599 while q < ve { let c2: i64 = st[q] as i64; if c2 >= 48 { if c2 <= 57 { cc = cc * 10 + (c2 - 48) } } q = q + 1 } 600 total = total + cc 601 i = ve + 1 602 } else { i = i + 1 } 603 } 604 o = ca_cat(out, o, "<tr><th>total</th><th>" as *u8) 605 o = ca_catnum(out, o, total) 606 o = ca_cat(out, o, "</th></tr></table></body></html>" as *u8) 607 return o 608} 609 610// W3 forms: per-IP sliding-window rate check. Returns 1 if allowed (and records the hit), 0 if over 611// the cap. Table is in-memory (the accept loop is sequential -> race-free). Fail-open if the table 612// is full (capacity 64 distinct IPs is generous; never block a legit visitor on bookkeeping). 613func ca_rate_ok(ip_arr: *i64, cnt_arr: *i64, ws_arr: *i64, nbox: *i64, ip: i64, now: i64, window: i64, maxn: i64) -> i64 { 614 var idx: i64 = 0 - 1 615 var i: i64 = 0 616 while i < nbox[0] { if ip_arr[i] == ip { idx = i; i = nbox[0] } else { i = i + 1 } } 617 if idx < 0 { 618 if nbox[0] >= 64 { return 1 } 619 idx = nbox[0] 620 ip_arr[idx] = ip 621 cnt_arr[idx] = 0 622 ws_arr[idx] = now 623 nbox[0] = nbox[0] + 1 624 } 625 if now - ws_arr[idx] > window { cnt_arr[idx] = 0; ws_arr[idx] = now } 626 if cnt_arr[idx] >= maxn { return 0 } 627 cnt_arr[idx] = cnt_arr[idx] + 1 628 return 1 629} 630 631// W3 forms: append one submission record (additive; never lost). Record line: 632// T<ts>|F<formname>|<urlencoded body>\n 633func ca_submit_append(spath: *u8, ts: i64, fname: *u8, fnl: i64, body: *u8, bn: i64) -> i64 { 634 let line: *u8 = sys_mmap(CA_REQCAP) 635 var o: i64 = ca_cat(line, 0, "T" as *u8) 636 o = ca_catnum(line, o, ts) 637 o = ca_cat(line, o, "|F" as *u8) 638 o = ca_catn(line, o, fname, fnl) 639 o = ca_cat(line, o, "|" as *u8) 640 var i: i64 = 0 641 while i < bn { let c: i64 = body[i] as i64; if c != 10 { if c != 13 { line[o] = c as u8; o = o + 1 } } i = i + 1 } 642 line[o] = 10 as u8 643 o = o + 1 644 let fd: i64 = sys_openat_append(spath, 0x1a4) 645 if fd < 0 { return 0 } 646 sys_write(fd, line, o) 647 sys_close(fd) 648 return 1 649} 650 651// W3 forms: render the submissions log as a session-only list (escaped). Returns length. 652func ca_subs_page(out: *u8, spath: *u8) -> i64 { 653 let st: *u8 = sys_mmap(CA_REQCAP) 654 var sn: i64 = cst_read(spath, st, CA_REQCAP - 1) 655 if sn < 0 { sn = 0 } 656 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Submissions</title><style>body{font-family:Georgia,serif;max-width:760px;margin:4vh auto}li{border-bottom:1px solid #ccc;padding:8px 0;word-break:break-all}</style></head><body><h1>Form submissions <span style=\"font-size:.6em;color:#888\">(additive; never lost)</span></h1><p><a href=\"/admin\">&larr; editor</a></p><ul>" as *u8) 657 var count: i64 = 0 658 var i: i64 = 0 659 while i < sn { 660 var e: i64 = i 661 while e < sn { if (st[e] as i64) == 10 { break } e = e + 1 } 662 if e > i { 663 o = ca_cat(out, o, "<li>" as *u8) 664 o = o + hs_escape(((st as i64) + i) as *u8, e - i, ((out as i64) + o) as *u8, CA_OUTCAP - o) 665 o = ca_cat(out, o, "</li>" as *u8) 666 count = count + 1 667 } 668 i = e + 1 669 } 670 o = ca_cat(out, o, "</ul><p>total: " as *u8) 671 o = ca_catnum(out, o, count) 672 o = ca_cat(out, o, "</p></body></html>" as *u8) 673 return o 674} 675 676// build blocks_html from the store's "blocks" key via the TEAM-AUTHORED renderer (_pe_blockr). 677// blocks value = lines "K|payload" (K = kind digit). -1 = no blocks key. 678func ca_blocks_html(store: *u8, sn: i64, out: *u8, cap: i64) -> i64 { 679 let bl: *u8 = sys_mmap(CA_MAGIC_131072) 680 let bn: i64 = cst_get(store, sn, "blocks" as *u8, bl, CA_MAGIC_131071) 681 if bn < 0 { return 0 - 1 } 682 var o: i64 = 0 683 var i: i64 = 0 684 while i < bn { 685 var e: i64 = i 686 while e < bn { if (bl[e] as i64) == 10 { break } e = e + 1 } 687 if e > i + 1 { 688 let k: i64 = (bl[i] as i64) - 48 689 if (bl[i+1] as i64) == 124 { 690 let fn2: i64 = _pe_blockr_one(k, ((bl as i64) + i + 2) as *u8, e - i - 2, ((out as i64) + o) as *u8, cap - o) 691 if fn2 > 0 { 692 o = o + fn2 693 if o < cap { out[o] = 10 as u8; o = o + 1 } 694 } 695 } 696 } 697 i = e + 1 698 } 699 return o 700} 701 702// rebuild the blocks value applying op (0=add 1=set 2=del) at block index idx with line bytes. 703func ca_blocks_mut(old: *u8, on: i64, op: i64, idx: i64, line: *u8, ln2: i64, out: *u8, cap: i64) -> i64 { 704 var o: i64 = 0 705 var bi: i64 = 0 706 var i: i64 = 0 707 while i < on { 708 var e: i64 = i 709 while e < on { if (old[e] as i64) == 10 { break } e = e + 1 } 710 if e > i { 711 var keep: i64 = 1 712 if op == 2 { if bi == idx { keep = 0 } } 713 if op == 1 { if bi == idx { 714 var q3: i64 = 0 715 while q3 < ln2 { if o < cap { out[o] = line[q3]; o = o + 1 } q3 = q3 + 1 } 716 if o < cap { out[o] = 10 as u8; o = o + 1 } 717 keep = 0 718 } } 719 if keep == 1 { 720 var q4: i64 = i 721 while q4 < e { if o < cap { out[o] = old[q4]; o = o + 1 } q4 = q4 + 1 } 722 if o < cap { out[o] = 10 as u8; o = o + 1 } 723 } 724 bi = bi + 1 725 } 726 i = e + 1 727 } 728 if op == 0 { 729 var q5: i64 = 0 730 while q5 < ln2 { if o < cap { out[o] = line[q5]; o = o + 1 } q5 = q5 + 1 } 731 if o < cap { out[o] = 10 as u8; o = o + 1 } 732 } 733 return o 734} 735 736// mkdirat via runtime-computed x86 number (the portable-syscall idiom) 737func ca_mkdir(path: *u8, mode: i64) -> i64 { 738 let nb: *i64 = sys_mmap(8) as *i64 739 nb[0] = 258 740 return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) 741} 742 743// the blocks editor page (session-only): per-block edit/delete forms + add + upload 744func ca_blocks_editor(out: *u8, store: *u8, sn: i64, csrf: *u8) -> i64 { 745 var o: i64 = ca_cat(out, 0, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Blocks</title><style>body{font-family:Georgia,serif;max-width:720px;margin:4vh auto;padding:0 24px}textarea{width:100%;min-height:60px}fieldset{margin-top:1rem;border:1px solid #aab}button{padding:6px 14px}</style></head><body><h1>Block Editor</h1><p><a href=\"/admin\">&larr; fields</a> | <a href=\"/\">view site</a></p>" as *u8) 746 // existing blocks 747 let bl: *u8 = sys_mmap(CA_MAGIC_131072) 748 let bn: i64 = cst_get(store, sn, "blocks" as *u8, bl, CA_MAGIC_131071) 749 var i: i64 = 0 750 var bi: i64 = 0 751 if bn > 0 { 752 while i < bn { 753 var e: i64 = i 754 while e < bn { if (bl[e] as i64) == 10 { break } e = e + 1 } 755 if e > i + 1 { 756 o = ca_cat(out, o, "<fieldset><legend>block " as *u8) 757 o = ca_catnum(out, o, bi) 758 o = ca_cat(out, o, " kind " as *u8) 759 o = ca_catn(out, o, ((bl as i64) + i) as *u8, 1) 760 o = ca_cat(out, o, "</legend><form method=\"POST\" action=\"/admin/blocks\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 761 o = ca_catn(out, o, csrf, 32) 762 o = ca_cat(out, o, "\"><input type=\"hidden\" name=\"op\" value=\"set\"><input type=\"hidden\" name=\"idx\" value=\"" as *u8) 763 o = ca_catnum(out, o, bi) 764 o = ca_cat(out, o, "\"><input type=\"hidden\" name=\"kind\" value=\"" as *u8) 765 o = ca_catn(out, o, ((bl as i64) + i) as *u8, 1) 766 o = ca_cat(out, o, "\"><textarea name=\"payload\">" as *u8) 767 o = o + hs_escape(((bl as i64) + i + 2) as *u8, e - i - 2, ((out as i64) + o) as *u8, CA_OUTCAP - o) 768 o = ca_cat(out, o, "</textarea><button type=\"submit\">Save</button></form><form method=\"POST\" action=\"/admin/blocks\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 769 o = ca_catn(out, o, csrf, 32) 770 o = ca_cat(out, o, "\"><input type=\"hidden\" name=\"op\" value=\"del\"><input type=\"hidden\" name=\"idx\" value=\"" as *u8) 771 o = ca_catnum(out, o, bi) 772 o = ca_cat(out, o, "\"><button type=\"submit\">Delete</button></form></fieldset>" as *u8) 773 bi = bi + 1 774 } 775 i = e + 1 776 } 777 } 778 // add form (kinds: 1=p rich, 2=h2, 3=h3, 4=quote, 5=img src|alt) 779 o = ca_cat(out, o, "<fieldset><legend>add block</legend><form method=\"POST\" action=\"/admin/blocks\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 780 o = ca_catn(out, o, csrf, 32) 781 o = ca_cat(out, o, "\"><input type=\"hidden\" name=\"op\" value=\"add\"><label>kind (1=text 2=h2 3=h3 4=quote 5=img src|alt)</label><input name=\"kind\" value=\"1\"><label>payload</label><textarea name=\"payload\"></textarea><button type=\"submit\">Add</button></form></fieldset>" as *u8) 782 // upload form 783 o = ca_cat(out, o, "<fieldset><legend>upload image (png/jpg/gif)</legend><form method=\"POST\" action=\"/admin/upload\" enctype=\"multipart/form-data\"><input type=\"hidden\" name=\"csrf\" value=\"" as *u8) 784 o = ca_catn(out, o, csrf, 32) 785 o = ca_cat(out, o, "\"><input type=\"file\" name=\"file\"><button type=\"submit\">Upload</button></form></fieldset></body></html>" as *u8) 786 return o 787} 788 789// render the PENDING DRAFT store through the template into out (memory only, no file writes). 790// Returns page length or -1 (no draft / no template). 791func ca_render_draft(sitedir: *u8, out: *u8) -> i64 { 792 let dpath: *u8 = sys_mmap(512) 793 let tpath: *u8 = sys_mmap(512) 794 ca_path(dpath, sitedir, "draft.txt" as *u8) 795 ca_path(tpath, sitedir, "template.html" as *u8) 796 let store: *u8 = sys_mmap(CA_REQCAP) 797 var sn: i64 = cst_read(dpath, store, CA_REQCAP - 1) 798 if sn < 0 { return 0 - 1 } 799 let tpl: *u8 = sys_mmap(CA_REQCAP) 800 let tn: i64 = cst_read(tpath, tpl, CA_REQCAP - 1) 801 if tn < 0 { return 0 - 1 } 802 // C11: drafts render blocks too (same injection as publish) 803 let bh: *u8 = sys_mmap(CA_OUTCAP) 804 let bhn: i64 = ca_blocks_html(store, sn, bh, CA_OUTCAP - 1) 805 var rstore: *u8 = store 806 if bhn >= 0 { 807 let s2b: *u8 = sys_mmap(CA_REQCAP) 808 sn = cst_set(store, sn, "blocks_html" as *u8, bh, bhn, s2b, CA_REQCAP - 1) 809 rstore = s2b 810 } 811 return crd_render(tpl, tn, rstore, sn, out, CA_OUTCAP - 1) 812} 813 814// store one form field (sanitizing _html keys); returns new store length (ping-pongs s1->s2) 815func ca_save_field(body: *u8, bn: i64, key: *u8, ishtml: i64, s_in: *u8, n_in: i64, s_out: *u8) -> i64 { 816 let raw: *u8 = sys_mmap(CA_MAGIC_131072) 817 let vl: *i64 = sys_mmap(16) as *i64 818 if nx_http_form_get_field(body, bn, key, ca_len(key), raw, CA_MAGIC_131071, vl) != 0 { 819 // field absent -> copy store through unchanged 820 var i: i64 = 0 821 while i < n_in { s_out[i] = s_in[i]; i = i + 1 } 822 return n_in 823 } 824 if ishtml == 1 { 825 let clean: *u8 = sys_mmap(CA_MAGIC_131072) 826 let cl: i64 = hs_sanitize(raw, vl[0], clean, CA_MAGIC_131071) 827 return cst_set(s_in, n_in, key, clean, cl, s_out, CA_REQCAP - 1) 828 } 829 return cst_set(s_in, n_in, key, raw, vl[0], s_out, CA_REQCAP - 1) 830} 831 832func main(argc: i64, argv: *i64) -> i64 { 833 if argc < 4 { 834 sys_write(1, "usage: nx_cms_admin <port> <sitedir> <request-budget>\n" as *u8, 55) 835 sys_exit(2) 836 return 2 837 } 838 let port: i64 = ca_atoi(argv[1] as *u8) 839 let sitedir: *u8 = argv[2] as *u8 840 let budget: i64 = ca_atoi(argv[3] as *u8) 841 // C8: argv[4]="tls" -> session cookie gains Secure (set when deployed behind the TLS front) 842 var securecookie: i64 = 0 843 if argc > 4 { 844 let m4: *u8 = argv[4] as *u8 845 if m4[0] == (116 as u8) { if m4[1] == (108 as u8) { if m4[2] == (115 as u8) { if m4[3] == (0 as u8) { securecookie = 1 } } } } 846 } 847 848 // listen on 127.0.0.1:port (loopback ONLY v1 -- TLS termination is the sites-daemon rung) 849 let addr: *u8 = sys_mmap(16) 850 addr[0] = 2 as u8; addr[1] = 0 as u8 851 addr[2] = ((port >> 8) & 0xff) as u8 852 addr[3] = (port & 0xff) as u8 853 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8 854 var zi: i64 = 8 855 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 856 let lfd: i64 = sys_socket(2, 1, 0) // AF_INET, SOCK_STREAM 857 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 858 let optval: *u8 = sys_mmap(4) 859 optval[0] = 1 as u8; optval[1] = 0 as u8; optval[2] = 0 as u8; optval[3] = 0 as u8 860 sys_setsockopt(lfd, 1, 2, optval, 4) // SOL_SOCKET, SO_REUSEADDR 861 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 } 862 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 863 sys_write(1, "CMS-ADMIN-UP\n" as *u8, 13) 864 865 // fail-fast startup validation (rule 20): content + template + a credential must load. 866 // C10: admin_pw.ar2 (argon2id, self-describing params) is checked FIRST and always wins; 867 // admin_pw.sha256 is the legacy migration fallback ONLY -- a stale legacy file alongside a 868 // .ar2 can never downgrade the verify path. 869 let arpath: *u8 = sys_mmap(512) 870 ca_path(arpath, sitedir, "admin_pw.ar2" as *u8) 871 let arbuf: *u8 = sys_mmap(256) 872 let arn: i64 = cst_read(arpath, arbuf, 200) 873 let arbox: *i64 = sys_mmap(32) as *i64 874 let arsalt: *u8 = sys_mmap(64) 875 let artag: *u8 = sys_mmap(96) 876 var pwmode: i64 = 0 877 if arn > 0 { if cpw_parse(arbuf, arn, arbox, arsalt, artag) == 1 { pwmode = 1 } } 878 var arctx: *NxArgon2idCtx = 0 as *NxArgon2idCtx 879 if pwmode == 1 { arctx = cpw_ctx_new(arbox[0]) } // ONE m_kib-sized arena, reused per login 880 let pwpath: *u8 = sys_mmap(512) 881 ca_path(pwpath, sitedir, "admin_pw.sha256" as *u8) 882 let pwhex: *u8 = sys_mmap(128) 883 var pwn: i64 = 0 884 if pwmode == 0 { 885 pwn = cst_read(pwpath, pwhex, 80) 886 if pwn < 64 { sys_write(1, "NO-PW-HASH\n" as *u8, 11); sys_exit(1); return 1 } 887 sys_write(1, "PW-MODE-LEGACY-SHA256 (migrate: nx_cms_setpw2)\n" as *u8, 47) 888 } 889 if pwmode == 1 { sys_write(1, "PW-MODE-ARGON2ID\n" as *u8, 17) } 890 // #12: optional admin MFA -- base32 TOTP secret in <site>/admin_totp.b32. Absent/empty -> not enrolled 891 // -> password-only (graceful, backward-compatible). Present -> a valid 6-digit code is required at login. 892 let totppath: *u8 = sys_mmap(512) 893 ca_path(totppath, sitedir, "admin_totp.b32" as *u8) 894 let totpsec: *u8 = sys_mmap(64) 895 var totpseclen: i64 = cst_read(totppath, totpsec, 63) 896 if totpseclen < 0 { totpseclen = 0 } 897 var totp_trim: i64 = 0 898 while totp_trim == 0 { 899 if totpseclen <= 0 { totp_trim = 1 } else { 900 let tc: u8 = totpsec[totpseclen - 1] 901 if tc == (10 as u8) { totpseclen = totpseclen - 1 } else { 902 if tc == (13 as u8) { totpseclen = totpseclen - 1 } else { 903 if tc == (32 as u8) { totpseclen = totpseclen - 1 } else { totp_trim = 1 } } } 904 } 905 } 906 if mfa_enrolled(totpsec, totpseclen) == 1 { sys_write(1, "MFA-ENROLLED (admin_totp.b32, TOTP-SHA1)\n" as *u8, 41) } 907 if ca_render_publish(sitedir) != 1 { sys_write(1, "SEED-RENDER-FAIL\n" as *u8, 17); sys_exit(1); return 1 } 908 909 let sess: *u8 = sys_mmap(64) // sess[0]=0 -> none 910 let csrf: *u8 = sys_mmap(64) 911 sess[0] = 0 as u8 912 var failed_logins: i64 = 0 913 var pubstate: i64 = 1 // PUBLISHED (seed render above) 914 // CLIENT PORTAL (role 2): separate session + credential. Enabled iff <site>/client_pw.sha256 exists 915 // (sha256 hex of the client password). The lawyer/staff role stays the existing /admin login. 916 let csess: *u8 = sys_mmap(64) 917 csess[0] = 0 as u8 918 var client_failed: i64 = 0 919 let cpwpath: *u8 = sys_mmap(512) 920 ca_path(cpwpath, sitedir, "client_pw.sha256" as *u8) 921 let cpwhex: *u8 = sys_mmap(128) 922 var has_client: i64 = 0 923 let cpwn: i64 = cst_read(cpwpath, cpwhex, 80) 924 if cpwn >= 64 { has_client = 1; sys_write(1, "CLIENT-PORTAL-ENABLED\n" as *u8, 22) } 925 926 let cpath: *u8 = sys_mmap(512) 927 ca_path(cpath, sitedir, "content.txt" as *u8) 928 let ppath: *u8 = sys_mmap(512) 929 ca_path(ppath, sitedir, "page.html" as *u8) 930 // C9: draft lifecycle (team-authored _pe_draft guard). A draft pending on disk survives restarts. 931 let dpath: *u8 = sys_mmap(512) 932 ca_path(dpath, sitedir, "draft.txt" as *u8) 933 let dlast: *u8 = sys_mmap(512) 934 ca_path(dlast, sitedir, "draft.last" as *u8) 935 // W5: first-party analytics store (additive per-path counters; survives restart) 936 let apath: *u8 = sys_mmap(512) 937 ca_path(apath, sitedir, "analytics.txt" as *u8) 938 var draftstate: i64 = 0 // 0=CLEAN 1=DRAFTED (the _pe_draft states) 939 let dchk: i64 = sys_openat_rd(dpath) 940 if dchk >= 0 { draftstate = 1; sys_close(dchk) } 941 // C11: uploads dir + size cap from <site>/upload.conf ("max_bytes=N"; bootstrap default 200000, 942 // bounded by the request cap -- rule 17 config chain, rule 11 no magic numbers in routes) 943 let updir: *u8 = sys_mmap(512) 944 ca_path(updir, sitedir, "uploads" as *u8) 945 ca_mkdir(updir, 0x1ed) 946 var maxup: i64 = CA_MAGIC_200000 947 let ucpath: *u8 = sys_mmap(512) 948 ca_path(ucpath, sitedir, "upload.conf" as *u8) 949 let ucbuf: *u8 = sys_mmap(256) 950 let ucn: i64 = cst_read(ucpath, ucbuf, 200) 951 if ucn > 0 { maxup = ca_conf_int(ucbuf, ucn, "max_bytes=" as *u8, 10, maxup) } 952 if maxup > CA_REQCAP - CA_MAGIC_4096 { maxup = CA_REQCAP - CA_MAGIC_4096 } 953 954 // W3: forms config (rule 11/17) + per-IP rate table (in-memory; the accept loop is sequential) 955 let spath: *u8 = sys_mmap(512) 956 ca_path(spath, sitedir, "submissions.txt" as *u8) 957 var form_max: i64 = 5 // max submissions per IP per window 958 var form_window: i64 = CA_MAGIC_3600 // seconds 959 let fcpath: *u8 = sys_mmap(512) 960 ca_path(fcpath, sitedir, "form.conf" as *u8) 961 let fcbuf: *u8 = sys_mmap(256) 962 let fcn: i64 = cst_read(fcpath, fcbuf, 200) 963 if fcn > 0 { 964 form_max = ca_conf_int(fcbuf, fcn, "max_per_window=" as *u8, 15, form_max) 965 form_window = ca_conf_int(fcbuf, fcn, "window_sec=" as *u8, 11, form_window) 966 } 967 let rl_ip: *i64 = sys_mmap(8 * 64) as *i64 // peer IPv4 as int 968 let rl_cnt: *i64 = sys_mmap(8 * 64) as *i64 969 let rl_ws: *i64 = sys_mmap(8 * 64) as *i64 970 let rl_nbox: *i64 = sys_mmap(16) as *i64 971 rl_nbox[0] = 0 972 let sock_addr: *u8 = sys_mmap(64) 973 let sock_len: *i64 = sys_mmap(16) as *i64 974 975 var served: i64 = 0 976 while served < budget { 977 sock_len[0] = 16 978 let cfd: i64 = sys_accept_with_addr(lfd, sock_addr, sock_len) 979 var peer_ip: i64 = 0 980 peer_ip = ((sock_addr[4] as i64) << 24) + ((sock_addr[5] as i64) << 16) + ((sock_addr[6] as i64) << 8) + (sock_addr[7] as i64) 981 if cfd >= 0 { 982 sys_set_socket_timeout(cfd, 5) 983 let req: *u8 = sys_mmap(CA_REQCAP) 984 let hend_o: *i64 = sys_mmap(16) as *i64 985 let rn: i64 = ca_read_req(cfd, req, CA_REQCAP - 1, hend_o) 986 let hend: i64 = hend_o[0] 987 let out: *u8 = sys_mmap(CA_OUTCAP) 988 var done: i64 = 0 989 if rn <= 0 { done = 1 } 990 if hend < 0 { done = 1 } 991 992 // ---- GET / : the published page (W5: count the view first-party) ---- 993 if done == 0 { if ca_index(req, rn, "GET / HTTP" as *u8, 10) == 0 { 994 let page: *u8 = sys_mmap(CA_OUTCAP) 995 let pn: i64 = cst_read(ppath, page, CA_OUTCAP - 1) 996 if pn >= 0 { ca_analytics_hit(apath, "/" as *u8); ca_resp(cfd, "200 OK" as *u8, "" as *u8, page, pn) } 997 if pn < 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "not published\n" as *u8, 14) } 998 done = 1 999 } } 1000 1001 // ---- WEBDEV STUDIO: MOVED out of this daemon. The capability-adaptive studio for nishifamily.com/webdev 1002 // now lives in its OWN daemon (nx_sitegen_studio_daemon) BEHIND THE CANONICAL Modern Auth 1003 // (OPAQUE-3DH + NO-COOKIE X-Nishi-Session via nx_site_auth) -- the same login every family site uses. 1004 // nx_cms_admin is the andelinwest CMS (its pre-charter cookie auth is separate migration debt). The 1005 // earlier cookie-based /webdev routes here were a mismatch and have been removed. ---- 1006 1007 // ---- W3: POST /form/<name> : public form submission (honeypot + per-IP rate) ---- 1008 if done == 0 { if ca_index(req, rn, "POST /form/" as *u8, 11) == 0 { 1009 let body: *u8 = (req + hend + 4) as *u8 1010 let bn: i64 = rn - hend - 4 1011 // form name from the path (after "/form/", up to space) 1012 let fname: *u8 = sys_mmap(128) 1013 var fnl: i64 = 0 1014 var fp: i64 = 11 1015 var goingf: i64 = 1 1016 while goingf == 1 { 1017 goingf = 0 1018 if fp < rn { if fnl < 100 { 1019 let cf: i64 = req[fp] as i64 1020 if cf != 32 { if cf != 13 { if cf != 10 { if cf != 63 { 1021 var okc2: i64 = 0 1022 if cf >= 97 { if cf <= 122 { okc2 = 1 } } 1023 if cf >= 65 { if cf <= 90 { okc2 = 1 } } 1024 if cf >= 48 { if cf <= 57 { okc2 = 1 } } 1025 if cf == 95 { okc2 = 1 } 1026 if cf == 45 { okc2 = 1 } 1027 if okc2 == 1 { fname[fnl] = cf as u8; fnl = fnl + 1 } 1028 fp = fp + 1; goingf = 1 1029 } } } } 1030 } } 1031 } 1032 // honeypot: a non-empty hp_url field = a bot. Accept-looking (200) but DISCARD. 1033 let hp: *u8 = sys_mmap(256) 1034 let hpl: *i64 = sys_mmap(16) as *i64 1035 var spam: i64 = 0 1036 if nx_http_form_get_field(body, bn, "hp_url" as *u8, 6, hp, 255, hpl) == 0 { if hpl[0] > 0 { spam = 1 } } 1037 if fnl == 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "no such form\n" as *u8, 13) } 1038 if fnl > 0 { 1039 if spam == 1 { ca_resp(cfd, "200 OK" as *u8, "" as *u8, "thanks\n" as *u8, 7) } 1040 if spam == 0 { 1041 if ca_rate_ok(rl_ip, rl_cnt, rl_ws, rl_nbox, peer_ip, sys_now_realtime_sec(), form_window, form_max) == 0 { 1042 ca_resp(cfd, "429 Too Many Requests" as *u8, "" as *u8, "rate limited\n" as *u8, 13) 1043 } else { 1044 ca_submit_append(spath, sys_now_realtime_sec(), fname, fnl, body, bn) 1045 ca_resp(cfd, "303 See Other" as *u8, "Location: /\r\n" as *u8, "thanks\n" as *u8, 7) 1046 } 1047 } 1048 } 1049 done = 1 1050 } } 1051 1052 // ---- W4: POST /admin/snapshot : pack the site into snapshot.nxsnap (session + CSRF) ---- 1053 if done == 0 { if ca_index(req, rn, "POST /admin/snapshot" as *u8, 20) == 0 { 1054 let body: *u8 = (req + hend + 4) as *u8 1055 let bn: i64 = rn - hend - 4 1056 var authed: i64 = ca_session_ok(req, hend, sess) 1057 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1058 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1059 if authed == 1 { 1060 let names: *i64 = sys_mmap(8 * 8) as *i64 1061 names[0] = "content.txt" as *u8 as i64 1062 names[1] = "template.html" as *u8 as i64 1063 names[2] = "analytics.txt" as *u8 as i64 1064 names[3] = "submissions.txt" as *u8 as i64 1065 let arc: *u8 = sys_mmap(CA_OUTCAP) 1066 let al: i64 = snap_pack(sitedir, names, 4, arc, CA_OUTCAP - 1) 1067 var sok: i64 = 0 1068 if al > 0 { 1069 let spp: *u8 = sys_mmap(512) 1070 ca_path(spp, sitedir, "snapshot.nxsnap" as *u8) 1071 if cst_write_atomic(spp, arc, al) == 1 { sok = 1 } 1072 } 1073 if sok == 1 { ca_resp(cfd, "303 See Other" as *u8, "Location: /admin\r\n" as *u8, "snapshot saved\n" as *u8, 15) } 1074 if sok == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "snapshot failed\n" as *u8, 16) } 1075 } 1076 done = 1 1077 } } 1078 1079 // ---- W4: GET /admin/snapshot.nxsnap : download the snapshot (session) ---- 1080 if done == 0 { if ca_index(req, rn, "GET /admin/snapshot.nxsnap" as *u8, 26) == 0 { 1081 if ca_session_ok(req, hend, sess) == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1082 if ca_session_ok(req, hend, sess) == 1 { 1083 let spp2: *u8 = sys_mmap(512) 1084 ca_path(spp2, sitedir, "snapshot.nxsnap" as *u8) 1085 let ab: *u8 = sys_mmap(CA_OUTCAP) 1086 let an2: i64 = cst_read(spp2, ab, CA_OUTCAP - 1) 1087 if an2 < 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "no snapshot\n" as *u8, 12) } 1088 if an2 >= 0 { ca_resp_ct(cfd, "200 OK" as *u8, "application/octet-stream" as *u8, ab, an2) } 1089 } 1090 done = 1 1091 } } 1092 1093 // ---- W4: POST /admin/restore : restore from snapshot.nxsnap byte-exact (session + CSRF) ---- 1094 if done == 0 { if ca_index(req, rn, "POST /admin/restore" as *u8, 19) == 0 { 1095 let body: *u8 = (req + hend + 4) as *u8 1096 let bn: i64 = rn - hend - 4 1097 var authed: i64 = ca_session_ok(req, hend, sess) 1098 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1099 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1100 if authed == 1 { 1101 let spp3: *u8 = sys_mmap(512) 1102 ca_path(spp3, sitedir, "snapshot.nxsnap" as *u8) 1103 let ab2: *u8 = sys_mmap(CA_OUTCAP) 1104 let an3: i64 = cst_read(spp3, ab2, CA_OUTCAP - 1) 1105 var rok: i64 = 0 1106 if an3 > 0 { 1107 if snap_unpack(ab2, an3, sitedir) >= 0 { 1108 if ca_render_publish(sitedir) == 1 { rok = 1 } 1109 } 1110 } 1111 if rok == 1 { ca_resp(cfd, "303 See Other" as *u8, "Location: /\r\n" as *u8, "restored\n" as *u8, 9) } 1112 if rok == 0 { ca_resp(cfd, "409 Conflict" as *u8, "" as *u8, "no snapshot to restore\n" as *u8, 23) } 1113 } 1114 done = 1 1115 } } 1116 1117 // ---- W3: GET /admin/submissions : session-only submissions list ---- 1118 if done == 0 { if ca_index(req, rn, "GET /admin/submissions" as *u8, 22) == 0 { 1119 if ca_session_ok(req, hend, sess) == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1120 if ca_session_ok(req, hend, sess) == 1 { 1121 let sl: i64 = ca_subs_page(out, spath) 1122 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, sl) 1123 } 1124 done = 1 1125 } } 1126 1127 // ---- W5: GET /admin/stats : session-only first-party analytics ---- 1128 if done == 0 { if ca_index(req, rn, "GET /admin/stats" as *u8, 16) == 0 { 1129 if ca_session_ok(req, hend, sess) == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1130 if ca_session_ok(req, hend, sess) == 1 { 1131 let sl: i64 = ca_stats_page(out, apath) 1132 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, sl) 1133 } 1134 done = 1 1135 } } 1136 1137 // ---- W2: GET /robots.txt (public; references the sitemap when site_url is set) ---- 1138 if done == 0 { if ca_index(req, rn, "GET /robots.txt" as *u8, 15) == 0 { 1139 let st12: *u8 = sys_mmap(CA_REQCAP) 1140 let sn12: i64 = cst_read(cpath, st12, CA_REQCAP - 1) 1141 let url12: *u8 = sys_mmap(CA_MAGIC_2048) 1142 var ul12: i64 = 0 1143 if sn12 >= 0 { ul12 = cst_get(st12, sn12, "site_url" as *u8, url12, CA_MAGIC_2047) } 1144 let rb12: *u8 = sys_mmap(CA_MAGIC_4096) 1145 var ro12: i64 = ca_cat(rb12, 0, "User-agent: *\nAllow: /\n" as *u8) 1146 if ul12 > 0 { 1147 ro12 = ca_cat(rb12, ro12, "Sitemap: " as *u8) 1148 ro12 = ca_catn(rb12, ro12, url12, ul12) 1149 if (url12[ul12-1] as i64) != 47 { rb12[ro12] = 47 as u8; ro12 = ro12 + 1 } 1150 ro12 = ca_cat(rb12, ro12, "sitemap.xml\n" as *u8) 1151 } 1152 ca_resp_ct(cfd, "200 OK" as *u8, "text/plain; charset=utf-8" as *u8, rb12, ro12) 1153 done = 1 1154 } } 1155 1156 // ---- W2: GET /sitemap.xml (public; lists the published home page) ---- 1157 if done == 0 { if ca_index(req, rn, "GET /sitemap.xml" as *u8, 16) == 0 { 1158 let st13: *u8 = sys_mmap(CA_REQCAP) 1159 let sn13: i64 = cst_read(cpath, st13, CA_REQCAP - 1) 1160 let url13: *u8 = sys_mmap(CA_MAGIC_2048) 1161 var ul13: i64 = 0 1162 if sn13 >= 0 { ul13 = cst_get(st13, sn13, "site_url" as *u8, url13, CA_MAGIC_2047) } 1163 let sm13: *u8 = sys_mmap(CA_MAGIC_4096) 1164 var so13: i64 = ca_cat(sm13, 0, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n<url><loc>" as *u8) 1165 if ul13 > 0 { 1166 so13 = so13 + hs_escape(url13, ul13, ((sm13 as i64) + so13) as *u8, CA_MAGIC_4095 - so13) 1167 if (url13[ul13-1] as i64) != 47 { sm13[so13] = 47 as u8; so13 = so13 + 1 } 1168 } 1169 so13 = ca_cat(sm13, so13, "</loc></url>\n</urlset>\n" as *u8) 1170 ca_resp_ct(cfd, "200 OK" as *u8, "application/xml; charset=utf-8" as *u8, sm13, so13) 1171 done = 1 1172 } } 1173 1174 // ---- GET /admin : login or editor ---- 1175 if done == 0 { if ca_index(req, rn, "GET /admin HTTP" as *u8, 15) == 0 { 1176 if ca_session_ok(req, hend, sess) == 1 { 1177 let store: *u8 = sys_mmap(CA_REQCAP) 1178 let sn: i64 = cst_read(cpath, store, CA_REQCAP - 1) 1179 let el: i64 = ca_editor_page(out, store, sn, csrf) 1180 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, el) 1181 } 1182 if ca_session_ok(req, hend, sess) == 0 { 1183 var lk: i64 = 0 1184 if failed_logins >= CA_LOCKOUT { lk = 1 } 1185 let ll: i64 = ca_login_page(out, lk) 1186 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, ll) 1187 } 1188 done = 1 1189 } } 1190 1191 // ---- POST /admin/login ---- 1192 if done == 0 { if ca_index(req, rn, "POST /admin/login" as *u8, 17) == 0 { 1193 var ok: i64 = 0 1194 if failed_logins < CA_LOCKOUT { 1195 let body: *u8 = (req + hend + 4) as *u8 1196 let bn: i64 = rn - hend - 4 1197 let pw: *u8 = sys_mmap(512) 1198 let pl: *i64 = sys_mmap(16) as *i64 1199 if nx_http_form_get_field(body, bn, "pw" as *u8, 2, pw, 511, pl) == 0 { 1200 if pwmode == 1 { 1201 // C10: memory-hard verify; params came from the credential file 1202 let thx: *u8 = sys_mmap(96) 1203 if cpw_derive(arctx, pw, pl[0], arsalt, arbox[0], arbox[1], thx) == 1 { 1204 if st_eq_ct(thx, artag, 64) == 1 { ok = 1 } 1205 } 1206 } 1207 if pwmode == 0 { 1208 let dig: *u8 = sys_mmap(32) 1209 sha256_digest(pw, pl[0], dig) 1210 let hex: *u8 = sys_mmap(80) 1211 let hxc: *u8 = "0123456789abcdef" as *u8 1212 var i: i64 = 0 1213 while i < 32 { 1214 hex[i*2] = hxc[((dig[i] as i64) >> 4) & 15] 1215 hex[i*2+1] = hxc[(dig[i] as i64) & 15] 1216 i = i + 1 1217 } 1218 if st_eq_ct(hex, pwhex, 64) == 1 { ok = 1 } 1219 } 1220 } 1221 } 1222 // #12: second factor -- when MFA is enrolled, the password alone is NOT enough; a valid TOTP 1223 // code is also required. A missing/wrong code flips ok->0 (so it also counts toward lockout). 1224 if ok == 1 { if ca_mfa_ok((req + hend + 4) as *u8, rn - hend - 4, totpsec, totpseclen, sys_now_realtime_sec()) == 0 { ok = 0 } } 1225 if ok == 1 { 1226 failed_logins = 0 1227 st_hex128(sess) 1228 st_hex128(csrf) 1229 sess_issue(sess, sys_now_realtime_ms()) // #8: stamp issued_at + last_seen for this fresh (rotated) token 1230 var o: i64 = ca_cat(out, 0, "Set-Cookie: nsess=" as *u8) 1231 o = ca_catn(out, o, sess, 32) 1232 o = ca_cat(out, o, "; HttpOnly; Path=/; SameSite=Strict" as *u8) 1233 if securecookie == 1 { o = ca_cat(out, o, "; Secure" as *u8) } 1234 o = ca_cat(out, o, "\r\nLocation: /admin\r\n" as *u8) 1235 out[o] = 0 as u8 1236 ca_resp(cfd, "303 See Other" as *u8, out, "ok\n" as *u8, 3) 1237 } 1238 if ok == 0 { 1239 failed_logins = failed_logins + 1 1240 ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "bad credentials\n" as *u8, 16) 1241 } 1242 done = 1 1243 } } 1244 1245 // ---- POST /admin/save ---- 1246 if done == 0 { if ca_index(req, rn, "POST /admin/save" as *u8, 16) == 0 { 1247 let body: *u8 = (req + hend + 4) as *u8 1248 let bn: i64 = rn - hend - 4 1249 var authed: i64 = ca_session_ok(req, hend, sess) 1250 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1251 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1252 if authed == 1 { 1253 // C9: mode=draft routes this save to the pending working copy (public untouched) 1254 let mv: *u8 = sys_mmap(32) 1255 let ml: *i64 = sys_mmap(16) as *i64 1256 var isdraft: i64 = 0 1257 if nx_http_form_get_field(body, bn, "mode" as *u8, 4, mv, 31, ml) == 0 { 1258 if ml[0] == 5 { if ca_index(mv, 5, "draft" as *u8, 5) == 0 { isdraft = 1 } } 1259 } 1260 let s1: *u8 = sys_mmap(CA_REQCAP) 1261 let s2: *u8 = sys_mmap(CA_REQCAP) 1262 var n: i64 = 0 - 1 1263 if isdraft == 1 { n = cst_read(dpath, s1, CA_REQCAP - 1) } // keep drafting on the draft 1264 if n < 0 { n = cst_read(cpath, s1, CA_REQCAP - 1) } 1265 if n < 0 { n = 0 } 1266 n = ca_save_field(body, bn, "hero_title" as *u8, 0, s1, n, s2) 1267 n = ca_save_field(body, bn, "hero_tag" as *u8, 0, s2, n, s1) 1268 n = ca_save_field(body, bn, "note_html" as *u8, 1, s1, n, s2) 1269 n = ca_save_field(body, bn, "phone" as *u8, 0, s2, n, s1) 1270 n = ca_save_field(body, bn, "email" as *u8, 0, s1, n, s2) 1271 n = ca_save_field(body, bn, "address" as *u8, 0, s2, n, s1) 1272 n = ca_save_field(body, bn, "hours" as *u8, 0, s1, n, s2) 1273 n = ca_save_field(body, bn, "practice_areas" as *u8, 0, s2, n, s1) 1274 n = ca_save_field(body, bn, "foot" as *u8, 0, s1, n, s2) 1275 // SEO fields (plain text, escaped at render) 1276 n = ca_save_field(body, bn, "seo_title" as *u8, 0, s2, n, s1) 1277 n = ca_save_field(body, bn, "seo_desc" as *u8, 0, s1, n, s2) 1278 // ★og_image WAS MISSING FROM THE SAVE PATH (found 2026-08-01 by nx_cms_seo_gate row 4, 1279 // once the gate was repaired enough to actually reach its subject). The renderer 1280 // ca_seo_head READS og_image, the editor form POSTS og_image, and the capability 1281 // register ADVERTISES og:image as part of the SEO head vocabulary -- but this save 1282 // handler never persisted it, so the tag could never appear on a rendered page and 1283 // nothing anywhere said why. ★A FIELD THAT IS ACCEPTED BY THE FORM AND READ BY THE 1284 // RENDERER BUT DROPPED IN BETWEEN FAILS SILENTLY AND LOOKS LIKE A RENDERER BUG. 1285 // ⚠⚠AND THE INSERTION ITSELF TAUGHT THE SECOND HALF OF THE LESSON. These calls 1286 // PING-PONG between two scratch buffers -- each must READ the buffer the previous 1287 // one WROTE. My first attempt read s1 (the pre-seo_desc state) and so silently 1288 // DISCARDED seo_desc: og:image appeared and the XSS-escaping row went red instead. 1289 // ★IN A PING-PONG BUFFER CHAIN, INSERTING ONE STEP FLIPS THE PARITY OF EVERY STEP 1290 // AFTER IT -- and a wrong link does not crash, it silently reverts the field before 1291 // it. Correct chain: seo_title s2->s1, seo_desc s1->s2, og_image s2->s1, 1292 // site_url s1->s2, so the RESULT NOW LIVES IN s2 and both writers below say s2. 1293 // ✅CONTROL RUN COMPLETED 2026-08-01, RESULT RECORDED: with this og_image line REMOVED 1294 // and the parity reverted, nx_cms_gate row 08 (stored-XSS sanitize) STAYED RED -- 1295 // so the XSS reds are PRE-EXISTING and this change is exonerated BY MEASUREMENT, 1296 // not by plausibility. Line restored below. ★WHEN YOUR OWN EDIT IS A SUSPECT, THE 1297 // CONTROL IS CHEAPER THAN THE ARGUMENT -- two builds settled what an hour of 1298 // reasoning could not, and would have caught the opposite result just as fast. 1299 n = ca_save_field(body, bn, "og_image" as *u8, 0, s2, n, s1) 1300 n = ca_save_field(body, bn, "site_url" as *u8, 0, s1, n, s2) 1301 if isdraft == 1 { 1302 let dst: i64 = _pe_draft_step(draftstate, 0) // team-authored guard 1303 var dsaved: i64 = 0 1304 if dst >= 0 { 1305 if cst_write_atomic(dpath, s2, n) == 1 { dsaved = 1; draftstate = dst } 1306 } 1307 if dsaved == 1 { ca_resp(cfd, "303 See Other" as *u8, "Location: /admin/preview\r\n" as *u8, "draft saved\n" as *u8, 12) } 1308 if dsaved == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "draft save failed\n" as *u8, 18) } 1309 } 1310 if isdraft == 0 { 1311 // team-authored lifecycle: EDIT then PUBLISH (both must be legal) 1312 var st: i64 = _pe_pub_step(pubstate, 0) 1313 if st >= 0 { st = _pe_pub_step(st, 1) } 1314 var saved: i64 = 0 1315 if st >= 0 { 1316 if cst_write_atomic(cpath, s2, n) == 1 { 1317 if ca_render_publish(sitedir) == 1 { saved = 1; pubstate = st } 1318 } 1319 } 1320 if saved == 1 { ca_resp(cfd, "303 See Other" as *u8, "Location: /\r\n" as *u8, "published\n" as *u8, 10) } 1321 if saved == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "publish failed\n" as *u8, 15) } 1322 } 1323 } 1324 done = 1 1325 } } 1326 1327 // ---- GET /uploads/<name> : public image serve (sanitized names only, C11) ---- 1328 if done == 0 { if ca_index(req, rn, "GET /uploads/" as *u8, 13) == 0 { 1329 let nm: *u8 = sys_mmap(256) 1330 var nl2: i64 = 0 1331 var p7: i64 = 13 1332 var going7: i64 = 1 1333 while going7 == 1 { 1334 going7 = 0 1335 if p7 < rn { if nl2 < 120 { 1336 let c7: i64 = req[p7] as i64 1337 if c7 != 32 { if c7 != 13 { if c7 != 10 { nm[nl2] = c7 as u8; nl2 = nl2 + 1; p7 = p7 + 1; going7 = 1 } } } 1338 } } 1339 } 1340 var served7: i64 = 0 1341 if ca_sane_name(nm, nl2) == 1 { 1342 let t7: i64 = ca_img_type(nm, nl2) 1343 if t7 >= 0 { 1344 let up7: *u8 = sys_mmap(512) 1345 var uo7: i64 = ca_cat(up7, 0, updir) 1346 uo7 = ca_cat(up7, uo7, "/" as *u8) 1347 nm[nl2] = 0 as u8 1348 uo7 = ca_cat(up7, uo7, nm) 1349 up7[uo7] = 0 as u8 1350 let fb7: *u8 = sys_mmap(CA_OUTCAP) 1351 let fn7: i64 = cst_read(up7, fb7, CA_OUTCAP - 1) 1352 if fn7 >= 0 { ca_resp_ct(cfd, "200 OK" as *u8, ca_img_ctype(t7), fb7, fn7); served7 = 1 } 1353 } 1354 } 1355 if served7 == 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "no such upload\n" as *u8, 15) } 1356 done = 1 1357 } } 1358 1359 // ---- GET /admin/blocks : the block editor (session-only, C11) ---- 1360 if done == 0 { if ca_index(req, rn, "GET /admin/blocks" as *u8, 17) == 0 { 1361 if ca_session_ok(req, hend, sess) == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1362 if ca_session_ok(req, hend, sess) == 1 { 1363 let store8: *u8 = sys_mmap(CA_REQCAP) 1364 let sn8: i64 = cst_read(cpath, store8, CA_REQCAP - 1) 1365 let el8: i64 = ca_blocks_editor(out, store8, sn8, csrf) 1366 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, el8) 1367 } 1368 done = 1 1369 } } 1370 1371 // ---- POST /admin/blocks : block ops add/set/del (session + CSRF, C11) ---- 1372 if done == 0 { if ca_index(req, rn, "POST /admin/blocks" as *u8, 18) == 0 { 1373 let body: *u8 = (req + hend + 4) as *u8 1374 let bn: i64 = rn - hend - 4 1375 var authed: i64 = ca_session_ok(req, hend, sess) 1376 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1377 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1378 if authed == 1 { 1379 // fields: op (add|set|del), idx, kind (1..5), payload 1380 let opv: *u8 = sys_mmap(16) 1381 let opl: *i64 = sys_mmap(16) as *i64 1382 var op9: i64 = 0 - 1 1383 if nx_http_form_get_field(body, bn, "op" as *u8, 2, opv, 15, opl) == 0 { 1384 if opl[0] == 3 { if ca_index(opv, 3, "add" as *u8, 3) == 0 { op9 = 0 } } 1385 if opl[0] == 3 { if ca_index(opv, 3, "set" as *u8, 3) == 0 { op9 = 1 } } 1386 if opl[0] == 3 { if ca_index(opv, 3, "del" as *u8, 3) == 0 { op9 = 2 } } 1387 } 1388 let idxv: *u8 = sys_mmap(16) 1389 let idxl: *i64 = sys_mmap(16) as *i64 1390 var idx9: i64 = 0 1391 if nx_http_form_get_field(body, bn, "idx" as *u8, 3, idxv, 15, idxl) == 0 { 1392 var q9: i64 = 0 1393 while q9 < idxl[0] { 1394 let c8: i64 = idxv[q9] as i64 1395 if c8 >= 48 { if c8 <= 57 { idx9 = idx9 * 10 + (c8 - 48) } } 1396 q9 = q9 + 1 1397 } 1398 } 1399 let kv: *u8 = sys_mmap(16) 1400 let kl9: *i64 = sys_mmap(16) as *i64 1401 var kind9: i64 = 0 - 1 1402 if nx_http_form_get_field(body, bn, "kind" as *u8, 4, kv, 15, kl9) == 0 { 1403 if kl9[0] == 1 { kind9 = (kv[0] as i64) - 48 } 1404 } 1405 var pay9: *u8 = sys_mmap(CA_MAGIC_131072) 1406 let pl9: *i64 = sys_mmap(16) as *i64 1407 pl9[0] = 0 1408 nx_http_form_get_field(body, bn, "payload" as *u8, 7, pay9, CA_MAGIC_131071, pl9) 1409 // del needs no kind/payload; add/set validate kind + payload 1410 var valid9: i64 = 0 1411 if op9 == 2 { valid9 = 1 } 1412 if op9 >= 0 { if op9 <= 1 { if kind9 >= 1 { if kind9 <= 5 { valid9 = 1 } } } } 1413 var line9: *u8 = sys_mmap(CA_MAGIC_131080) 1414 var ln9: i64 = 0 1415 if valid9 == 1 { if op9 <= 1 { 1416 // payload: strip CR, LF -> space (a block is ONE line in the store) 1417 var w9: i64 = 0 1418 var r9: i64 = 0 1419 while r9 < pl9[0] { 1420 let c10: i64 = pay9[r9] as i64 1421 if c10 == 13 { r9 = r9 + 1 } 1422 if c10 != 13 { 1423 if c10 == 10 { pay9[w9] = 32 as u8 } 1424 if c10 != 10 { pay9[w9] = c10 as u8 } 1425 w9 = w9 + 1 1426 r9 = r9 + 1 1427 } 1428 } 1429 pl9[0] = w9 1430 // rich kinds sanitized ON WRITE; img payload must be /uploads/<sane>|alt 1431 if kind9 == 1 { let cl9: *u8 = sys_mmap(CA_MAGIC_131072); let cn9: i64 = hs_sanitize(pay9, pl9[0], cl9, CA_MAGIC_131071); pay9 = cl9; pl9[0] = cn9 } 1432 if kind9 == 4 { let cl0: *u8 = sys_mmap(CA_MAGIC_131072); let cn0: i64 = hs_sanitize(pay9, pl9[0], cl0, CA_MAGIC_131071); pay9 = cl0; pl9[0] = cn0 } 1433 if kind9 == 5 { 1434 var sp9: i64 = 0 - 1 1435 var si9: i64 = 0 1436 while si9 < pl9[0] { if (pay9[si9] as i64) == 124 { if sp9 < 0 { sp9 = si9 } } si9 = si9 + 1 } 1437 valid9 = 0 1438 if sp9 > 9 { if ca_index(pay9, 9, "/uploads/" as *u8, 9) == 0 { 1439 if ca_sane_name(((pay9 as i64) + 9) as *u8, sp9 - 9) == 1 { valid9 = 1 } 1440 } } 1441 } 1442 line9[0] = (48 + kind9) as u8 1443 line9[1] = 124 as u8 1444 var q0: i64 = 0 1445 while q0 < pl9[0] { line9[2 + q0] = pay9[q0]; q0 = q0 + 1 } 1446 ln9 = 2 + pl9[0] 1447 } } 1448 if valid9 == 0 { ca_resp(cfd, "400 Bad Request" as *u8, "" as *u8, "bad block\n" as *u8, 10) } 1449 if valid9 == 1 { 1450 let s1b: *u8 = sys_mmap(CA_REQCAP) 1451 var nb9: i64 = cst_read(cpath, s1b, CA_REQCAP - 1) 1452 if nb9 < 0 { nb9 = 0 } 1453 let oldv: *u8 = sys_mmap(CA_MAGIC_131072) 1454 var ovn: i64 = cst_get(s1b, nb9, "blocks" as *u8, oldv, CA_MAGIC_131071) 1455 if ovn < 0 { ovn = 0 } 1456 let newv: *u8 = sys_mmap(CA_MAGIC_131072) 1457 let nvn: i64 = ca_blocks_mut(oldv, ovn, op9, idx9, line9, ln9, newv, CA_MAGIC_131071) 1458 let s2c: *u8 = sys_mmap(CA_REQCAP) 1459 let n2c: i64 = cst_set(s1b, nb9, "blocks" as *u8, newv, nvn, s2c, CA_REQCAP - 1) 1460 var st9: i64 = _pe_pub_step(pubstate, 0) 1461 if st9 >= 0 { st9 = _pe_pub_step(st9, 1) } 1462 var ok9: i64 = 0 1463 if st9 >= 0 { 1464 if cst_write_atomic(cpath, s2c, n2c) == 1 { 1465 if ca_render_publish(sitedir) == 1 { ok9 = 1; pubstate = st9 } 1466 } 1467 } 1468 if ok9 == 1 { ca_resp(cfd, "303 See Other" as *u8, "Location: /admin/blocks\r\n" as *u8, "blocks saved\n" as *u8, 13) } 1469 if ok9 == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "blocks save failed\n" as *u8, 19) } 1470 } 1471 } 1472 done = 1 1473 } } 1474 1475 // ---- POST /admin/upload : multipart image upload (session + CSRF part, C11) ---- 1476 if done == 0 { if ca_index(req, rn, "POST /admin/upload" as *u8, 18) == 0 { 1477 let body: *u8 = (req + hend + 4) as *u8 1478 let bn: i64 = rn - hend - 4 1479 var authed: i64 = ca_session_ok(req, hend, sess) 1480 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1481 if authed == 1 { 1482 // boundary= from the Content-Type header 1483 let bnd: *u8 = sys_mmap(128) 1484 var bl9: i64 = 0 - 1 1485 let bp9: i64 = ca_index(req, hend, "boundary=" as *u8, 9) 1486 if bp9 >= 0 { 1487 bl9 = 0 1488 var p9: i64 = bp9 + 9 1489 var going9: i64 = 1 1490 while going9 == 1 { 1491 going9 = 0 1492 if p9 < hend { if bl9 < 100 { 1493 let c11: i64 = req[p9] as i64 1494 if c11 != 13 { if c11 != 10 { if c11 != 59 { if c11 != 32 { bnd[bl9] = c11 as u8; bl9 = bl9 + 1; p9 = p9 + 1; going9 = 1 } } } } 1495 } } 1496 } 1497 } 1498 var status11: i64 = 400 // default refusal 1499 var fname: *u8 = sys_mmap(256) 1500 var fnl: i64 = 0 - 1 1501 var payoff: i64 = 0 - 1 1502 var paylen: i64 = 0 1503 var csrfok11: i64 = 0 1504 if bl9 > 0 { 1505 let parts: *i64 = sys_mmap(8 * 4 * 8) as *i64 1506 let np: i64 = cms_mp_parse(body, bn, bnd, bl9, parts, 8) 1507 var pi: i64 = 0 1508 while pi < np { 1509 let hoff: i64 = parts[pi*4] 1510 let hlen: i64 = parts[pi*4+1] 1511 let boff: i64 = parts[pi*4+2] 1512 let bln: i64 = parts[pi*4+3] 1513 let nv11: *u8 = sys_mmap(256) 1514 let nl11: i64 = cms_mp_field(body, hoff, hoff + hlen, " name=\"" as *u8, nv11, 200) 1515 if nl11 == 4 { if ca_index(nv11, 4, "csrf" as *u8, 4) == 0 { 1516 if bln == 32 { if st_eq_ct(((body as i64) + boff) as *u8, csrf, 32) == 1 { csrfok11 = 1 } } 1517 } } 1518 if nl11 == 4 { if ca_index(nv11, 4, "file" as *u8, 4) == 0 { 1519 fnl = cms_mp_field(body, hoff, hoff + hlen, "filename=\"" as *u8, fname, 200) 1520 payoff = boff 1521 paylen = bln 1522 } } 1523 pi = pi + 1 1524 } 1525 } 1526 if csrfok11 == 0 { status11 = 403 } 1527 if csrfok11 == 1 { if fnl > 0 { if payoff >= 0 { 1528 status11 = 400 1529 if paylen > maxup { status11 = 413 } 1530 if paylen <= maxup { if paylen > 0 { 1531 if ca_sane_name(fname, fnl) == 1 { 1532 let t11: i64 = ca_img_type(fname, fnl) 1533 if t11 >= 0 { 1534 if ca_magic_ok(t11, ((body as i64) + payoff) as *u8, paylen) == 1 { 1535 let dst11: *u8 = sys_mmap(512) 1536 var do11: i64 = ca_cat(dst11, 0, updir) 1537 do11 = ca_cat(dst11, do11, "/" as *u8) 1538 fname[fnl] = 0 as u8 1539 do11 = ca_cat(dst11, do11, fname) 1540 dst11[do11] = 0 as u8 1541 let uf: i64 = sys_openat_wr(dst11, 0x1a4) 1542 if uf >= 0 { 1543 sys_write(uf, ((body as i64) + payoff) as *u8, paylen) 1544 sys_close(uf) 1545 status11 = 303 1546 } 1547 } 1548 } 1549 } 1550 } } 1551 } } } 1552 if status11 == 303 { ca_resp(cfd, "303 See Other" as *u8, "Location: /admin/blocks\r\n" as *u8, "uploaded\n" as *u8, 9) } 1553 if status11 == 403 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1554 if status11 == 413 { ca_resp(cfd, "413 Payload Too Large" as *u8, "" as *u8, "too large\n" as *u8, 10) } 1555 if status11 == 400 { ca_resp(cfd, "400 Bad Request" as *u8, "" as *u8, "bad upload\n" as *u8, 11) } 1556 } 1557 done = 1 1558 } } 1559 1560 // ---- GET /admin/preview : authed render of the PENDING DRAFT (never public) ---- 1561 if done == 0 { if ca_index(req, rn, "GET /admin/preview" as *u8, 18) == 0 { 1562 var pauth: i64 = ca_session_ok(req, hend, sess) 1563 if pauth == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1564 if pauth == 1 { 1565 var pn2: i64 = 0 - 1 1566 if draftstate == 1 { pn2 = ca_render_draft(sitedir, out) } 1567 if pn2 < 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "no pending draft\n" as *u8, 17) } 1568 if pn2 >= 0 { ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\nX-Robots-Tag: noindex\r\n" as *u8, out, pn2) } 1569 } 1570 done = 1 1571 } } 1572 1573 // ---- POST /admin/publish : promote the pending draft (C9, _pe_draft guard) ---- 1574 if done == 0 { if ca_index(req, rn, "POST /admin/publish" as *u8, 19) == 0 { 1575 let body: *u8 = (req + hend + 4) as *u8 1576 let bn: i64 = rn - hend - 4 1577 var authed: i64 = ca_session_ok(req, hend, sess) 1578 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1579 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1580 if authed == 1 { 1581 let dst2: i64 = _pe_draft_step(draftstate, 1) // refuses from CLEAN -> 409 1582 if dst2 < 0 { ca_resp(cfd, "409 Conflict" as *u8, "" as *u8, "no pending draft\n" as *u8, 17) } 1583 if dst2 >= 0 { 1584 let db: *u8 = sys_mmap(CA_REQCAP) 1585 let dn: i64 = cst_read(dpath, db, CA_REQCAP - 1) 1586 var pub: i64 = 0 1587 if dn >= 0 { 1588 var st3: i64 = _pe_pub_step(pubstate, 0) 1589 if st3 >= 0 { st3 = _pe_pub_step(st3, 1) } 1590 if st3 >= 0 { 1591 if cst_write_atomic(cpath, db, dn) == 1 { 1592 if ca_render_publish(sitedir) == 1 { pub = 1; pubstate = st3 } 1593 } 1594 } 1595 } 1596 if pub == 1 { 1597 sys_renameat(dpath, dlast) // set the consumed draft aside, additive 1598 draftstate = dst2 1599 ca_resp(cfd, "303 See Other" as *u8, "Location: /\r\n" as *u8, "draft published\n" as *u8, 16) 1600 } 1601 if pub == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "draft publish failed\n" as *u8, 21) } 1602 } 1603 } 1604 done = 1 1605 } } 1606 1607 // ---- POST /admin/discard : set the pending draft aside (C9, _pe_draft guard) ---- 1608 if done == 0 { if ca_index(req, rn, "POST /admin/discard" as *u8, 19) == 0 { 1609 let body: *u8 = (req + hend + 4) as *u8 1610 let bn: i64 = rn - hend - 4 1611 var authed: i64 = ca_session_ok(req, hend, sess) 1612 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1613 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1614 if authed == 1 { 1615 let dst4: i64 = _pe_draft_step(draftstate, 2) // refuses from CLEAN -> 409 1616 if dst4 < 0 { ca_resp(cfd, "409 Conflict" as *u8, "" as *u8, "no pending draft\n" as *u8, 17) } 1617 if dst4 >= 0 { 1618 var disc: i64 = 0 1619 if sys_renameat(dpath, dlast) == 0 { disc = 1 } 1620 if disc == 1 { draftstate = dst4; ca_resp(cfd, "303 See Other" as *u8, "Location: /admin\r\n" as *u8, "draft discarded\n" as *u8, 16) } 1621 if disc == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "discard failed\n" as *u8, 15) } 1622 } 1623 } 1624 done = 1 1625 } } 1626 1627 // ---- POST /admin/rollback ---- 1628 if done == 0 { if ca_index(req, rn, "POST /admin/rollback" as *u8, 20) == 0 { 1629 let body: *u8 = (req + hend + 4) as *u8 1630 let bn: i64 = rn - hend - 4 1631 var authed: i64 = ca_session_ok(req, hend, sess) 1632 if authed == 1 { if ca_csrf_ok(body, bn, csrf) == 0 { authed = 0 } } 1633 if authed == 0 { ca_resp(cfd, "403 Forbidden" as *u8, "" as *u8, "forbidden\n" as *u8, 10) } 1634 if authed == 1 { 1635 let st2: i64 = _pe_pub_step(pubstate, 2) // ROLLBACK guard (team-authored) 1636 if st2 < 0 { ca_resp(cfd, "409 Conflict" as *u8, "" as *u8, "nothing to roll back\n" as *u8, 21) } 1637 if st2 >= 0 { 1638 var rb: i64 = cst_rollback(cpath) 1639 if rb == 1 { rb = ca_render_publish(sitedir) } 1640 if rb == 1 { pubstate = st2; ca_resp(cfd, "303 See Other" as *u8, "Location: /\r\n" as *u8, "rolled back\n" as *u8, 12) } 1641 if rb == 0 { ca_resp(cfd, "500 Internal Server Error" as *u8, "" as *u8, "rollback failed\n" as *u8, 16) } 1642 } 1643 } 1644 done = 1 1645 } } 1646 1647 // ---- GET /portal : client login form (no session) | portal (client session) ---- 1648 if done == 0 { if ca_index(req, rn, "GET /portal HTTP" as *u8, 16) == 0 { 1649 if ca_client_ok(req, hend, csess) == 1 { 1650 let pp5: i64 = ca_portal_page(out) 1651 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, pp5) 1652 } 1653 if ca_client_ok(req, hend, csess) == 0 { 1654 let cl5: i64 = ca_client_login_page(out, 0) 1655 ca_resp(cfd, "200 OK" as *u8, "Cache-Control: no-store\r\n" as *u8, out, cl5) 1656 } 1657 done = 1 1658 } } 1659 1660 // ---- POST /portal/login : verify client_pw -> client session cookie (5-strike lockout) ---- 1661 if done == 0 { if ca_index(req, rn, "POST /portal/login" as *u8, 18) == 0 { 1662 var cok: i64 = 0 1663 if has_client == 1 { if client_failed < CA_LOCKOUT { 1664 let body: *u8 = (req + hend + 4) as *u8 1665 let bn: i64 = rn - hend - 4 1666 let pw: *u8 = sys_mmap(512) 1667 let pl: *i64 = sys_mmap(16) as *i64 1668 if nx_http_form_get_field(body, bn, "pw" as *u8, 2, pw, 511, pl) == 0 { 1669 let dig: *u8 = sys_mmap(32) 1670 sha256_digest(pw, pl[0], dig) 1671 let hex: *u8 = sys_mmap(80) 1672 let hxc: *u8 = "0123456789abcdef" as *u8 1673 var i: i64 = 0 1674 while i < 32 { 1675 hex[i*2] = hxc[((dig[i] as i64) >> 4) & 15] 1676 hex[i*2+1] = hxc[(dig[i] as i64) & 15] 1677 i = i + 1 1678 } 1679 if st_eq_ct(hex, cpwhex, 64) == 1 { cok = 1 } 1680 } 1681 } } 1682 if cok == 1 { 1683 client_failed = 0 1684 st_hex128(csess) 1685 var o: i64 = ca_cat(out, 0, "Set-Cookie: nclient=" as *u8) 1686 o = ca_catn(out, o, csess, 32) 1687 o = ca_cat(out, o, "; HttpOnly; Path=/; SameSite=Strict" as *u8) 1688 if securecookie == 1 { o = ca_cat(out, o, "; Secure" as *u8) } 1689 o = ca_cat(out, o, "\r\nLocation: /portal\r\n" as *u8) 1690 out[o] = 0 as u8 1691 ca_resp(cfd, "303 See Other" as *u8, out, "ok\n" as *u8, 3) 1692 } 1693 if cok == 0 { 1694 client_failed = client_failed + 1 1695 let cl6: i64 = ca_client_login_page(out, 1) 1696 ca_resp(cfd, "403 Forbidden" as *u8, "Cache-Control: no-store\r\n" as *u8, out, cl6) 1697 } 1698 done = 1 1699 } } 1700 1701 // ---- POST /portal/logout : clear client session ---- 1702 if done == 0 { if ca_index(req, rn, "POST /portal/logout" as *u8, 19) == 0 { 1703 csess[0] = 0 as u8 1704 ca_resp(cfd, "303 See Other" as *u8, "Set-Cookie: nclient=x; Max-Age=0; Path=/\r\nLocation: /portal\r\n" as *u8, "bye\n" as *u8, 4) 1705 done = 1 1706 } } 1707 1708 if done == 0 { ca_resp(cfd, "404 Not Found" as *u8, "" as *u8, "no such route\n" as *u8, 14) } 1709 sys_close(cfd) 1710 served = served + 1 1711 } 1712 } 1713 sys_write(1, "CMS-ADMIN-BUDGET-DONE\n" as *u8, 22) 1714 sys_exit(0) 1715 return 0 1716}