cache_control.nx source
↩ module page · 262 lines · 8623 B
1// cache_control.nx -- parse HTTP Cache-Control header.
2//
3// RFC 7234 §5.2. Controls caching behaviour at every level
4// (browser, proxy, CDN, reverse-proxy). Getting it right is
5// the difference between a fast site and a stampeded origin.
6//
7// Format: comma-separated directives, some with arguments.
8// Cache-Control: max-age=3600, public
9// Cache-Control: no-store
10// Cache-Control: private, max-age=0, must-revalidate
11// Cache-Control: public, max-age=31536000, immutable
12// Cache-Control: no-cache, s-maxage=600
13//
14// We parse into a typed CacheControl struct with flag bits for
15// the named directives + integer values for max-age / s-maxage /
16// max-stale / min-fresh / stale-while-revalidate.
17//
18// Invariants:
19// CC1 Empty input yields default CacheControl (all flags 0,
20// ages = -1 meaning \"not specified\").
21// CC2 Unknown directives silently ignored per RFC 7234 §5.2
22// tolerance rule.
23// CC3 Directive names are case-insensitive (we lowercase on
24// match). Argument values are case-sensitive integers.
25
26import "syscalls.nx"
27
28struct CacheControl {
29 // Primary mode flags.
30 no_store: i64,
31 no_cache: i64,
32 is_public: i64,
33 is_private: i64,
34 immutable: i64,
35 must_revalidate: i64,
36 proxy_revalidate: i64,
37 no_transform: i64,
38 only_if_cached: i64,
39
40 // Integer values; -1 means \"not specified\".
41 max_age: i64,
42 s_maxage: i64,
43 max_stale: i64,
44 min_fresh: i64,
45 stale_while_revalidate: i64,
46 stale_if_error: i64,
47}
48
49// Init a CacheControl to \"nothing specified\".
50func cache_control_init(cc: *CacheControl) -> i64 {
51 cc.no_store = 0
52 cc.no_cache = 0
53 cc.is_public = 0
54 cc.is_private = 0
55 cc.immutable = 0
56 cc.must_revalidate = 0
57 cc.proxy_revalidate = 0
58 cc.no_transform = 0
59 cc.only_if_cached = 0
60 cc.max_age = -1
61 cc.s_maxage = -1
62 cc.max_stale = -1
63 cc.min_fresh = -1
64 cc.stale_while_revalidate = -1
65 cc.stale_if_error = -1
66 return 0
67}
68
69// ASCII lowercase.
70func cc_lower(b: i64) -> i64 {
71 if b >= 0x41 {
72 if b <= 0x5A { return b + 0x20 }
73 }
74 return b
75}
76
77// Compare two byte ranges case-insensitively.
78func cc_eq_ci(a: *u8, a_off: i64, a_len: i64,
79 b: *u8, b_len: i64) -> i64 {
80 if a_len != b_len { return 0 }
81 var i: i64 = 0
82 while i < a_len {
83 if cc_lower(a[a_off + i]) != cc_lower(b[i]) { return 0 }
84 i = i + 1
85 }
86 return 1
87}
88
89// Parse a non-negative decimal starting at off.
90func cc_parse_int(buf: *u8, off: i64, end: i64) -> i64 {
91 var v: i64 = 0
92 var i: i64 = off
93 var any: i64 = 0
94 while i < end {
95 let b: i64 = buf[i]
96 if b < 0x30 { break }
97 if b > 0x39 { break }
98 v = v * 10 + (b - 0x30)
99 any = 1
100 i = i + 1
101 }
102 if any == 0 { return -1 }
103 return v
104}
105
106// Parse. Returns 0 on success (always -- unknown directives
107// don't error per RFC).
108func cache_control_parse(buf: *u8, n: i64, cc: *CacheControl) -> i64 {
109 cache_control_init(cc)
110 var i: i64 = 0
111 while i < n {
112 // Skip whitespace + commas.
113 while i < n {
114 let b: i64 = buf[i]
115 if b != 0x20 {
116 if b != 0x09 {
117 if b != 0x2C { break }
118 }
119 }
120 i = i + 1
121 }
122 if i >= n { break }
123
124 // Find directive end: '=' or ',' or EOF.
125 let name_start: i64 = i
126 while i < n {
127 if buf[i] == 0x3D { break }
128 if buf[i] == 0x2C { break }
129 if buf[i] == 0x20 { break }
130 i = i + 1
131 }
132 let name_len: i64 = i - name_start
133
134 // Optional argument after '='.
135 var arg_start: i64 = -1
136 var arg_len: i64 = 0
137 if i < n {
138 if buf[i] == 0x3D {
139 i = i + 1
140 // Optional quoted value.
141 if i < n {
142 if buf[i] == 0x22 {
143 i = i + 1
144 arg_start = i
145 while i < n {
146 if buf[i] == 0x22 { break }
147 i = i + 1
148 }
149 arg_len = i - arg_start
150 if i < n { i = i + 1 }
151 } else {
152 arg_start = i
153 while i < n {
154 if buf[i] == 0x2C { break }
155 if buf[i] == 0x20 { break }
156 i = i + 1
157 }
158 arg_len = i - arg_start
159 }
160 }
161 }
162 }
163
164 // Match directive name (case-insensitive).
165 if cc_eq_ci(buf, name_start, name_len, "no-store", 8) == 1 {
166 cc.no_store = 1
167 }
168 if cc_eq_ci(buf, name_start, name_len, "no-cache", 8) == 1 {
169 cc.no_cache = 1
170 }
171 if cc_eq_ci(buf, name_start, name_len, "public", 6) == 1 {
172 cc.is_public = 1
173 }
174 if cc_eq_ci(buf, name_start, name_len, "private", 7) == 1 {
175 cc.is_private = 1
176 }
177 if cc_eq_ci(buf, name_start, name_len, "immutable", 9) == 1 {
178 cc.immutable = 1
179 }
180 if cc_eq_ci(buf, name_start, name_len, "must-revalidate", 15) == 1 {
181 cc.must_revalidate = 1
182 }
183 if cc_eq_ci(buf, name_start, name_len, "proxy-revalidate", 16) == 1 {
184 cc.proxy_revalidate = 1
185 }
186 if cc_eq_ci(buf, name_start, name_len, "no-transform", 12) == 1 {
187 cc.no_transform = 1
188 }
189 if cc_eq_ci(buf, name_start, name_len, "only-if-cached", 14) == 1 {
190 cc.only_if_cached = 1
191 }
192
193 // Integer-valued directives.
194 if cc_eq_ci(buf, name_start, name_len, "max-age", 7) == 1 {
195 if arg_start >= 0 {
196 cc.max_age = cc_parse_int(buf, arg_start, arg_start + arg_len)
197 }
198 }
199 if cc_eq_ci(buf, name_start, name_len, "s-maxage", 8) == 1 {
200 if arg_start >= 0 {
201 cc.s_maxage = cc_parse_int(buf, arg_start, arg_start + arg_len)
202 }
203 }
204 if cc_eq_ci(buf, name_start, name_len, "max-stale", 9) == 1 {
205 if arg_start >= 0 {
206 cc.max_stale = cc_parse_int(buf, arg_start, arg_start + arg_len)
207 }
208 }
209 if cc_eq_ci(buf, name_start, name_len, "min-fresh", 9) == 1 {
210 if arg_start >= 0 {
211 cc.min_fresh = cc_parse_int(buf, arg_start, arg_start + arg_len)
212 }
213 }
214 if cc_eq_ci(buf, name_start, name_len,
215 "stale-while-revalidate", 22) == 1 {
216 if arg_start >= 0 {
217 cc.stale_while_revalidate = cc_parse_int(buf, arg_start,
218 arg_start + arg_len)
219 }
220 }
221 if cc_eq_ci(buf, name_start, name_len, "stale-if-error", 14) == 1 {
222 if arg_start >= 0 {
223 cc.stale_if_error = cc_parse_int(buf, arg_start,
224 arg_start + arg_len)
225 }
226 }
227 }
228 return 0
229}
230
231// Compile-only smoke.
232func main() -> i64 {
233 let cc_raw: *u8 = sys_mmap(256)
234 let cc: *CacheControl = cc_raw as *CacheControl
235
236 cache_control_parse("max-age=3600, public", 20, cc)
237 if cc.max_age != 3600 { return 1 }
238 if cc.is_public != 1 { return 2 }
239 if cc.no_store != 0 { return 3 }
240
241 cache_control_parse("no-store", 8, cc)
242 if cc.no_store != 1 { return 4 }
243 if cc.max_age != -1 { return 5 }
244
245 cache_control_parse("private, max-age=0, must-revalidate", 35, cc)
246 if cc.is_private != 1 { return 6 }
247 if cc.max_age != 0 { return 7 }
248 if cc.must_revalidate != 1 { return 8 }
249
250 cache_control_parse("public, max-age=31536000, immutable", 35, cc)
251 if cc.max_age != 31536000 { return 9 }
252 if cc.immutable != 1 { return 10 }
253
254 cache_control_parse("no-cache, s-maxage=600", 22, cc)
255 if cc.no_cache != 1 { return 11 }
256 if cc.s_maxage != 600 { return 12 }
257
258 // Unknown directives tolerated.
259 cache_control_parse("weird-thing, max-age=1", 22, cc)
260 if cc.max_age != 1 { return 13 }
261 return 0
262}