src/http.bend
bend-kube-runtime@0.2.0.0 · 519 lines · 70 definitions
1import Base
2import bend-kube-runtime@0.2.0.0/src/utf8.bend as U
3
4
5
6
7
8
9
10
11
12type Header is Data:
13 Header{name: String, value: String}
14
15type Req is Data:
16 Req{method: String, path: String, headers: List<&2, Header>, body: String}
17
18type Head is Data:
19 Head{status: U32, reason: String, headers: List<&2, Header>}
20
21
22
23
24
25
26
27
28
29
30type Phase is Data:
31 PHead{buf: List<&2, U32>, tail: U32}
32 PLen{left: U32}
33 PSize{size: U32, ext: Bool, any: Bool}
34 PData{left: U32}
35 PCrlf{}
36 PTrail{blank: Bool}
37 PClose{}
38 PDone{}
39 PErr{msg: String}
40 PLost{msg: String}
41
42
43
44type Resp is Data:
45 Resp{phase: Phase, head: Maybe<&2, Head>, out: List<&2, U32>}
46
47
48
49
50def Http.lower(s: String) -> String:
51 String.to_lower(s)
52
53def Http.header.if(+name: String, v: String, t: List<&2, Header>, hit: Bool) ->
54 Maybe<&2, String>:
55 match hit:
56 case True{}:
57 Some{String.trim(v)}
58 case False{}:
59 None{}
60
61def Http.header(+name: String, hs: List<&2, Header>) -> Maybe<&2, String>:
62 match hs:
63 case Nil{}:
64 None{}
65 case Con{Header{n, v}, t}:
66 Http.header.if(name, v, t, String.eq(Http.lower(n), Http.lower(name)))
67
68
69def Http.find(hs: List<&2, Header>, +name: String) -> Maybe<&2, String>:
70 match hs:
71 case Nil{}:
72 None{}
73 case Con{+h, t}:
74 Maybe.or(&2, String, Http.header(name, [h]), Http.find(t, name))
75
76
77
78
79def Http.render_headers(hs: List<&2, Header>) -> String:
80 match hs:
81 case Nil{}:
82 ""
83 case Con{Header{n, v}, t}:
84 n ++ ": " ++ v ++ "\r\n" ++ Http.render_headers(t)
85
86
87
88def Http.render(req: Req, host: String) -> String:
89 match req:
90 case Req{m, p, hs, +body}:
91 m ++ " " ++ p ++ " HTTP/1.1\r\nHost: " ++ host ++ "\r\n"
92 ++ Http.render_headers(hs)
93 ++ "Content-Length: " ++ U32.show(U.Utf8.size(body)) ++ "\r\n\r\n" ++ body
94
95def Http.pct_hex.if(+n: U32, lt: Bool) -> Char:
96 match lt:
97 case True{}:
98 Chr{(n + 48 : U32)}
99 case False{}:
100 Chr{(n + 55 : U32)}
101
102
103def Http.pct_hex(+n: U32) -> Char:
104 Http.pct_hex.if(n, U32.is_lt(n, 10))
105
106def Http.keep(c: Char) -> Bool:
107 match c:
108 case '-':
109 True{}
110 case '_':
111 True{}
112 case '.':
113 True{}
114 case '~':
115 True{}
116 case Chr{+x}:
117 Bool.or(Char.is_alpha(Chr{x}), Char.is_digit(Chr{x}))
118
119def Http.pct_bytes(bs: List<&2, U32>, rest: String) -> String:
120 match bs:
121 case Nil{}:
122 rest
123 case Con{+b, t}:
124 SCon{'%', SCon{Http.pct_hex((b >> 4n : U32)), SCon{Http.pct_hex((b .&. 15 : U32)),
125 Http.pct_bytes(t, rest)}}}
126
127def Http.encode.chr(+c: Char, rest: String, keep: Bool) -> String:
128 match keep:
129 case True{}:
130 SCon{c, rest}
131 case False{}:
132 Http.pct_bytes(U.Utf8.encode(SCon{c, SNil{}}), rest)
133
134def Http.encode(s: String) -> String:
135 match s:
136 case SNil{}:
137 SNil{}
138 case SCon{+c, t}:
139 Http.encode.chr(c, Http.encode(t), Http.keep(c))
140
141def Http.parse_header.go(parts: List<&2, String>) -> Maybe<&2, Header>:
142 match parts:
143 case Con{n, Con{v, rest}}:
144 Some{Header{String.trim(n), String.join(v <> rest, ":")}}
145 case _:
146 None{}
147
148
149
150
151def Http.parse_header(line: String) -> Maybe<&2, Header>:
152 Http.parse_header.go(String.split(line, ':'))
153
154def Http.parse_headers.put(h: Maybe<&2, Header>, rest: List<&2, Header>) -> List<&2, Header>:
155 match h:
156 case Some{x}:
157 x <> rest
158 case None{}:
159 rest
160
161def Http.parse_headers(lines: List<&2, String>) -> List<&2, Header>:
162 match lines:
163 case Nil{}:
164 Nil{}
165 case Con{l, t}:
166 Http.parse_headers.put(Http.parse_header(String.trim(l)), Http.parse_headers(t))
167
168def Http.parse_status.fin(code: Maybe<&2, U32>, reason: String) -> Maybe<&1, U32 & String>:
169 match code:
170 case Some{c}:
171 Some{(c, reason)}
172 case None{}:
173 None{}
174
175def Http.parse_status.go(parts: List<&2, String>) -> Maybe<&1, U32 & String>:
176 match parts:
177 case Con{v, Con{code, rest}}:
178 Http.parse_status.fin(U32.read(code), String.join(rest, " "))
179 case _:
180 None{}
181
182
183def Http.parse_status(line: String) -> Maybe<&1, U32 & String>:
184 Http.parse_status.go(String.split(String.trim(line), ' '))
185
186def Http.parse_head.fin(st: Maybe<&1, U32 & String>, hs: List<&2, Header>) -> Maybe<&2, Head>:
187 match st:
188 case Some{(c, r)}:
189 Some{Head{c, r, hs}}
190 case None{}:
191 None{}
192
193def Http.parse_head.go(lines: List<&2, String>) -> Maybe<&2, Head>:
194 match lines:
195 case Con{first, rest}:
196 Http.parse_head.fin(Http.parse_status(first), Http.parse_headers(rest))
197 case Nil{}:
198 None{}
199
200def Http.parse_head(text: String) -> Maybe<&2, Head>:
201 Http.parse_head.go(String.lines(text))
202
203def Http.framing.te(chunked: Bool) -> Phase:
204 match chunked:
205 case True{}:
206 PSize{0, False{}, False{}}
207 case False{}:
208 PClose{}
209
210def Http.framing.len.if(+k: U32, zero: Bool) -> Phase:
211 match zero:
212 case True{}:
213 PDone{}
214 case False{}:
215 PLen{k}
216
217def Http.framing.len(n: Maybe<&2, U32>) -> Phase:
218 match n:
219 case Some{+k}:
220 Http.framing.len.if(k, U32.is_eq(k, 0))
221 case None{}:
222 PErr{"bad Content-Length"}
223
224def Http.framing.pick(none: Bool, te: Maybe<&2, String>, cl: Maybe<&2, String>) -> Phase:
225 match none te cl:
226 case True{} _ _:
227 PDone{}
228 case False{} Some{t} _:
229 Http.framing.te(String.contains(Http.lower(t), "chunked"))
230 case False{} None{} Some{n}:
231 Http.framing.len(U32.read(n))
232 case False{} None{} None{}:
233 PClose{}
234
235def Http.no_body(+code: U32) -> Bool:
236 Bool.or(U32.is_lt(code, 200), Bool.or(U32.is_eq(code, 204), U32.is_eq(code, 304)))
237
238def Http.framing.go(+code: U32, te: Maybe<&2, String>, cl: Maybe<&2, String>) -> Phase:
239 Http.framing.pick(Http.no_body(code), te, cl)
240
241
242
243def Http.framing(+h: Head) -> Phase:
244 match h:
245 case Head{+code, r, hs}:
246 Http.framing.go(code, Http.find(hs, "transfer-encoding"), Http.find(hs, "content-length"))
247
248
249
250
251def Http.resp.init() -> Resp:
252 Resp{PHead{Nil{}, 0}, None{}, Nil{}}
253
254def Http.head_tail.go(+tail: U32, cr: Bool, lf: Bool, even: Bool) -> U32:
255 match cr lf even:
256 case True{} _ True{}:
257 (tail + 1 : U32)
258 case True{} _ False{}:
259 1
260 case False{} True{} False{}:
261 (tail + 1 : U32)
262 case _ _ _:
263 0
264
265
266def Http.head_tail(+tail: U32, +b: U32) -> U32:
267 Http.head_tail.go(tail, U32.is_eq(b, 13), U32.is_eq(b, 10), U32.is_eq((tail .&. 1 : U32), 0))
268
269def Http.end_head.go(h: Maybe<&2, Head>) -> Resp:
270 match h:
271 case Some{+x}:
272 Resp{Http.framing(x), Some{x}, Nil{}}
273 case None{}:
274 Resp{PErr{"bad response head"}, None{}, Nil{}}
275
276def Http.end_head(buf: List<&2, U32>) -> Resp:
277 Http.end_head.go(Http.parse_head(U.Utf8.decode_all(List.reverse(&2, U32, buf))))
278
279def Http.step.head.end(buf: List<&2, U32>, +tail: U32, done: Bool) -> Resp:
280 match done:
281 case True{}:
282 Http.end_head(buf)
283 case False{}:
284 Resp{PHead{buf, tail}, None{}, Nil{}}
285
286def Http.step.head.if(buf: List<&2, U32>, +tail: U32) -> Resp:
287 Http.step.head.end(buf, tail, U32.is_eq(tail, 4))
288
289def Http.step.head(buf: List<&2, U32>, +tail: U32, +b: U32) -> Resp:
290 Http.step.head.if(b <> buf, Http.head_tail(tail, b))
291
292def Http.count.if(+left: U32, chunk: Bool, zero: Bool) -> Phase:
293 match chunk zero:
294 case True{} True{}:
295 PCrlf{}
296 case False{} True{}:
297 PDone{}
298 case True{} False{}:
299 PData{left}
300 case False{} False{}:
301 PLen{left}
302
303
304def Http.count(+left: U32, chunk: Bool) -> Phase:
305 Http.count.if(left, chunk, U32.is_eq(left, 0))
306
307def Http.hex.pick(+c: U32, digit: Bool, lower: Bool, upper: Bool) -> Maybe<&2, U32>:
308 match digit lower upper:
309 case True{} _ _:
310 Some{(c - 48 : U32)}
311 case False{} True{} _:
312 Some{(c - 87 : U32)}
313 case False{} False{} True{}:
314 Some{(c - 55 : U32)}
315 case _ _ _:
316 None{}
317
318def Http.hex.go(+c: U32, a: Bool, b: Bool, d: Bool, e: Bool, f: Bool, g: Bool) -> Maybe<&2, U32>:
319 Http.hex.pick(c, Bool.and(a, b), Bool.and(d, e), Bool.and(f, g))
320
321def Http.hex(+c: U32) -> Maybe<&2, U32>:
322 Http.hex.go(c, U32.is_ge(c, 48), U32.is_le(c, 57), U32.is_ge(c, 97), U32.is_le(c, 102),
323 U32.is_ge(c, 65), U32.is_le(c, 70))
324
325def Http.step.size.digit(+size: U32, +v: U32, big: Bool) -> Phase:
326 match big:
327 case True{}:
328 PErr{"chunk too large"}
329 case False{}:
330 PSize{((size << 4n) + v : U32), False{}, True{}}
331
332def Http.step.size.end(+size: U32, last: Bool) -> Phase:
333 match last:
334 case True{}:
335 PTrail{True{}}
336 case False{}:
337 PData{size}
338
339def Http.step.size.go(+size: U32, lf: Bool, ext: Bool, h: Maybe<&2, U32>, semi: Bool,
340 cr: Bool, any: Bool) -> Phase:
341 match lf ext h semi cr any:
342 case True{} _ _ _ _ True{}:
343 Http.step.size.end(size, U32.is_eq(size, 0))
344 case True{} _ _ _ _ False{}:
345 PErr{"empty chunk size"}
346 case False{} True{} _ _ _ _:
347 PSize{size, True{}, any}
348 case False{} False{} Some{v} _ _ _:
349 Http.step.size.digit(size, v, U32.is_ge(size, 134217728))
350 case False{} False{} None{} True{} _ _:
351 PSize{size, True{}, any}
352 case False{} False{} None{} False{} True{} _:
353 PSize{size, ext, any}
354 case _ _ _ _ _ _:
355 PErr{"bad chunk size"}
356
357
358
359def Http.step.size(+size: U32, ext: Bool, any: Bool, +b: U32) -> Phase:
360 Http.step.size.go(size, U32.is_eq(b, 10), ext, Http.hex(b), U32.is_eq(b, 59),
361 U32.is_eq(b, 13), any)
362
363def Http.step.crlf.go(cr: Bool, lf: Bool) -> Phase:
364 match cr lf:
365 case True{} _:
366 PCrlf{}
367 case False{} True{}:
368 PSize{0, False{}, False{}}
369 case _ _:
370 PErr{"missing CRLF after chunk"}
371
372def Http.step.crlf(+b: U32) -> Phase:
373 Http.step.crlf.go(U32.is_eq(b, 13), U32.is_eq(b, 10))
374
375def Http.step.trail.go(lf: Bool, cr: Bool, blank: Bool) -> Phase:
376 match lf cr blank:
377 case True{} _ True{}:
378 PDone{}
379 case True{} _ False{}:
380 PTrail{True{}}
381 case False{} True{} b:
382 PTrail{b}
383 case False{} False{} _:
384 PTrail{False{}}
385
386def Http.step.trail(blank: Bool, +b: U32) -> Phase:
387 Http.step.trail.go(U32.is_eq(b, 10), U32.is_eq(b, 13), blank)
388
389
390def Http.step(r: Resp, +b: U32) -> Resp:
391 match r:
392 case Resp{PHead{buf, tail}, h, out}:
393 Http.step.head(buf, tail, b)
394 case Resp{PLen{+left}, h, out}:
395 Resp{Http.count((left - 1 : U32), False{}), h, b <> out}
396 case Resp{PSize{size, ext, any}, h, out}:
397 Resp{Http.step.size(size, ext, any, b), h, out}
398 case Resp{PData{+left}, h, out}:
399 Resp{Http.count((left - 1 : U32), True{}), h, b <> out}
400 case Resp{PCrlf{}, h, out}:
401 Resp{Http.step.crlf(b), h, out}
402 case Resp{PTrail{blank}, h, out}:
403 Resp{Http.step.trail(blank, b), h, out}
404 case Resp{PClose{}, h, out}:
405 Resp{PClose{}, h, b <> out}
406 case Resp{PDone{}, h, out}:
407 Resp{PErr{"bytes after the body"}, h, out}
408 case Resp{PErr{m}, h, out}:
409 Resp{PErr{m}, h, out}
410 case Resp{PLost{m}, h, out}:
411 Resp{PLost{m}, h, out}
412
413
414def Http.feed(bs: List<&2, U32>, r: Resp) -> Resp:
415 match bs:
416 case Nil{}:
417 r
418 case Con{+b, t}:
419 Http.feed(t, Http.step(r, b))
420
421
422def Http.take(r: Resp) -> Resp & List<&2, U32>:
423 match r:
424 case Resp{p, h, out}:
425 (Resp{p, h, Nil{}}, List.reverse(&2, U32, out))
426
427
428def Http.eof(r: Resp) -> Resp:
429 match r:
430 case Resp{PClose{}, h, out}:
431 Resp{PDone{}, h, out}
432 case Resp{PDone{}, h, out}:
433 Resp{PDone{}, h, out}
434 case Resp{PErr{m}, h, out}:
435 Resp{PErr{m}, h, out}
436 case Resp{PLost{m}, h, out}:
437 Resp{PLost{m}, h, out}
438 case Resp{PHead{Nil{}, t}, h, out}:
439 Resp{PLost{"connection closed before a response"}, h, out}
440 case Resp{p, h, out}:
441 Resp{PErr{"connection closed mid-response"}, h, out}
442
443def Http.is_done(r: Resp) -> Bool:
444 match r:
445 case Resp{PDone{}, h, out}:
446 True{}
447 case _:
448 False{}
449
450def Http.error(r: Resp) -> Maybe<&2, String>:
451 match r:
452 case Resp{PErr{m}, h, out}:
453 Some{m}
454 case Resp{PLost{m}, h, out}:
455 Some{m}
456 case _:
457 None{}
458
459def Http.decode.fin(rb: Resp & List<&2, U32>) -> Result<&2, &1, String, Head & String>:
460 match rb:
461 case (Resp{PDone{}, Some{h}, o}, body):
462 Done{(h, U.Utf8.decode_all(body))}
463 case (Resp{PErr{m}, h, o}, body):
464 Fail{m}
465 case (Resp{PLost{m}, h, o}, body):
466 Fail{m}
467 case _:
468 Fail{"incomplete response"}
469
470
471def Http.decode(bs: List<&2, U32>) -> Result<&2, &1, String, Head & String>:
472 Http.decode.fin(Http.take(Http.eof(Http.feed(bs, Http.resp.init()))))
473
474def Lines.cur.if(+b: U32, cur: List<&2, U32>, lf: Bool) -> List<&2, U32>:
475 match lf:
476 case True{}:
477 Nil{}
478 case False{}:
479 b <> cur
480
481def Lines.cur(+b: U32, +cur: List<&2, U32>) -> List<&2, U32>:
482 Lines.cur.if(b, cur, U32.is_eq(b, 10))
483
484def Lines.done.if(cur: List<&2, U32>, done: List<&2, List<&2, U32>>, lf: Bool) ->
485 List<&2, List<&2, U32>>:
486 match lf:
487 case True{}:
488 List.reverse(&2, U32, cur) <> done
489 case False{}:
490 done
491
492def Lines.done(+b: U32, +cur: List<&2, U32>, done: List<&2, List<&2, U32>>) ->
493 List<&2, List<&2, U32>>:
494 Lines.done.if(cur, done, U32.is_eq(b, 10))
495
496
497
498
499
500
501
502def Lines.go(bs: List<&2, U32>, +cur: List<&2, U32>, done: List<&2, List<&2, U32>>) ->
503 List<&2, U32> & List<&2, List<&2, U32>>:
504 match bs:
505 case Nil{}:
506 (cur, done)
507 case Con{+b, t}:
508 Lines.go(t, Lines.cur(b, cur), Lines.done(b, cur, done))
509
510def Lines.split.fin(r: List<&2, U32> & List<&2, List<&2, U32>>) ->
511 List<&2, U32> & List<&2, List<&2, U32>>:
512 match r:
513 case (cur, done):
514 (cur, List.reverse(&2, List<&2, U32>, done))
515
516
517def Lines.split(cur: List<&2, U32>, bs: List<&2, U32>) ->
518 List<&2, U32> & List<&2, List<&2, U32>>:
519 Lines.split.fin(Lines.go(bs, cur, Nil{}))