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# HTTP/1.1
5# ========
6#
7# Pure: a request renders to text, and a response decodes from bytes as they
8# arrive. The decoder is a byte-at-a-time automaton (so it terminates by
9# structure and streams), covering Content-Length, chunked and
10# read-until-close bodies, which is all the Kubernetes API server sends.
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# Where the decoder is.
22#   PHead: header bytes, reversed, and how much of CRLF CRLF ends them
23#   PLen: body bytes left under Content-Length
24#   PSize: a chunk's hex size so far; ext once past a ';' extension
25#   PData: chunk bytes left; PCrlf: the CRLF after a chunk's data
26#   PTrail: trailer lines after the last chunk; blank if this line is empty
27#   PClose: a body that runs to the end of the stream
28#   PErr: a broken response; PLost: the connection failed before any byte
29#   of a response (a request on it may safely be retried)
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# The decoder: its phase, the head once read, and body bytes decoded since
43# the last take, reversed.
44type Resp is Data:
45  Resp{phase: Phase, head: Maybe<&2, Head>, out: List<&2, U32>}
46
47# Text
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# The first header named name (case-insensitive).
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# Requests
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# The request's bytes, as text: a Host header, the given headers, and a
87# Content-Length that counts the body's UTF-8 bytes.
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# Percent-encodes a query or path component (RFC 3986 unreserved kept).
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# Response heads
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# "HTTP/1.1 200 OK": the status and the reason.
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# The body's framing, once the head is read: chunked, a length, or none
242# (204, 304 and 1xx carry no body).
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# The decoder
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# How far into CRLF CRLF the head is after byte b.
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# A counted run of body bytes: PLen or PData, with what follows at zero.
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# A byte of a chunk-size line. The line ends at LF; a zero size means the
358# last chunk, and its trailers follow.
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# One byte.
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# Feeds a cut of bytes.
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# Takes the body bytes decoded so far, in order, leaving none behind.
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# The stream ended: a read-until-close body is then complete.
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# A whole response from its bytes: the head and the decoded body.
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# Lines
497# -----
498#
499# A watch body is a stream of JSON objects, one per line. Lines splits
500# body bytes at LF, carrying the unfinished line (reversed) across cuts.
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# Splits a cut: the carried partial line, and the whole lines, in order.
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{}))