code wiki / wiki / nx_wiki_admin_wiring.nx

nx_wiki_admin_wiring.nx source

↩ module page · 558 lines · 24000 B

1// nx_wiki_admin_wiring.nx -- V-MODAUTH-7 per-site wiring for wiki admin routes. 2// 3// REWRITTEN 2026-05-27 from V1 cookie-based wiring to V-MODAUTH-6 4// SEALED API per NISHI_MODERN_AUTH_CHARTER §8. Composes 5// hub/nx_modern_auth_flow (V-MODAUTH-6) which itself composes 6// hub/nx_no_cookie_session (V-MODAUTH-5). 7// 8// NO COOKIES. NO Set-Cookie. NO Cookie: reads. Per C1 cardinal. 9// 10// Per-request flow (the hot path): 11// 1. Caller extracts X-Nishi-Session header from HTTP request 12// via nx_http_header_find 13// 2. Caller base64-decodes the header value to a 152-byte binary 14// session token 15// 3. Caller passes (NxAuthContext + token bytes + now_unix_s) to 16// nx_wiki_admin_handle 17// 4. nx_wiki_admin_handle calls nx_modern_auth_validate_session 18// 5. On success: emit 200/302 to protected content 19// On failure: emit 401 with hint "POST passphrase to /wiki/admin/login" 20// 21// COMPOSES per "avoid duplicate primitives": 22// hub/nx_modern_auth_flow V-MODAUTH-6 SEALED §8 API 23// nx_http_header_find HTTP header extraction 24// 25// COMPOSED BY: 26// wiki/nx_wiki_routes.nx dispatcher (updated to pass *NxAuthContext) 27// 28// SPEC REFERENCES: 29// NISHI_MODERN_AUTH_CHARTER §8 SEALED 5-fn API 30// NISHI_MODERN_AUTH_CHARTER §9.1 nishifamily.com wiring example 31// 32// V2 (2026-06-10): POST /wiki/admin/login is LIVE -- OPAQUE (RFC 9807, KAT-gated) 33// + argon2id KSF + account store + Ed25519 session mint. Flow: 34// POST /wiki/admin/login body "handle=<h>&passphrase=<p>" (form-urlencoded) 35// -> nx_modern_auth_login -> 200 {"token":"<base64 152B>"} | 401 36// GET /wiki/admin/* X-Nishi-Session: <base64 token> -> 200 | 401 37// The 503 LOGIN_NOT_READY path remains ONLY for an unarmed context (no 38// account store / oprf_seed wired at boot) -- honest, not a pretend-stub. 39// Admin REGISTRATION is out-of-band (ops provisioning via 40// nx_modern_auth_register; no public register endpoint on the admin realm). 41 42import "nx_syscalls.nx" 43import "nx_http_header_find.nx" 44import "nx_base64.nx" 45import "hub/nx_modern_auth_flow.nx" 46 47// ===== Sealed verdict surface (codes 1420-1439) ================================================= 48const NX_WAW_OK: i64 = 0 49const NX_WAW_BAD_INPUT: i64 = 1420 50const NX_WAW_BUF_OVERFLOW: i64 = 1421 51const NX_WAW_AUTH_FAILED: i64 = 1422 52const NX_WAW_LOGIN_NOT_READY: i64 = 1423 // /wiki/admin/login waits for V-MODAUTH-2/4 53 54// ===== Named constants (M7) ================================================= 55const NX_WAW_MAX_HEADER_SCAN_BYTES: i64 = 8192 56const NX_WAW_RESP_CAP: i64 = 4096 57const NX_WAW_HDR_SESSION_NAME_N: i64 = 15 // "X-Nishi-Session" (was 16 = NEVER matched; 58 // latent since V1 -- caught by nx_wiki_login_live_gate row 3) 59 60// HTTP method-kind constants per nx_http_io convention. 61const NX_WAW_METHOD_GET: i64 = 1 62const NX_WAW_METHOD_POST: i64 = 2 63 64// ===== Helpers ================================================= 65 66func _waw_hdr_session_name() -> *u8 { return "X-Nishi-Session" as *u8 } 67 68func _waw_append(resp_buf: *u8, out_off: *i64, resp_cap: i64, 69 src: *u8, src_n: i64) -> i64 { 70 if out_off[0] + src_n > resp_cap { return 0 - NX_WAW_BUF_OVERFLOW } 71 var i: i64 = 0 72 while i < src_n { 73 if i >= NX_WAW_RESP_CAP { return 0 - NX_WAW_BUF_OVERFLOW } 74 resp_buf[out_off[0] + i] = src[i] 75 i = i + 1 76 } 77 out_off[0] = out_off[0] + src_n 78 return NX_WAW_OK 79} 80 81func _waw_append_z(resp_buf: *u8, out_off: *i64, resp_cap: i64, 82 src: *u8) -> i64 { 83 if (src as i64) == 0 { return 0 - NX_WAW_BAD_INPUT } 84 var n: i64 = 0 85 while n < NX_WAW_MAX_HEADER_SCAN_BYTES { 86 if src[n] == (0 as u8) { 87 return _waw_append(resp_buf, out_off, resp_cap, src, n) 88 } 89 n = n + 1 90 } 91 return 0 - NX_WAW_BUF_OVERFLOW 92} 93 94// ===== Response builders ================================================= 95 96// 401 Unauthorized with hint to POST passphrase to /wiki/admin/login. 97// NO Set-Cookie. NO WWW-Authenticate Basic (per modern-research-auth 98// cardinal, Basic Auth is also old tech; use POST form). 99func _waw_emit_401(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 { 100 let response: *u8 = "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 198\r\n\r\n<!DOCTYPE html><html><body><h1>Authentication required</h1><p>POST your passphrase to <code>/wiki/admin/login</code>. Session tokens go in <code>X-Nishi-Session</code> header (NEVER cookies).</p></body></html>" as *u8 101 return _waw_append_z(resp_buf, out_off, resp_cap, response) 102} 103 104// 200 OK for the protected content placeholder; route-specific 105// rendering would replace this in V-MODAUTH-7b. 106func _waw_emit_authed_200(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 { 107 let response: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 64\r\n\r\n<!DOCTYPE html><html><body><h1>Admin dashboard</h1></body></html>" as *u8 108 return _waw_append_z(resp_buf, out_off, resp_cap, response) 109} 110 111// 503 Service Unavailable for /wiki/admin/login until M2 + M4 land. 112// Honest per M6 anti-pretend-stub: tell user explicitly. 113func _waw_emit_login_not_ready(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 { 114 let response: *u8 = "HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 232\r\n\r\n<!DOCTYPE html><html><body><h1>Login temporarily unavailable</h1><p>The OPAQUE PAKE login flow lands when V-MODAUTH-2 ships. Per NISHI_MODERN_AUTH_CHARTER section 13, that milestone composes nx_voprf + nx_argon2id (both substrate-resident).</p></body></html>" as *u8 115 return _waw_append_z(resp_buf, out_off, resp_cap, response) 116} 117 118// 200 with a single-key JSON body: {"<key>":"<val>"}. Shared by login/refresh 119// (key="token", val=base64 session token) and recover (key="mnemonic"). 120func _waw_emit_json_kv(resp_buf: *u8, out_off: *i64, resp_cap: i64, 121 key: *u8, key_n: i64, 122 val: *u8, val_n: i64) -> i64 { 123 // body {"k":"v"} = key_n + val_n + 7 framing bytes ({ " " : " " }) 124 // (the V1 login emitter overstated Content-Length by 2 -- fixed here) 125 let body_n: i64 = key_n + val_n + 7 126 let head: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " as *u8 127 let rc1: i64 = _waw_append_z(resp_buf, out_off, resp_cap, head) 128 if rc1 != NX_WAW_OK { return rc1 } 129 let dig: *u8 = sys_mmap(8) 130 var dn: i64 = 0 131 var v: i64 = body_n 132 if v >= 100 { dig[dn] = (48 + (v / 100)) as u8; dn = dn + 1; v = v % 100 } 133 dig[dn] = (48 + (v / 10)) as u8 134 dig[dn + 1] = (48 + (v % 10)) as u8 135 let rc2: i64 = _waw_append(resp_buf, out_off, resp_cap, dig, dn + 2) 136 if rc2 != NX_WAW_OK { return rc2 } 137 let rc3: i64 = _waw_append_z(resp_buf, out_off, resp_cap, "\r\n\r\n{\"" as *u8) 138 if rc3 != NX_WAW_OK { return rc3 } 139 let rc4: i64 = _waw_append(resp_buf, out_off, resp_cap, key, key_n) 140 if rc4 != NX_WAW_OK { return rc4 } 141 let rc5: i64 = _waw_append_z(resp_buf, out_off, resp_cap, "\":\"" as *u8) 142 if rc5 != NX_WAW_OK { return rc5 } 143 let rc6: i64 = _waw_append(resp_buf, out_off, resp_cap, val, val_n) 144 if rc6 != NX_WAW_OK { return rc6 } 145 return _waw_append_z(resp_buf, out_off, resp_cap, "\"}" as *u8) 146} 147 148// 200 with the freshly minted session token as JSON. 149func _waw_emit_login_ok(resp_buf: *u8, out_off: *i64, resp_cap: i64, 150 token_152: *u8) -> i64 { 151 let b64: *u8 = sys_mmap(256) 152 let b64_n: i64 = b64_encode(token_152, NX_MAUTH_SESSION_TOKEN_BYTES, b64) 153 if b64_n < 1 { return 0 - NX_WAW_BAD_INPUT } 154 return _waw_emit_json_kv(resp_buf, out_off, resp_cap, "token" as *u8, 5, b64, b64_n) 155} 156 157// 401 JSON for failed login POST (wrong passphrase / unknown handle -- 158// SAME response for both per client-enumeration defense). 159func _waw_emit_login_401(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 { 160 let response: *u8 = "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nContent-Length: 26\r\n\r\n{\"error\":\"unauthorized\"}\r\n" as *u8 161 return _waw_append_z(resp_buf, out_off, resp_cap, response) 162} 163 164// ===== Login POST body parsing ================================================= 165// 166// body: "handle=<h>&passphrase=<p>" (application/x-www-form-urlencoded; 167// '+' -> space, %XX -> byte). Body length from Content-Length header. 168 169func _waw_hex_nib(c: i64) -> i64 { 170 if c >= 48 { if c <= 57 { return c - 48 } } 171 if c >= 97 { if c <= 102 { return c - 87 } } 172 if c >= 65 { if c <= 70 { return c - 55 } } 173 return 0 - 1 174} 175 176// urldecode src[0..n) into out (cap out_cap); returns decoded length or negative. 177func _waw_urldecode(src: *u8, n: i64, out: *u8, out_cap: i64) -> i64 { 178 var i: i64 = 0 179 var o: i64 = 0 180 while i < n { 181 if o >= out_cap { return 0 - NX_WAW_BUF_OVERFLOW } 182 let c: i64 = src[i] as i64 183 if c == 43 { out[o] = 32 as u8; i = i + 1 } // '+' 184 if c == 37 { // '%' 185 if i + 3 > n { return 0 - NX_WAW_BAD_INPUT } 186 let hi: i64 = _waw_hex_nib(src[i + 1] as i64) 187 let lo: i64 = _waw_hex_nib(src[i + 2] as i64) 188 if hi < 0 { return 0 - NX_WAW_BAD_INPUT } 189 if lo < 0 { return 0 - NX_WAW_BAD_INPUT } 190 out[o] = ((hi << 4) | lo) as u8 191 i = i + 3 192 } 193 if c != 43 { if c != 37 { out[o] = c as u8; i = i + 1 } } 194 o = o + 1 195 } 196 return o 197} 198 199// find "name=" value slice inside form body; returns value length via out_n, 200// raw (still url-encoded) offset via out_off. 1 if found, 0 if not. 201func _waw_form_field(body: *u8, body_n: i64, 202 name: *u8, name_n: i64, 203 out_off: *i64, out_n: *i64) -> i64 { 204 var pos: i64 = 0 205 while pos < body_n { 206 // field starts at pos; check name match + '=' 207 var m: i64 = 1 208 if pos + name_n + 1 > body_n { m = 0 } 209 if m == 1 { 210 var i: i64 = 0 211 while i < name_n { 212 if (body[pos + i] as i64) != (name[i] as i64) { m = 0; i = name_n } 213 if i < name_n { i = i + 1 } 214 } 215 } 216 if m == 1 { if (body[pos + name_n] as i64) != 61 { m = 0 } } // '=' 217 // value ends at '&' or body end 218 var vend: i64 = pos 219 var scan: i64 = 1 220 while scan == 1 { 221 if vend >= body_n { scan = 0 } 222 if scan == 1 { if (body[vend] as i64) == 38 { scan = 0 } } // '&' 223 if scan == 1 { vend = vend + 1 } 224 } 225 if m == 1 { 226 out_off[0] = pos + name_n + 1 227 out_n[0] = vend - (pos + name_n + 1) 228 return 1 229 } 230 pos = vend + 1 231 } 232 return 0 233} 234 235// parse Content-Length header value -> integer (0 if absent/garbled). 236func _waw_content_length(http_req_buf: *u8, http_headers_end: i64) -> i64 { 237 let off_box: *i64 = sys_mmap(8) as *i64 238 let len_box: *i64 = sys_mmap(8) as *i64 239 off_box[0] = 0 240 len_box[0] = 0 241 let name: *u8 = "Content-Length" as *u8 242 if nx_http_header_find(http_req_buf, http_headers_end, name, 14, off_box, len_box) != NXHF_FOUND { 243 return 0 244 } 245 var v: i64 = 0 246 var i: i64 = 0 247 while i < len_box[0] { 248 let c: i64 = http_req_buf[off_box[0] + i] as i64 249 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 250 i = i + 1 251 } 252 if v < 0 { return 0 } 253 if v > 4096 { return 0 } 254 return v 255} 256 257// Handle POST /wiki/admin/login against an ARMED context. 258func _waw_handle_login_post( 259 ctx: *NxAuthContext, 260 http_req_buf: *u8, http_headers_end: i64, 261 resp_buf: *u8, out_off: *i64, resp_cap: i64, 262 out_resp_n: *i64 263) -> i64 { 264 let body_n: i64 = _waw_content_length(http_req_buf, http_headers_end) 265 let body: *u8 = (http_req_buf as i64 + http_headers_end) as *u8 266 267 var ok: i64 = 1 268 let h_off: *i64 = sys_mmap(8) as *i64 269 let h_n: *i64 = sys_mmap(8) as *i64 270 let p_off: *i64 = sys_mmap(8) as *i64 271 let p_n: *i64 = sys_mmap(8) as *i64 272 if body_n < 3 { ok = 0 } 273 if ok == 1 { if _waw_form_field(body, body_n, "handle" as *u8, 6, h_off, h_n) != 1 { ok = 0 } } 274 if ok == 1 { if _waw_form_field(body, body_n, "passphrase" as *u8, 10, p_off, p_n) != 1 { ok = 0 } } 275 276 let handle: *u8 = sys_mmap(NX_MAUTH_MAX_USER_HANDLE_LEN + 8) 277 var handle_n: i64 = 0 278 let pass: *u8 = sys_mmap(NX_MAUTH_MAX_PASSPHRASE_LEN + 8) 279 var pass_n: i64 = 0 280 if ok == 1 { 281 handle_n = _waw_urldecode((body as i64 + h_off[0]) as *u8, h_n[0], 282 handle, NX_MAUTH_MAX_USER_HANDLE_LEN) 283 if handle_n < 1 { ok = 0 } 284 } 285 if ok == 1 { 286 pass_n = _waw_urldecode((body as i64 + p_off[0]) as *u8, p_n[0], 287 pass, NX_MAUTH_MAX_PASSPHRASE_LEN) 288 if pass_n < 1 { ok = 0 } 289 } 290 291 var authed: i64 = 0 292 let token: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 293 if ok == 1 { 294 let tok_n: *i64 = sys_mmap(8) as *i64 295 let rc: i64 = nx_modern_auth_login(ctx, handle, handle_n, pass, pass_n, 296 token, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) 297 if rc == NX_MAUTH_OK { if tok_n[0] == NX_MAUTH_SESSION_TOKEN_BYTES { authed = 1 } } 298 } 299 300 if authed == 1 { 301 let rc_ok: i64 = _waw_emit_login_ok(resp_buf, out_off, resp_cap, token) 302 if rc_ok != NX_WAW_OK { return rc_ok } 303 out_resp_n[0] = out_off[0] 304 return NX_WAW_OK 305 } 306 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap) 307 if rc_401 != NX_WAW_OK { return rc_401 } 308 out_resp_n[0] = out_off[0] 309 return NX_WAW_OK 310} 311 312// Handle POST /wiki/admin/recover: body "handle=<h>&mnemonic=<24 words>&passphrase=<new>". 313// 200 {"mnemonic":"<NEW rotated 24 words>"} | 401 (same body for unknown handle / 314// wrong mnemonic: enumeration defense). 315func _waw_handle_recover_post( 316 ctx: *NxAuthContext, 317 http_req_buf: *u8, http_headers_end: i64, 318 resp_buf: *u8, out_off: *i64, resp_cap: i64, 319 out_resp_n: *i64 320) -> i64 { 321 let body_n: i64 = _waw_content_length(http_req_buf, http_headers_end) 322 let body: *u8 = (http_req_buf as i64 + http_headers_end) as *u8 323 324 var ok: i64 = 1 325 let h_off: *i64 = sys_mmap(8) as *i64 326 let h_n: *i64 = sys_mmap(8) as *i64 327 let m_off: *i64 = sys_mmap(8) as *i64 328 let m_n: *i64 = sys_mmap(8) as *i64 329 let p_off: *i64 = sys_mmap(8) as *i64 330 let p_n: *i64 = sys_mmap(8) as *i64 331 if body_n < 3 { ok = 0 } 332 if ok == 1 { if _waw_form_field(body, body_n, "handle" as *u8, 6, h_off, h_n) != 1 { ok = 0 } } 333 if ok == 1 { if _waw_form_field(body, body_n, "mnemonic" as *u8, 8, m_off, m_n) != 1 { ok = 0 } } 334 if ok == 1 { if _waw_form_field(body, body_n, "passphrase" as *u8, 10, p_off, p_n) != 1 { ok = 0 } } 335 336 let handle: *u8 = sys_mmap(NX_MAUTH_MAX_USER_HANDLE_LEN + 8) 337 var handle_n: i64 = 0 338 let mnemonic: *u8 = sys_mmap(NX_MAUTH_MAX_MNEMONIC_LEN + 8) 339 var mnemonic_n: i64 = 0 340 let pass: *u8 = sys_mmap(NX_MAUTH_MAX_PASSPHRASE_LEN + 8) 341 var pass_n: i64 = 0 342 if ok == 1 { 343 handle_n = _waw_urldecode((body as i64 + h_off[0]) as *u8, h_n[0], 344 handle, NX_MAUTH_MAX_USER_HANDLE_LEN) 345 if handle_n < 1 { ok = 0 } 346 } 347 if ok == 1 { 348 mnemonic_n = _waw_urldecode((body as i64 + m_off[0]) as *u8, m_n[0], 349 mnemonic, NX_MAUTH_MAX_MNEMONIC_LEN) 350 if mnemonic_n < 1 { ok = 0 } 351 } 352 if ok == 1 { 353 pass_n = _waw_urldecode((body as i64 + p_off[0]) as *u8, p_n[0], 354 pass, NX_MAUTH_MAX_PASSPHRASE_LEN) 355 if pass_n < 1 { ok = 0 } 356 } 357 358 var recovered: i64 = 0 359 let new_mn: *u8 = sys_mmap(NX_MAUTH_MAX_MNEMONIC_LEN + 8) 360 let new_mn_n: *i64 = sys_mmap(8) as *i64 361 new_mn_n[0] = 0 362 if ok == 1 { 363 let rc: i64 = nx_modern_auth_recover(ctx, handle, handle_n, 364 mnemonic, mnemonic_n, 365 pass, pass_n, 366 new_mn, NX_MAUTH_MAX_MNEMONIC_LEN, new_mn_n) 367 if rc == NX_MAUTH_OK { if new_mn_n[0] > 0 { recovered = 1 } } 368 } 369 370 if recovered == 1 { 371 let rc_ok: i64 = _waw_emit_json_kv(resp_buf, out_off, resp_cap, 372 "mnemonic" as *u8, 8, new_mn, new_mn_n[0]) 373 if rc_ok != NX_WAW_OK { return rc_ok } 374 out_resp_n[0] = out_off[0] 375 return NX_WAW_OK 376 } 377 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap) 378 if rc_401 != NX_WAW_OK { return rc_401 } 379 out_resp_n[0] = out_off[0] 380 return NX_WAW_OK 381} 382 383// Handle POST /wiki/admin/refresh: valid X-Nishi-Session in -> fresh token out 384// (bumped expiry, no passphrase re-prompt). 200 {"token":...} | 401. 385func _waw_handle_refresh_post( 386 ctx: *NxAuthContext, 387 http_req_buf: *u8, http_headers_end: i64, 388 now_unix_s: i64, 389 resp_buf: *u8, out_off: *i64, resp_cap: i64, 390 out_resp_n: *i64 391) -> i64 { 392 let sess_off: *i64 = sys_mmap(8) as *i64 393 let sess_len: *i64 = sys_mmap(8) as *i64 394 sess_off[0] = 0 395 sess_len[0] = 0 396 var ok: i64 = 0 397 if nx_http_header_find(http_req_buf, http_headers_end, 398 _waw_hdr_session_name(), NX_WAW_HDR_SESSION_NAME_N, 399 sess_off, sess_len) == NXHF_FOUND { ok = 1 } 400 let new_tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 401 var refreshed: i64 = 0 402 if ok == 1 { 403 let token_ptr: *u8 = (http_req_buf as i64 + sess_off[0]) as *u8 404 var tok: *u8 = token_ptr 405 var tok_n: i64 = sess_len[0] 406 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES { 407 let dec: *u8 = sys_mmap(256) 408 let dec_n: i64 = b64_decode(token_ptr, sess_len[0], dec) 409 tok = dec 410 tok_n = dec_n 411 } 412 let new_n: *i64 = sys_mmap(8) as *i64 413 if nx_modern_auth_refresh_session(ctx, tok, tok_n, now_unix_s, 414 new_tok, NX_MAUTH_SESSION_TOKEN_BYTES, 415 new_n) == NX_MAUTH_OK { refreshed = 1 } 416 } 417 if refreshed == 1 { 418 let rc_ok: i64 = _waw_emit_login_ok(resp_buf, out_off, resp_cap, new_tok) 419 if rc_ok != NX_WAW_OK { return rc_ok } 420 out_resp_n[0] = out_off[0] 421 return NX_WAW_OK 422 } 423 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap) 424 if rc_401 != NX_WAW_OK { return rc_401 } 425 out_resp_n[0] = out_off[0] 426 return NX_WAW_OK 427} 428 429// path equality helper for the POST endpoint dispatch. 430func _waw_path_eq(url_path: *u8, url_path_n: i64, want: *u8, want_n: i64) -> i64 { 431 if url_path_n != want_n { return 0 } 432 var i: i64 = 0 433 while i < want_n { 434 if (url_path[i] as i64) != (want[i] as i64) { return 0 } 435 i = i + 1 436 } 437 return 1 438} 439 440// ===== Top-level wiring entry ================================================= 441// 442// Inputs: 443// ctx V-MODAUTH-6 NxAuthContext (per-realm; caller-init at boot) 444// http_req_buf HTTP request bytes (parsed by nx_http_server) 445// http_headers_end byte offset where headers end 446// url_path request URL path 447// url_path_n length 448// method_kind 1 = GET, 2 = POST 449// now_unix_s epoch seconds (caller-supplied; substrate-pure) 450// resp_buf output: HTTP response bytes 451// resp_cap output capacity 452// out_resp_n receives bytes written 453// 454// Behavior: 455// POST /wiki/admin/login -> 503 LOGIN_NOT_READY (until V-MODAUTH-2/4) 456// GET /wiki/admin/* -> validate X-Nishi-Session; 200 if good, 401 if not 457// POST /wiki/admin/refresh -> 501 not-yet-wired (V+1 micro-arc) 458 459func nx_wiki_admin_handle( 460 ctx: *NxAuthContext, 461 http_req_buf: *u8, http_headers_end: i64, 462 url_path: *u8, url_path_n: i64, 463 method_kind: i64, 464 now_unix_s: i64, 465 resp_buf: *u8, resp_cap: i64, 466 out_resp_n: *i64 467) -> i64 { 468 if (ctx as i64) == 0 { return 0 - NX_WAW_BAD_INPUT } 469 if ctx.valid != 1 { return 0 - NX_WAW_BAD_INPUT } 470 if (resp_buf as i64) == 0 { return 0 - NX_WAW_BAD_INPUT } 471 if (out_resp_n as i64) == 0 { return 0 - NX_WAW_BAD_INPUT } 472 if resp_cap < NX_WAW_RESP_CAP { return 0 - NX_WAW_BAD_INPUT } 473 out_resp_n[0] = 0 474 475 let out_off: *i64 = (sys_mmap(8)) as *i64 476 out_off[0] = 0 477 478 // POST endpoint dispatch: login / recover need an ARMED OPAQUE context 479 // (otherwise the honest 503); refresh needs only the session keys. 480 if method_kind == NX_WAW_METHOD_POST { 481 var is_login: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/login" as *u8, 17) 482 var is_recover: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/recover" as *u8, 19) 483 var is_refresh: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/refresh" as *u8, 19) 484 if is_refresh == 1 { 485 return _waw_handle_refresh_post(ctx, http_req_buf, http_headers_end, 486 now_unix_s, 487 resp_buf, out_off, resp_cap, out_resp_n) 488 } 489 var needs_armed: i64 = 0 490 if is_login == 1 { needs_armed = 1 } 491 if is_recover == 1 { needs_armed = 1 } 492 if needs_armed == 1 { 493 var armed: i64 = 1 494 if ctx.account_store == 0 { armed = 0 } 495 if (ctx.opaque_skS_32 as i64) == 0 { armed = 0 } 496 if armed == 0 { 497 let rc_lnr: i64 = _waw_emit_login_not_ready(resp_buf, out_off, resp_cap) 498 if rc_lnr != NX_WAW_OK { return rc_lnr } 499 out_resp_n[0] = out_off[0] 500 return NX_WAW_OK 501 } 502 if is_login == 1 { 503 return _waw_handle_login_post(ctx, http_req_buf, http_headers_end, 504 resp_buf, out_off, resp_cap, out_resp_n) 505 } 506 return _waw_handle_recover_post(ctx, http_req_buf, http_headers_end, 507 resp_buf, out_off, resp_cap, out_resp_n) 508 } 509 } 510 511 // Extract X-Nishi-Session header (no cookies; never). 512 let sess_off: *i64 = (sys_mmap(8)) as *i64 513 let sess_len: *i64 = (sys_mmap(8)) as *i64 514 sess_off[0] = 0 515 sess_len[0] = 0 516 let rc_find: i64 = nx_http_header_find(http_req_buf, http_headers_end, 517 _waw_hdr_session_name(), 518 NX_WAW_HDR_SESSION_NAME_N, 519 sess_off, sess_len) 520 521 // No session header -> 401. 522 if rc_find != NXHF_FOUND { 523 let rc_401: i64 = _waw_emit_401(resp_buf, out_off, resp_cap) 524 if rc_401 != NX_WAW_OK { return rc_401 } 525 out_resp_n[0] = out_off[0] 526 return NX_WAW_OK 527 } 528 529 // Header carries base64(152B token) (what /wiki/admin/login returns). 530 // Raw 152-byte binary also accepted (CLI clients on the LAN). 531 let token_ptr: *u8 = (http_req_buf as i64 + sess_off[0]) as *u8 532 var tok: *u8 = token_ptr 533 var tok_n: i64 = sess_len[0] 534 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES { 535 let dec: *u8 = sys_mmap(256) 536 let dec_n: i64 = b64_decode(token_ptr, sess_len[0], dec) 537 tok = dec 538 tok_n = dec_n 539 } 540 let rc_val: i64 = nx_modern_auth_validate_session( 541 ctx, 542 tok, tok_n, 543 now_unix_s, 544 (0 as i64) as *u8, 0, (0 as i64) as *i64) 545 546 if rc_val != NX_MAUTH_OK { 547 let rc_401b: i64 = _waw_emit_401(resp_buf, out_off, resp_cap) 548 if rc_401b != NX_WAW_OK { return rc_401b } 549 out_resp_n[0] = out_off[0] 550 return NX_WAW_OK 551 } 552 553 // Authed. 554 let rc_200: i64 = _waw_emit_authed_200(resp_buf, out_off, resp_cap) 555 if rc_200 != NX_WAW_OK { return rc_200 } 556 out_resp_n[0] = out_off[0] 557 return NX_WAW_OK 558}