1 import Base 2 import ./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 12 type Header is Data: 13 Header{name: String, value: String} 14 15 type Req is Data: 16 Req{method: String, path: String, headers: List<&2, Header>, body: String} 17 18 type 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) 30 type 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. 44 type Resp is Data: 45 Resp{phase: Phase, head: Maybe<&2, Head>, out: List<&2, U32>} 46 47 # Text 48 # ---- 49 50 def Http.lower(s: String) -> String: 51 String.to_lower(s) 52 53 def 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 61 def 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). 69 def 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 79 def 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. 88 def 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 95 def 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). 103 def Http.pct_hex(+n: U32) -> Char: 104 Http.pct_hex.if(n, U32.is_lt(n, 10)) 105 106 def 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 119 def 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 127 def 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 134 def 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 141 def 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 151 def Http.parse_header(line: String) -> Maybe<&2, Header>: 152 Http.parse_header.go(String.split(line, ':')) 153 154 def 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 161 def 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 168 def 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 175 def 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. 183 def Http.parse_status(line: String) -> Maybe<&1, U32 & String>: 184 Http.parse_status.go(String.split(String.trim(line), ' ')) 185 186 def 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 193 def 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 200 def Http.parse_head(text: String) -> Maybe<&2, Head>: 201 Http.parse_head.go(String.lines(text)) 202 203 def Http.framing.te(chunked: Bool) -> Phase: 204 match chunked: 205 case True{}: 206 PSize{0, False{}, False{}} 207 case False{}: 208 PClose{} 209 210 def Http.framing.len.if(+k: U32, zero: Bool) -> Phase: 211 match zero: 212 case True{}: 213 PDone{} 214 case False{}: 215 PLen{k} 216 217 def 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 224 def 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 235 def 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 238 def 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). 243 def 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 251 def Http.resp.init() -> Resp: 252 Resp{PHead{Nil{}, 0}, None{}, Nil{}} 253 254 def 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. 266 def 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 269 def 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 276 def 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 279 def 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 286 def Http.step.head.if(buf: List<&2, U32>, +tail: U32) -> Resp: 287 Http.step.head.end(buf, tail, U32.is_eq(tail, 4)) 288 289 def 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 292 def 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. 304 def Http.count(+left: U32, chunk: Bool) -> Phase: 305 Http.count.if(left, chunk, U32.is_eq(left, 0)) 306 307 def 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 318 def 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 321 def 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 325 def 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 332 def 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 339 def 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. 359 def 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 363 def 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 372 def Http.step.crlf(+b: U32) -> Phase: 373 Http.step.crlf.go(U32.is_eq(b, 13), U32.is_eq(b, 10)) 374 375 def 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 386 def 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. 390 def 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. 414 def 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. 422 def 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. 428 def 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 443 def Http.is_done(r: Resp) -> Bool: 444 match r: 445 case Resp{PDone{}, h, out}: 446 True{} 447 case _: 448 False{} 449 450 def 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 459 def 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. 471 def 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 474 def 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 481 def Lines.cur(+b: U32, +cur: List<&2, U32>) -> List<&2, U32>: 482 Lines.cur.if(b, cur, U32.is_eq(b, 10)) 483 484 def 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 492 def 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 502 def 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 510 def 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. 517 def 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{}))