code wiki / (root) / cookie.nx

cookie.nx source

↩ module page · 185 lines · 5980 B

1// cookie.nx -- parse HTTP Cookie header (RFC 6265 ยง4.2). 2// 3// Parses the Cookie request header value: 4// "name1=value1; name2=value2; name3=value3" 5// 6// This is the client-sent form that every HTTP server sees on 7// every authenticated request. Used for: session IDs, CSRF 8// tokens, language prefs, A/B flags. Nishi-pages + sovereign 9// HTTP handlers need this to validate session cookies. 10// 11// Differences from query.nx: 12// - Separator is "; " (semicolon + space) per RFC, though we 13// also accept bare ";" for robustness. 14// - No percent-encoding on names; values may be quoted with 15// surrounding double-quotes (we strip them on parse). 16// - Empty values legal ("foo=; bar=baz") means cookie deletion. 17// 18// Not handled here: Set-Cookie (the server-sent response header) 19// which has many more attributes (Path, Domain, Secure, HttpOnly, 20// SameSite, Max-Age, Expires). That's a separate set_cookie.nx 21// shipper for the writer side. 22// 23// Invariants: 24// C1 Zero-alloc -- offsets into caller's buffer. 25// C2 Leading/trailing whitespace on names + values trimmed. 26// C3 Quoted values ("foo") have quotes stripped; embedded 27// escaped characters NOT unescaped today (RFC doesn't 28// specify quoted-string escape semantics for Cookie). 29 30import "syscalls.nx" 31 32const COOKIE_ERR_OVERFLOW: i64 = -1 33 34struct CookieEntry { 35 name_off: i64, name_len: i64, 36 val_off: i64, val_len: i64, 37} 38 39// Is byte ASCII whitespace (space or tab)? 40func cookie_is_ws(b: i64) -> i64 { 41 if b == 0x20 { return 1 } 42 if b == 0x09 { return 1 } 43 return 0 44} 45 46// Skip leading whitespace. 47func cookie_ltrim(buf: *u8, off: i64, end: i64) -> i64 { 48 var i: i64 = off 49 while i < end { 50 if cookie_is_ws(buf[i]) == 0 { return i } 51 i = i + 1 52 } 53 return end 54} 55 56// Trim trailing whitespace from [start, end). 57func cookie_rtrim(buf: *u8, start: i64, end: i64) -> i64 { 58 var e: i64 = end 59 while e > start { 60 if cookie_is_ws(buf[e - 1]) == 0 { return e } 61 e = e - 1 62 } 63 return e 64} 65 66// Parse a Cookie header value into caller's entry table. Returns 67// number of cookies parsed or COOKIE_ERR_OVERFLOW. 68func cookie_parse(buf: *u8, n: i64, 69 entries: *CookieEntry, cap: i64) -> i64 { 70 var count: i64 = 0 71 var i: i64 = 0 72 73 while i < n { 74 // Skip leading separator whitespace. 75 i = cookie_ltrim(buf, i, n) 76 if i >= n { break } 77 78 // Name runs to '='. 79 let name_start: i64 = i 80 while i < n { 81 if buf[i] == 0x3D { break } 82 if buf[i] == 0x3B { break } 83 i = i + 1 84 } 85 let name_end: i64 = cookie_rtrim(buf, name_start, i) 86 87 if count >= cap { return COOKIE_ERR_OVERFLOW } 88 let e: *CookieEntry = entries + count * 32 89 e.name_off = name_start 90 e.name_len = name_end - name_start 91 e.val_off = i 92 e.val_len = 0 93 94 // If we stopped on '=' there's a value; else empty. 95 if i < n { 96 if buf[i] == 0x3D { 97 i = i + 1 98 let v_start_trim: i64 = cookie_ltrim(buf, i, n) 99 var v_start: i64 = v_start_trim 100 i = v_start 101 while i < n { 102 if buf[i] == 0x3B { break } 103 i = i + 1 104 } 105 let v_end_trim: i64 = cookie_rtrim(buf, v_start, i) 106 // Strip surrounding quotes if present. 107 var v_end: i64 = v_end_trim 108 if v_end > v_start { 109 if buf[v_start] == 0x22 { 110 if buf[v_end - 1] == 0x22 { 111 v_start = v_start + 1 112 v_end = v_end - 1 113 } 114 } 115 } 116 e.val_off = v_start 117 e.val_len = v_end - v_start 118 } 119 } 120 121 count = count + 1 122 123 // Consume ';' if present. 124 if i < n { 125 if buf[i] == 0x3B { i = i + 1 } 126 } 127 } 128 return count 129} 130 131// Find cookie by name. Returns index or -1. 132func cookie_find(buf: *u8, entries: *CookieEntry, count: i64, 133 name: *u8, name_len: i64) -> i64 { 134 var i: i64 = 0 135 while i < count { 136 let e: *CookieEntry = entries + i * 32 137 if e.name_len == name_len { 138 var j: i64 = 0 139 var ok: i64 = 1 140 while j < name_len { 141 if buf[e.name_off + j] != name[j] { 142 ok = 0 143 break 144 } 145 j = j + 1 146 } 147 if ok == 1 { return i } 148 } 149 i = i + 1 150 } 151 return -1 152} 153 154// Compile-only smoke: "sid=abc123; theme=dark; empty=". 155func main() -> i64 { 156 let entries_raw: *u8 = sys_mmap(512) 157 let entries: *CookieEntry = entries_raw as *CookieEntry 158 159 let input: *u8 = "sid=abc123; theme=dark; empty=" 160 let n: i64 = cookie_parse(input, 30, entries, 16) 161 if n != 3 { return 1 } 162 163 let e0: *CookieEntry = entries 164 if e0.name_len != 3 { return 2 } 165 if e0.val_len != 6 { return 3 } 166 if input[e0.name_off] != 0x73 { return 4 } // 's' 167 if input[e0.val_off] != 0x61 { return 5 } // 'a' 168 169 let e2: *CookieEntry = entries + 2 * 32 170 if e2.name_len != 5 { return 6 } 171 if e2.val_len != 0 { return 7 } 172 173 // Quoted value. 174 let q: *u8 = "user=\"Alice Smith\"" 175 let q_entries: *CookieEntry = (sys_mmap(512)) as *CookieEntry 176 let qn: i64 = cookie_parse(q, 18, q_entries, 16) 177 if qn != 1 { return 8 } 178 let qe: *CookieEntry = q_entries 179 if qe.val_len != 11 { return 9 } // "Alice Smith" (quotes stripped) 180 181 // Lookup 182 let idx: i64 = cookie_find(input, entries, n, "theme", 5) 183 if idx != 1 { return 10 } 184 return 0 185}