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}