1 # HTTP/1.1 and HTTP/2 client, with an HTTP/1.1 server. Source: https://github.com/paymog/bend-kit/tree/main/http 2 import Base 3 import ./admission.bend as Admission 4 import ./completion.bend as Completion 5 # By hash: bend-kit-url@0.4.1.0, -encoding@0.3.0.0, -json@0.5.0.1, -bytes@0.3.0.0, -zlib@0.2.0.0. 6 import bend-kit-wire@0.4.6.0/wire.bend as Wire 7 import bend-kit-http2@0.1.2.0/http2.bend as H2 8 import bend-kit-http2@0.1.2.0/hpack.bend as Hpack 9 import bend-kit-bytes@0.3.1.0/bytes.bend as HBytes 10 import bend-kit-time@0.1.0.0/time.bend as Time 11 import bend-kit-int@0.2.0.0/int.bend as Int 12 import bend-kit-concurrency@0.1.0.0/concurrency.bend as Conc 13 import 0x1f2d80f53f971b16c6de6a65cb1918ae/url.bend as Url 14 import 0xcfc8be7b076f41f95c8e118383892d55/encoding.bend as Enc 15 import 0x584fc27920487ceab242392391418d7f/json.bend as Json 16 import 0x49814d83de8f70993a43e1002be29ecd/bytes.bend as Bytes 17 import bend-kit-dns@0.5.0.0/dns.bend as Dns 18 import 0xaca98ab7f724003ea421c18792cafe52/zlib.bend as Zlib 19 20 # HTTP/1.1 codec and client (RFC 9112) for http:// and https:// (OpenSSL 3 at run time). 21 # parse: start-line → field lines → blank → body length (§6.3); chunked (§7.1). 22 # Bodies are Bytes. The String parsers below are the spec: one Char per octet. 23 # fetch reads until frame() says the response is whole. It returns Result, not Maybe. 24 # Build with -o for big bodies: the `bend file.bend` runner overflows on ~30 KB strings; native binaries do not. 25 # import bend-kit-http@0.22.0.0/http.bend as Http 26 27 type Req is Type: 28 Req{method: String, path: String, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes} 29 30 type Res is Type: 31 Res{status: U32, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes} 32 33 # A typed Data payload can be captured in receipt; the response remains affine. 34 type Reply is Type: 35 Reply{response: Res, receipt: Completion.Receipt} 36 37 def reply.complete(~D: Data, ~done: Completion.Completion -> IO(Unit), payload: D, response: Res) -> Reply: 38 Reply{response, Completion.receipt(~D, ~done, payload)} 39 40 41 # The body type and its conversions, so a caller needs no bytes import. 42 def Body() -> Type: 43 Bytes.Bytes 44 45 # A byte string (one Char per octet) to a body, and back. 46 def from_string(+s: String) -> Bytes.Bytes: 47 Bytes.from_string(s) 48 49 def to_string(b: Bytes.Bytes) -> String: 50 Bytes.to_string(b) 51 52 def length(b: Bytes.Bytes) -> Bytes.Bytes & U32: 53 Bytes.length(b) 54 55 # Why fetch failed. Wire errors keep the errno and the text the effect already had. 56 # ponytail: 60 and 110 are ETIMEDOUT on macOS and Linux. Windows is out of scope. 57 type Err is Data: 58 ErrUrl{} 59 ErrDns{} 60 ErrConnect{code: U32, why: String} 61 ErrTls{code: U32, why: String} 62 ErrRead{code: U32, why: String} 63 ErrWrite{code: U32, why: String} 64 ErrTimeout{} 65 ErrRedirect{} 66 ErrBad{} 67 68 def err.late(+code: U32) -> Bool: 69 Bool.or(U32.is_eq(code, 60), U32.is_eq(code, 110)) 70 71 def err.pick(e: Err, late: Bool) -> Err: 72 match late: 73 case True{}: 74 ErrTimeout{} 75 case False{}: 76 e 77 78 def err.or_late(code: U32, e: Err) -> Err: 79 err.pick(e, err.late(code)) 80 81 type Fields is Data: 82 FieldsBad{} 83 FieldsOk{m: Map<&2, List<&2, String>>} 84 85 # One list per field name, in arrival order. header() is the first value. 86 # Set-Cookie is never joined. Encode writes one line per value. 87 def empty() -> Map<&2, List<&2, String>>: 88 Map.new(&2, List<&2, String>) 89 90 def fields.of(r: Map<&2, List<&2, String>> & List<&2, String>) -> List<&2, String>: 91 (h, xs) = r 92 xs 93 94 def fields(+h: Map<&2, List<&2, String>>, k: String) -> List<&2, String>: 95 fields.of(Map.get(List<&2, String>, Nil{}, h, k)) 96 97 def header.first(xs: List<&2, String>) -> String: 98 match xs: 99 case Nil{}: 100 "" 101 case Con{v, t}: 102 v 103 104 def header(+h: Map<&2, List<&2, String>>, k: String) -> String: 105 header.first(fields(h, k)) 106 107 def field.last(xs: List<&2, String>) -> String: 108 match xs: 109 case Nil{}: 110 "" 111 case Con{v, Nil{}}: 112 v 113 case Con{v, t}: 114 field.last(t) 115 116 # Transfer-Encoding is a list; chunked, when present, is the last coding. 117 def header.last(+h: Map<&2, List<&2, String>>, k: String) -> String: 118 field.last(fields(h, k)) 119 120 def snoc(xs: List<&2, String>, v: String) -> List<&2, String>: 121 match xs: 122 case Nil{}: 123 [v] 124 case Con{h, t}: 125 Con{h, snoc(t, v)} 126 127 def set(m: Map<&2, List<&2, String>>, k: String, v: String) -> Map<&2, List<&2, String>>: 128 Map.set(&2, List<&2, String>, m, k, [v]) 129 130 def add(+m: Map<&2, List<&2, String>>, +k: String, v: String) -> Map<&2, List<&2, String>>: 131 Map.set(&2, List<&2, String>, m, k, snoc(fields(m, k), v)) 132 133 def sanitize.cons(ch: Char, rest: String, drop: Bool) -> String: 134 match drop: 135 case True{}: 136 rest 137 case False{}: 138 SCon{ch, rest} 139 140 def sanitize(s: String) -> String: 141 match s: 142 case SNil{}: 143 SNil{} 144 case SCon{Chr{+c}, t}: 145 sanitize.cons(Chr{c}, sanitize(t), Bool.or(U32.is_eq(c, 13), U32.is_eq(c, 10))) 146 147 def drop_cr.last.if(cr: Bool, c: U32) -> String: 148 match cr: 149 case True{}: 150 SNil{} 151 case False{}: 152 SCon{Chr{c}, SNil{}} 153 154 def drop_cr.last(+c: U32) -> String: 155 drop_cr.last.if(U32.is_eq(c, 13), c) 156 157 # prev is the char before s; it is kept unless it is a final CR. 158 def drop_cr.go(s: String, prev: U32) -> String: 159 match s: 160 case SNil{}: 161 drop_cr.last(prev) 162 case SCon{Chr{c}, t}: 163 SCon{Chr{prev}, drop_cr.go(t, c)} 164 165 def drop_cr(s: String) -> String: 166 match s: 167 case SNil{}: 168 SNil{} 169 case SCon{Chr{c}, t}: 170 drop_cr.go(t, c) 171 172 # w holds the last four octets; a char past 255 clears it. 173 def blank_end.go(s: String, w: U32) -> Bool: 174 match s: 175 case SNil{}: 176 U32.is_eq(w, 218762506) 177 case SCon{Chr{+c}, t}: 178 blank_end.go(t, Bool.pick(U32, U32.is_lt(c, 256), (w * 256 + c : U32), 0)) 179 180 # String.ends_with(s, "\r\n\r\n") in one pass, with no reversed copy. 181 def blank_end(s: String) -> Bool: 182 blank_end.go(s, 0) 183 184 def trim_end.cons(h: Char, r: String, space: Bool) -> String: 185 match r: 186 case SNil{}: 187 match space: 188 case True{}: 189 SNil{} 190 case False{}: 191 SCon{h, SNil{}} 192 case SCon{a, t}: 193 SCon{h, SCon{a, t}} 194 195 # String.trim_end without the two reversed copies. 196 def trim_end(s: String) -> String: 197 match s: 198 case SNil{}: 199 SNil{} 200 case SCon{+h, t}: 201 trim_end.cons(h, trim_end(t), Char.is_space(h)) 202 203 def trim(s: String) -> String: 204 trim_end(String.trim_start(s)) 205 206 # Bool.pick evaluates both arms; a split that recursed inside it would reverse acc at every byte. 207 def take.cut(acc: String, rest: String) -> String & String: 208 (String.reverse(String.drop(acc, 1n)), rest) 209 210 # hit: the last char pushed onto acc was the stop char. 211 def take_sp.go(s: String, acc: String, hit: Bool) -> String & String: 212 match s: 213 case SNil{}: 214 match hit: 215 case True{}: 216 take.cut(acc, SNil{}) 217 case False{}: 218 (String.reverse(acc), SNil{}) 219 case SCon{Chr{+c}, t}: 220 match hit: 221 case True{}: 222 take.cut(acc, SCon{Chr{c}, t}) 223 case False{}: 224 take_sp.go(t, SCon{Chr{c}, acc}, U32.is_eq(c, 32)) 225 226 def take_sp(s: String) -> String & String: 227 take_sp.go(s, SNil{}, False{}) 228 229 def start_line.ver(m: String, pv: String & String) -> String & String & String: 230 (p, v) = pv 231 (m, p, drop_cr(v)) 232 233 def start_line.rest(mr: String & String) -> String & String & String: 234 (m, r) = mr 235 start_line.ver(m, take_sp(r)) 236 237 def start_line(s: String) -> String & String & String: 238 start_line.rest(take_sp(s)) 239 240 def split_at_blank.go(s: String, e: Bool, w: U32, acc: String) -> String & String: 241 match s: 242 case SNil{}: 243 (String.reverse(acc), SNil{}) 244 case SCon{Chr{+a}, t}: 245 match e: 246 case True{}: 247 (String.reverse(acc), SCon{Chr{a}, t}) 248 case False{}: 249 +w2 = (w * 256 + a : U32) 250 split_at_blank.go(t, U32.is_eq(w2, 218762506), w2, SCon{Chr{a}, acc}) 251 252 # split_at_blank, plus whether the head ends in the blank line. 253 def split_at_blank.flag.go(+raw: String, s: String, off: Nat, w: U32, e: Bool) -> String & String & Bool: 254 match s: 255 case SNil{}: 256 (String.take(raw, off), SNil{}, e) 257 case SCon{Chr{+a}, t}: 258 match e: 259 case True{}: 260 (String.take(raw, off), SCon{Chr{a}, t}, True{}) 261 case False{}: 262 +w2 = (w * 256 + a : U32) 263 +off1 = (1n + off : Nat) 264 split_at_blank.flag.go(raw, t, off1, w2, U32.is_eq(w2, 218762506)) 265 266 def split_at_blank.pair(r: String & String & Bool) -> String & String: 267 (head, rest, whole) = r 268 (head, rest) 269 270 def split_at_blank(+s: String) -> String & String: 271 split_at_blank.pair(split_at_blank.flag.go(s, s, 0n, 0, False{})) 272 273 def split_colon.go(s: String, acc: String, hit: Bool) -> String & String: 274 match s: 275 case SNil{}: 276 match hit: 277 case True{}: 278 take.cut(acc, SNil{}) 279 case False{}: 280 (String.reverse(acc), SNil{}) 281 case SCon{Chr{+c}, t}: 282 match hit: 283 case True{}: 284 take.cut(acc, SCon{Chr{c}, t}) 285 case False{}: 286 split_colon.go(t, SCon{Char.to_lower(Chr{c}), acc}, U32.is_eq(c, 58)) 287 288 def split_colon(+s: String) -> String & String: 289 split_colon.go(s, SNil{}, False{}) 290 291 def has_header.of(r: Map<&2, List<&2, String>> & Bool) -> Bool: 292 (h, b) = r 293 b 294 295 def has_header(+h: Map<&2, List<&2, String>>, k: String) -> Bool: 296 has_header.of(Map.has(&2, List<&2, String>, h, k)) 297 298 def folded(+line: String) -> Bool: 299 Bool.or(String.starts_with(line, " "), String.starts_with(line, "\t")) 300 301 def tracked(+k: String) -> Bool: 302 Bool.or(String.eq(k, "host"), String.eq(k, "content-length")) 303 304 def fields_put.dup(m: Map<&2, List<&2, String>>, k: String, v: String, bad: Bool) -> Fields: 305 match bad: 306 case True{}: 307 FieldsBad{} 308 case False{}: 309 FieldsOk{add(m, k, v)} 310 311 def fields_put.tracked(+m: Map<&2, List<&2, String>>, +k: String, v: String, t: Bool) -> Fields: 312 match t: 313 case True{}: 314 fields_put.dup(m, k, v, has_header(m, k)) 315 case False{}: 316 FieldsOk{add(m, k, v)} 317 318 def fields_put.key(+m: Map<&2, List<&2, String>>, +k: String, v: String) -> Fields: 319 fields_put.tracked(m, k, v, tracked(k)) 320 321 def fields_put.kv(m: Map<&2, List<&2, String>>, kv: String & String) -> Fields: 322 (k, v) = kv 323 fields_put.key(m, k, trim(v)) 324 325 def fields_put.fold(m: Map<&2, List<&2, String>>, line: String, fold: Bool) -> Fields: 326 match fold: 327 case True{}: 328 FieldsBad{} 329 case False{}: 330 fields_put.kv(m, split_colon(line)) 331 332 def fields_put.empty(m: Map<&2, List<&2, String>>, +line: String, e: Bool) -> Fields: 333 match e: 334 case True{}: 335 FieldsOk{m} 336 case False{}: 337 fields_put.fold(m, line, folded(line)) 338 339 def fields_put.ok(m: Map<&2, List<&2, String>>, +line: String) -> Fields: 340 fields_put.empty(m, line, String.is_empty(line)) 341 342 def fields_put(acc: Fields, line: String) -> Fields: 343 match acc: 344 case FieldsBad{}: 345 FieldsBad{} 346 case FieldsOk{m}: 347 fields_put.ok(m, line) 348 349 def parse.headers.done(acc: Fields) -> Maybe<&2, Map<&2, List<&2, String>>>: 350 match acc: 351 case FieldsBad{}: 352 None{} 353 case FieldsOk{m}: 354 Some{m} 355 356 357 def parse.headers.scan.line(acc: String, fields: Fields) -> Fields: 358 match acc: 359 case SCon{Chr{13}, t}: 360 fields_put(fields, String.reverse(t)) 361 case other: 362 fields_put(fields, String.reverse(other)) 363 364 def parse.headers.scan(s: String, acc: String, fields: Fields) -> Fields: 365 match s: 366 case SNil{}: 367 parse.headers.scan.line(acc, fields) 368 case SCon{Chr{10}, t}: 369 parse.headers.scan(t, SNil{}, parse.headers.scan.line(acc, fields)) 370 case SCon{c, t}: 371 parse.headers.scan(t, SCon{c, acc}, fields) 372 373 def parse.headers.raw(s: String) -> Maybe<&2, Map<&2, List<&2, String>>>: 374 parse.headers.done(parse.headers.scan(s, SNil{}, FieldsOk{empty()})) 375 376 def parse.first(s: String, acc: String) -> String & String: 377 match s: 378 case SNil{}: 379 (String.reverse(acc), SNil{}) 380 case SCon{Chr{10}, t}: 381 (String.reverse(acc), t) 382 case SCon{c, t}: 383 parse.first(t, SCon{c, acc}) 384 385 def parse_u32.digit(+c: U32) -> Bool: 386 Bool.and(U32.is_le(48, c), U32.is_le(c, 57)) 387 388 def parse_u32.add(acc: U32, c: U32) -> U32: 389 (acc * 10 + (c - 48 : U32) : U32) 390 391 def parse_u32.done(acc: U32, bad: Bool) -> Maybe<&2, U32>: 392 match bad: 393 case True{}: 394 None{} 395 case False{}: 396 Some{acc} 397 398 def parse_u32.go(s: String, acc: U32, bad: Bool) -> Maybe<&2, U32>: 399 match s: 400 case SNil{}: 401 parse_u32.done(acc, bad) 402 case SCon{Chr{+c}, t}: 403 parse_u32.go(t, parse_u32.add(acc, c), Bool.or(bad, Bool.not(parse_u32.digit(c)))) 404 405 def parse_u32(s: String) -> Maybe<&2, U32>: 406 match s: 407 case SNil{}: 408 None{} 409 case SCon{Chr{+c}, t}: 410 parse_u32.go(t, parse_u32.add(0, c), Bool.not(parse_u32.digit(c))) 411 412 # More: a valid prefix. Bad: cannot become a body. Body: a whole chunked body. 413 type Hold is Data: 414 HoldMore{} 415 HoldBad{} 416 HoldBody{b: String} 417 HoldUntil{b: String} 418 419 def is_hex(+c: U32) -> Bool: 420 Bool.or(parse_u32.digit(c), Bool.or(Bool.and(U32.is_le(97, c), U32.is_le(c, 102)), Bool.and(U32.is_le(65, c), U32.is_le(c, 70)))) 421 422 def hexv(+c: U32) -> U32: 423 Bool.pick(U32, parse_u32.digit(c), (c - 48 : U32), Bool.pick(U32, U32.is_le(c, 70), (c - 55 : U32), (c - 87 : U32))) 424 425 # A chunked body decoder that reads each byte once, so a body fed in pieces 426 # costs O(bytes) in total. Tr{j}: j bytes of the closing CRLF CRLF matched. 427 type Dc is Data: 428 DcSize{acc: U32, seen: Bool} 429 DcExt{n: U32} 430 DcSizeLf{n: U32} 431 DcData{k: U32} 432 DcDataCr{} 433 DcDataLf{} 434 DcTr{j: U32} 435 DcDone{} 436 DcBad{} 437 438 # st: where the decoder is. racc: the body so far, reversed. rest: bytes after a Done body. 439 type Dco is Data: 440 Dco{st: Dc, racc: String, rest: String} 441 442 def dc.start() -> Dc: 443 DcSize{0, False{}} 444 445 def dc.size.semi(+n: U32, semi: Bool) -> Dc: 446 match semi: 447 case True{}: 448 DcExt{n} 449 case False{}: 450 DcBad{} 451 452 def dc.size.cr(+n: U32, +c: U32, cr: Bool) -> Dc: 453 match cr: 454 case True{}: 455 DcSizeLf{n} 456 case False{}: 457 dc.size.semi(n, U32.is_eq(c, 59)) 458 459 def dc.size.end(+n: U32, +c: U32, seen: Bool) -> Dc: 460 match seen: 461 case False{}: 462 DcBad{} 463 case True{}: 464 dc.size.cr(n, c, U32.is_eq(c, 13)) 465 466 # A size that would overflow 32 bits ends the digits, and size.end then refuses it. 467 def dc.size(+acc: U32, seen: Bool, +c: U32, digit: Bool) -> Dc: 468 match digit: 469 case True{}: 470 DcSize{(acc * 16 + hexv(c) : U32), True{}} 471 case False{}: 472 dc.size.end(acc, c, seen) 473 474 # RFC 9112 §7.1.1: recipients ignore unknown chunk extensions; a bare LF is not CRLF. 475 def dc.ext.lf(+n: U32, lf: Bool) -> Dc: 476 match lf: 477 case True{}: 478 DcBad{} 479 case False{}: 480 DcExt{n} 481 482 def dc.ext(+n: U32, +c: U32, cr: Bool) -> Dc: 483 match cr: 484 case True{}: 485 DcSizeLf{n} 486 case False{}: 487 dc.ext.lf(n, U32.is_eq(c, 10)) 488 489 def dc.sized(+n: U32, zero: Bool) -> Dc: 490 match zero: 491 case True{}: 492 DcTr{2} 493 case False{}: 494 DcData{n} 495 496 def dc.size.lf(+n: U32, lf: Bool) -> Dc: 497 match lf: 498 case True{}: 499 dc.sized(n, U32.is_eq(n, 0)) 500 case False{}: 501 DcBad{} 502 503 # k data bytes are left and m of them were just read. 504 def dc.data(+k: U32, +m: U32) -> Dc: 505 Bool.pick(Dc, U32.is_le(k, m), DcDataCr{}, DcData{(k - m : U32)}) 506 507 def dc.pick(ok: Bool, next: Dc) -> Dc: 508 match ok: 509 case True{}: 510 next 511 case False{}: 512 DcBad{} 513 514 # ponytail: trailer fields are dropped unparsed (§7.1.2 allows discarding) 515 def dc.tr.hit(+j: U32) -> Dc: 516 Bool.pick(Dc, U32.is_eq(j, 3), DcDone{}, DcTr{(j + 1 : U32)}) 517 518 def dc.tr(+j: U32, +c: U32, hit: Bool) -> Dc: 519 match hit: 520 case True{}: 521 dc.tr.hit(j) 522 case False{}: 523 Bool.pick(Dc, U32.is_eq(c, 13), DcTr{1}, DcTr{0}) 524 525 def dc.tr.want(+j: U32) -> U32: 526 Bool.pick(U32, U32.is_eq(U32.mod(j, 2), 0), 13, 10) 527 528 def dc.byte(h: Char) -> U32: 529 Chr{c} = h 530 c 531 532 # One byte c. Done and Bad never look at it, and a data byte is not inspected. 533 def dc.step(st: Dc, +c: U32) -> Dc: 534 match st: 535 case DcBad{}: 536 DcBad{} 537 case DcSize{+acc, seen}: 538 dc.size(acc, seen, c, Bool.and(is_hex(c), U32.is_le(acc, 268435455))) 539 case DcExt{+n}: 540 dc.ext(n, c, U32.is_eq(c, 13)) 541 case DcSizeLf{+n}: 542 dc.size.lf(n, U32.is_eq(c, 10)) 543 case DcDataCr{}: 544 dc.pick(U32.is_eq(c, 13), DcDataLf{}) 545 case DcDataLf{}: 546 dc.pick(U32.is_eq(c, 10), dc.start()) 547 case DcTr{+j}: 548 dc.tr(j, c, U32.is_eq(c, dc.tr.want(j))) 549 case DcData{+k}: 550 dc.data(k, 1) 551 case DcDone{}: 552 DcDone{} 553 554 def dc.live(st: Dc) -> Bool: 555 match st: 556 case DcDone{}: 557 False{} 558 case DcBad{}: 559 False{} 560 case DcSize{a, b}: 561 True{} 562 case DcExt{n}: 563 True{} 564 case DcSizeLf{n}: 565 True{} 566 case DcData{k}: 567 True{} 568 case DcDataCr{}: 569 True{} 570 case DcDataLf{}: 571 True{} 572 case DcTr{j}: 573 True{} 574 575 def dc.feed(s: String, st: Dc, racc: String) -> Dco: 576 match s: 577 case SNil{}: 578 Dco{st, racc, ""} 579 case SCon{h, t}: 580 match st: 581 case DcBad{}: 582 Dco{DcBad{}, racc, ""} 583 case DcDone{}: 584 Dco{DcDone{}, racc, SCon{h, t}} 585 case DcData{+k}: 586 dc.feed(t, dc.data(k, 1), SCon{h, racc}) 587 case DcSize{+acc, seen}: 588 dc.feed(t, dc.step(DcSize{acc, seen}, dc.byte(h)), racc) 589 case DcExt{+n}: 590 dc.feed(t, dc.step(DcExt{n}, dc.byte(h)), racc) 591 case DcSizeLf{+n}: 592 dc.feed(t, dc.step(DcSizeLf{n}, dc.byte(h)), racc) 593 case DcDataCr{}: 594 dc.feed(t, dc.step(DcDataCr{}, dc.byte(h)), racc) 595 case DcDataLf{}: 596 dc.feed(t, dc.step(DcDataLf{}, dc.byte(h)), racc) 597 case DcTr{+j}: 598 dc.feed(t, dc.step(DcTr{j}, dc.byte(h)), racc) 599 600 def chunk.hold(o: Dco) -> Hold: 601 Dco{st, racc, rest} = o 602 match st: 603 case DcDone{}: 604 HoldBody{String.reverse(racc)} 605 case DcBad{}: 606 HoldBad{} 607 case DcSize{a, b}: 608 HoldMore{} 609 case DcExt{n}: 610 HoldMore{} 611 case DcSizeLf{n}: 612 HoldMore{} 613 case DcData{k}: 614 HoldMore{} 615 case DcDataCr{}: 616 HoldMore{} 617 case DcDataLf{}: 618 HoldMore{} 619 case DcTr{j}: 620 HoldMore{} 621 622 def chunk.live(s: String) -> Hold: 623 chunk.hold(dc.feed(s, dc.start(), "")) 624 625 def chunk.rest(o: Dco) -> Maybe<&2, String>: 626 Dco{st, racc, rest} = o 627 match st: 628 case DcDone{}: 629 Some{rest} 630 case DcBad{}: 631 None{} 632 case DcSize{a, b}: 633 None{} 634 case DcExt{n}: 635 None{} 636 case DcSizeLf{n}: 637 None{} 638 case DcData{k}: 639 None{} 640 case DcDataCr{}: 641 None{} 642 case DcDataLf{}: 643 None{} 644 case DcTr{j}: 645 None{} 646 647 def chunk.suffix(s: String) -> Maybe<&2, String>: 648 chunk.rest(dc.feed(s, dc.start(), "")) 649 650 def chunk.decode.hold(h: Hold) -> Maybe<&2, String>: 651 match h: 652 case HoldMore{}: 653 None{} 654 case HoldBad{}: 655 None{} 656 case HoldUntil{b}: 657 None{} 658 case HoldBody{b}: 659 Some{b} 660 661 def chunk.decode(s: String) -> Maybe<&2, String>: 662 chunk.decode.hold(chunk.live(s)) 663 664 def bytes.snd(r: Bytes.Bytes & Bytes.Bytes) -> Bytes.Bytes: 665 (a, b) = r 666 b 667 668 def found.at(+from: U32, m: Maybe<&2, U32>) -> Maybe<&2, U32>: 669 match m: 670 case None{}: 671 None{} 672 case Some{+j}: 673 Some{(from + j : U32)} 674 675 # A chunked body decoded from piece[i..]: a data run is sliced whole, and every other byte steps dc. 676 type Cs is Type: 677 Cs{piece: Bytes.Bytes, i: U32, st: Dc, parts: List<&1, Bytes.Bytes>} 678 679 # st: where the decoder stopped. parts: the data so far, newest first. rest: the bytes after a Done body. 680 type Ck is Type: 681 Ck{st: Dc, parts: List<&1, Bytes.Bytes>, rest: Bytes.Bytes} 682 683 def ck.next(+len: U32, +i: U32, +st: Dc, piece: Bytes.Bytes, parts: List<&1, Bytes.Bytes>) -> Bool & Cs: 684 (Bool.and(U32.is_lt(i, len), dc.live(st)), Cs{piece, i, st, parts}) 685 686 def ck.data(+len: U32, +i: U32, +k: U32, +m: U32, parts: List<&1, Bytes.Bytes>, r: Bytes.Bytes & Bytes.Bytes) -> Bool & Cs: 687 (piece, run) = r 688 ck.next(len, (i + m : U32), dc.data(k, m), piece, Con{run, parts}) 689 690 def ck.byte(+len: U32, +i: U32, st: Dc, parts: List<&1, Bytes.Bytes>, r: Bytes.Bytes & Maybe<&2, U32>) -> Bool & Cs: 691 (piece, m) = r 692 match m: 693 case None{}: 694 ck.next(len, i, DcBad{}, piece, parts) 695 case Some{+c}: 696 ck.next(len, (i + 1 : U32), dc.step(st, c), piece, parts) 697 698 def ck.step(+len: U32, cs: Cs) -> Bool & Cs: 699 Cs{piece, +i, st, parts} = cs 700 match st: 701 case DcData{+k}: 702 +m = U32.min(k, (len - i : U32)) 703 ck.data(len, i, k, m, parts, Bytes.slice(piece, i, m)) 704 case DcDone{}: 705 (False{}, Cs{piece, i, DcDone{}, parts}) 706 case DcBad{}: 707 (False{}, Cs{piece, i, DcBad{}, parts}) 708 case DcSize{+acc, seen}: 709 ck.byte(len, i, DcSize{acc, seen}, parts, Bytes.get(piece, i)) 710 case DcExt{+n}: 711 ck.byte(len, i, DcExt{n}, parts, Bytes.get(piece, i)) 712 case DcSizeLf{+n}: 713 ck.byte(len, i, DcSizeLf{n}, parts, Bytes.get(piece, i)) 714 case DcDataCr{}: 715 ck.byte(len, i, DcDataCr{}, parts, Bytes.get(piece, i)) 716 case DcDataLf{}: 717 ck.byte(len, i, DcDataLf{}, parts, Bytes.get(piece, i)) 718 case DcTr{+j}: 719 ck.byte(len, i, DcTr{j}, parts, Bytes.get(piece, i)) 720 721 def ck.rest(st: Dc, parts: List<&1, Bytes.Bytes>, r: Bytes.Bytes & Bytes.Bytes) -> Ck: 722 (piece, rest) = r 723 Ck{st, parts, rest} 724 725 def ck.end(+len: U32, cs: Cs) -> Ck: 726 Cs{piece, +i, st, parts} = cs 727 ck.rest(st, parts, Bytes.slice(piece, i, (len - i : U32))) 728 729 # Each step reads at least one byte, so len steps are enough. 730 def ck.feed(f: Nat, +len: U32, r: Bool & Cs) -> Ck: 731 match f: 732 case 0n: 733 (go, cs) = r 734 ck.end(len, cs) 735 case 1n+p: 736 (go, cs) = r 737 match go: 738 case False{}: 739 ck.end(len, cs) 740 case True{}: 741 ck.feed(p, len, ck.step(len, cs)) 742 743 def ck(piece: Bytes.Bytes, +st: Dc, parts: List<&1, Bytes.Bytes>) -> Ck: 744 Bytes.Bytes{+len, buf} = piece 745 ck.feed(U32.to_nat(len), len, ck.next(len, 0, st, Bytes.Bytes{len, buf}, parts)) 746 747 def parse.res.cl.have(+rest: String, +k: Nat, short: Bool) -> Hold: 748 match short: 749 case True{}: 750 HoldMore{} 751 case False{}: 752 HoldBody{String.take(rest, k)} 753 754 def parse.res.cl.k(+rest: String, +k: Nat) -> Hold: 755 parse.res.cl.have(rest, k, Nat.is_lt(String.length(rest), k)) 756 757 def parse.res.cl.n(rest: String, n: Maybe<&2, U32>) -> Hold: 758 match n: 759 case None{}: 760 HoldBad{} 761 case Some{k}: 762 parse.res.cl.k(rest, U32.to_nat(k)) 763 764 def parse.body.cl(+h: Map<&2, List<&2, String>>, rest: String, hascl: Bool) -> Hold: 765 match hascl: 766 case False{}: 767 HoldBody{""} 768 case True{}: 769 parse.res.cl.n(rest, parse_u32(header(h, "content-length"))) 770 771 # RFC 9112 §6.3 (4): a request coding other than chunked cannot be framed. 772 def parse.body.chunked.eq(rest: String, ok: Bool) -> Hold: 773 match ok: 774 case False{}: 775 HoldBad{} 776 case True{}: 777 chunk.live(rest) 778 779 def parse.body.chunked.val(te: String, rest: String) -> Hold: 780 parse.body.chunked.eq(rest, String.eq(String.to_lower(te), "chunked")) 781 782 def parse.body.chunked(+h: Map<&2, List<&2, String>>, rest: String, hascl: Bool) -> Hold: 783 match hascl: 784 case True{}: 785 HoldBad{} 786 case False{}: 787 parse.body.chunked.val(header.last(h, "transfer-encoding"), rest) 788 789 def parse.body.te(+h: Map<&2, List<&2, String>>, rest: String, te: Bool) -> Hold: 790 match te: 791 case True{}: 792 parse.body.chunked(h, rest, has_header(h, "content-length")) 793 case False{}: 794 parse.body.cl(h, rest, has_header(h, "content-length")) 795 796 def parse.body(+h: Map<&2, List<&2, String>>, rest: String) -> Hold: 797 parse.body.te(h, rest, has_header(h, "transfer-encoding")) 798 799 # Bad: never a request. More: the head is not whole; read on. Head: the head is whole 800 # and valid, and the body is still coming (req.body is empty). Req: a whole request. 801 type Got is Type: 802 GotBad{} 803 GotMore{} 804 GotHead{req: Req} 805 GotReq{req: Req} 806 807 def parse.finish(method: String, path: String, h: Map<&2, List<&2, String>>, body: Hold) -> Got: 808 match body: 809 case HoldBad{}: 810 GotBad{} 811 case HoldMore{}: 812 GotHead{Req{method, path, h, Bytes.new(0)}} 813 case HoldUntil{b}: 814 GotBad{} 815 case HoldBody{b}: 816 GotReq{Req{method, path, h, Bytes.from_string(b)}} 817 818 def parse.host(method: String, path: String, +h: Map<&2, List<&2, String>>, rest: String, has: Bool) -> Got: 819 match has: 820 case False{}: 821 GotBad{} 822 case True{}: 823 parse.finish(method, path, h, parse.body(h, rest)) 824 825 # RFC 9112 §3.2: only HTTP/1.1 requires Host. 826 def parse.fields(method: String, path: String, rest: String, v11: Bool, fs: Maybe<&2, Map<&2, List<&2, String>>>) -> Got: 827 match fs: 828 case None{}: 829 GotBad{} 830 case Some{+h}: 831 parse.host(method, path, h, rest, Bool.or(Bool.not(v11), has_header(h, "host"))) 832 833 def target_ok(+p: String) -> Bool: 834 Bool.or(String.starts_with(p, "/"), Bool.or(String.eq(p, "*"), String.starts_with(p, "http://"))) 835 836 def parse.target(method: String, +path: String, headers: String, rest: String, v11: Bool, ok: Bool) -> Got: 837 match ok: 838 case False{}: 839 GotBad{} 840 case True{}: 841 parse.fields(method, path, rest, v11, parse.headers.raw(headers)) 842 843 def parse.ver(method: String, +path: String, +v: String, headers: String, rest: String, ok: Bool) -> Got: 844 match ok: 845 case False{}: 846 GotBad{} 847 case True{}: 848 parse.target(method, path, headers, rest, String.eq(v, "HTTP/1.1"), target_ok(path)) 849 850 def parse.empty_method(method: String, path: String, +v: String, headers: String, rest: String, empty: Bool) -> Got: 851 match empty: 852 case True{}: 853 GotBad{} 854 case False{}: 855 parse.ver(method, path, v, headers, rest, Bool.or(String.eq(v, "HTTP/1.1"), String.eq(v, "HTTP/1.0"))) 856 857 def parse.start3(mpv: String & String & String, headers: String, rest: String) -> Got: 858 (+method, path, v) = mpv 859 parse.empty_method(method, path, v, headers, rest, String.is_empty(method)) 860 861 def parse.lines(lr: String & String, rest: String) -> Got: 862 (line, headers) = lr 863 parse.start3(start_line(line), headers, rest) 864 865 866 def parse.head(+head: String, rest: String, whole: Bool) -> Got: 867 match whole: 868 case False{}: 869 GotMore{} 870 case True{}: 871 parse.lines(parse.first(head, SNil{}), rest) 872 873 def parse.of(hb: String & String & Bool) -> Got: 874 (head, rest, whole) = hb 875 parse.head(head, rest, whole) 876 877 # serve reads until this is not GotMore. 878 def parse.got(+raw: String) -> Got: 879 parse.of(split_at_blank.flag.go(raw, raw, 0n, 0, False{})) 880 881 882 def parse.req(g: Got) -> Maybe<&1, Req>: 883 match g: 884 case GotReq{req}: 885 Some{req} 886 case GotBad{}: 887 None{} 888 case GotMore{}: 889 None{} 890 case GotHead{req}: 891 None{} 892 893 def parse(raw: String) -> Maybe<&1, Req>: 894 parse.req(parse.got(raw)) 895 896 def parse.res.cl(+h: Map<&2, List<&2, String>>, rest: String, hascl: Bool) -> Hold: 897 match hascl: 898 case False{}: 899 HoldBody{rest} 900 case True{}: 901 parse.res.cl.n(rest, parse_u32(header(h, "content-length"))) 902 903 def parse.res.chunked.eq(rest: String, ok: Bool) -> Hold: 904 match ok: 905 case False{}: 906 HoldUntil{rest} 907 case True{}: 908 chunk.live(rest) 909 910 def parse.res.chunked.val(te: String, rest: String) -> Hold: 911 parse.res.chunked.eq(rest, String.eq(String.to_lower(te), "chunked")) 912 913 def parse.res.chunked(+h: Map<&2, List<&2, String>>, rest: String, hascl: Bool) -> Hold: 914 match hascl: 915 case True{}: 916 HoldBad{} 917 case False{}: 918 parse.res.chunked.val(header.last(h, "transfer-encoding"), rest) 919 920 def parse.res.te(+h: Map<&2, List<&2, String>>, rest: String, te: Bool) -> Hold: 921 match te: 922 case True{}: 923 parse.res.chunked(h, rest, has_header(h, "content-length")) 924 case False{}: 925 parse.res.cl(h, rest, has_header(h, "content-length")) 926 927 def parse.res.body(+h: Map<&2, List<&2, String>>, rest: String) -> Hold: 928 parse.res.te(h, rest, has_header(h, "transfer-encoding")) 929 930 # Bad: never a message. More: a valid prefix; read on. Done: a whole response. 931 type Frame is Type: 932 FrameBad{} 933 FrameMore{} 934 FrameDone{res: Res} 935 936 # RFC 9112 §8: a message cut short by close is incomplete, not valid. 937 def frame.cut(closed: Bool) -> Frame: 938 match closed: 939 case True{}: 940 FrameBad{} 941 case False{}: 942 FrameMore{} 943 944 def frame.wait(res: Res, wait: Bool) -> Frame: 945 match wait: 946 case True{}: 947 FrameMore{} 948 case False{}: 949 FrameDone{res} 950 951 def frame.done(status: U32, h: Map<&2, List<&2, String>>, body: Hold, +closed: Bool, open: Bool) -> Frame: 952 match body: 953 case HoldBad{}: 954 FrameBad{} 955 case HoldMore{}: 956 frame.cut(closed) 957 case HoldUntil{b}: 958 frame.wait(Res{status, h, Bytes.from_string(b)}, Bool.not(closed)) 959 case HoldBody{b}: 960 frame.wait(Res{status, h, Bytes.from_string(b)}, Bool.and(open, Bool.not(closed))) 961 962 # RFC 9112 §6.3 (8): no TE and no CL ⇒ the body runs until the server closes. 963 def frame.open(+h: Map<&2, List<&2, String>>) -> Bool: 964 Bool.not(Bool.or(has_header(h, "transfer-encoding"), has_header(h, "content-length"))) 965 966 # RFC 9112 §6.3 (1): HEAD, 1xx, 204 and 304 responses end at the blank line. 967 def frame.nobody(+status: U32, head: Bool) -> Bool: 968 Bool.or(head, Bool.or(U32.is_lt(status, 200), Bool.or(U32.is_eq(status, 204), U32.is_eq(status, 304)))) 969 970 # 101 is the final response. Other 1xx are interim; a final response follows. 971 def frame.interim(+status: U32) -> Bool: 972 Bool.and(U32.is_lt(status, 200), Bool.not(U32.is_eq(status, 101))) 973 974 def frame.status(status: U32, +h: Map<&2, List<&2, String>>, rest: String, closed: Bool, nobody: Bool) -> Frame: 975 match nobody: 976 case True{}: 977 FrameDone{Res{status, h, Bytes.new(0)}} 978 case False{}: 979 frame.done(status, h, parse.res.body(h, rest), closed, frame.open(h)) 980 981 def parse.res.fields(+status: U32, rest: String, fs: Maybe<&2, Map<&2, List<&2, String>>>, closed: Bool, head: Bool) -> Frame: 982 match fs: 983 case None{}: 984 FrameBad{} 985 case Some{h}: 986 frame.status(status, h, rest, closed, frame.nobody(status, head)) 987 988 def parse.res.num(code: Maybe<&2, U32>, headers: String, rest: String, closed: Bool, head: Bool) -> Frame: 989 match code: 990 case None{}: 991 FrameBad{} 992 case Some{n}: 993 parse.res.fields(n, rest, parse.headers.raw(headers), closed, head) 994 995 def parse.res.ver(code: String, headers: String, rest: String, closed: Bool, head: Bool, ok: Bool) -> Frame: 996 match ok: 997 case False{}: 998 FrameBad{} 999 case True{}: 1000 parse.res.num(parse_u32(code), headers, rest, closed, head) 1001 1002 def parse.res.start3(mpv: String & String & String, headers: String, rest: String, closed: Bool, head: Bool) -> Frame: 1003 (+ver, code, reason) = mpv 1004 parse.res.ver(code, headers, rest, closed, head, Bool.or(String.eq(ver, "HTTP/1.1"), String.eq(ver, "HTTP/1.0"))) 1005 1006 def parse.res.lines(lr: String & String, rest: String, closed: Bool, head: Bool) -> Frame: 1007 (line, headers) = lr 1008 parse.res.start3(start_line(line), headers, rest, closed, head) 1009 1010 def parse.res.blank(hd: String, rest: String, +closed: Bool, head: Bool, ok: Bool) -> Frame: 1011 match ok: 1012 case False{}: 1013 frame.cut(closed) 1014 case True{}: 1015 parse.res.lines(parse.first(hd, SNil{}), rest, closed, head) 1016 1017 def parse.res.of(hb: String & String, closed: Bool, head: Bool) -> Frame: 1018 (+hd, rest) = hb 1019 parse.res.blank(hd, rest, closed, head, blank_end(hd)) 1020 1021 type Lead is Data: 1022 LeadCut{} 1023 LeadFinal{} 1024 LeadSkip{+rest: String} 1025 1026 def frame.lead.if(rest: String, interim: Bool) -> Lead: 1027 match interim: 1028 case True{}: 1029 LeadSkip{rest} 1030 case False{}: 1031 LeadFinal{} 1032 1033 def frame.lead.num(n: Maybe<&2, U32>, rest: String) -> Lead: 1034 match n: 1035 case None{}: 1036 LeadFinal{} 1037 case Some{s}: 1038 frame.lead.if(rest, frame.interim(s)) 1039 1040 def frame.lead.code(mpv: String & String & String, rest: String) -> Lead: 1041 (ver, code, reason) = mpv 1042 frame.lead.num(parse_u32(code), rest) 1043 1044 def frame.lead.lines(xs: List<&2, String>, rest: String) -> Lead: 1045 match xs: 1046 case Nil{}: 1047 LeadFinal{} 1048 case Con{line, headers}: 1049 frame.lead.code(start_line(line), rest) 1050 1051 def frame.lead.blank(+hd: String, rest: String, ok: Bool) -> Lead: 1052 match ok: 1053 case False{}: 1054 LeadCut{} 1055 case True{}: 1056 frame.lead.lines(String.lines(hd), rest) 1057 1058 def frame.lead(hb: String & String) -> Lead: 1059 (+hd, rest) = hb 1060 frame.lead.blank(hd, rest, blank_end(hd)) 1061 1062 def frame.at.cont(+raw: String, lead: Lead) -> String: 1063 match lead: 1064 case LeadCut{}: 1065 raw 1066 case LeadFinal{}: 1067 raw 1068 case LeadSkip{rest}: 1069 rest 1070 1071 def frame.at.zero.go(+raw: String, closed: Bool, head: Bool, lead: Lead) -> Frame: 1072 match lead: 1073 case LeadCut{}: 1074 frame.cut(closed) 1075 case LeadFinal{}: 1076 parse.res.of(split_at_blank(raw), closed, head) 1077 case LeadSkip{rest}: 1078 FrameBad{} 1079 1080 def frame.at.zero.of(+raw: String, closed: Bool, head: Bool) -> Frame: 1081 frame.at.zero.go(raw, closed, head, frame.lead(split_at_blank(raw))) 1082 1083 def frame.at.pos.do(f: Nat, +raw: String, closed: Bool, head: Bool, hb: String & String, lead: Lead) -> Frame: 1084 match f: 1085 case 0n: 1086 match lead: 1087 case LeadCut{}: 1088 frame.cut(closed) 1089 case LeadFinal{}: 1090 parse.res.of(hb, closed, head) 1091 case LeadSkip{+rest}: 1092 frame.at.zero.of(rest, closed, head) 1093 case 1n+g: 1094 match lead: 1095 case LeadCut{}: 1096 frame.cut(closed) 1097 case LeadFinal{}: 1098 parse.res.of(hb, closed, head) 1099 case LeadSkip{+rest}: 1100 frame.at.pos.do(g, rest, closed, head, split_at_blank(rest), frame.lead(split_at_blank(rest))) 1101 1102 def frame.at.zero(+raw: String, closed: Bool, head: Bool) -> Frame: 1103 frame.at.zero.of(raw, closed, head) 1104 1105 # ponytail: 8 interim responses; raise the fuel if a server sends more 1106 def frame.at(fuel: Nat, +raw: String, closed: Bool, head: Bool) -> Frame: 1107 match fuel: 1108 case 0n: 1109 frame.at.zero(raw, closed, head) 1110 case 1n+f: 1111 frame.at.pos.do(f, raw, closed, head, split_at_blank(raw), frame.lead(split_at_blank(raw))) 1112 1113 # frame(bytes so far, server closed?, request was HEAD?) 1114 def frame(raw: String, closed: Bool, head: Bool) -> Frame: 1115 frame.at(8n, raw, closed, head) 1116 1117 def frame.res(f: Frame) -> Maybe<&1, Res>: 1118 match f: 1119 case FrameDone{res}: 1120 Some{res} 1121 case FrameBad{}: 1122 None{} 1123 case FrameMore{}: 1124 None{} 1125 1126 def parse_res(raw: String) -> Maybe<&1, Res>: 1127 frame.res(frame(raw, True{}, False{})) 1128 1129 def res_fields.res(res: Res, k: String) -> List<&2, String>: 1130 Res{status, headers, body} = res 1131 fields(headers, k) 1132 1133 def res_fields(m: Maybe<&1, Res>, k: String) -> List<&2, String>: 1134 match m: 1135 case None{}: 1136 Nil{} 1137 case Some{res}: 1138 res_fields.res(res, k) 1139 1140 # The response body as it arrives. Len: bytes left. Chunk: the decoder. Close: until the 1141 # server closes. Done: the body is whole. Bad: it cannot become a body. 1142 type Rb is Data: 1143 RbLen{left: U32} 1144 RbChunk{st: Dc} 1145 RbClose{} 1146 RbDone{} 1147 RbBad{} 1148 1149 def rb.cl(n: Maybe<&2, U32>) -> Maybe<&2, Rb>: 1150 match n: 1151 case None{}: 1152 None{} 1153 case Some{+k}: 1154 Some{Bool.pick(Rb, U32.is_zero(k), RbDone{}, RbLen{k})} 1155 1156 def rb.te(+h: Map<&2, List<&2, String>>, cl: Bool) -> Maybe<&2, Rb>: 1157 match cl: 1158 case True{}: 1159 None{} 1160 case False{}: 1161 Some{Bool.pick(Rb, String.eq(String.to_lower(header.last(h, "transfer-encoding")), "chunked"), RbChunk{dc.start()}, RbClose{})} 1162 1163 def rb.len(+h: Map<&2, List<&2, String>>, te: Bool) -> Maybe<&2, Rb>: 1164 match te: 1165 case True{}: 1166 rb.te(h, has_header(h, "content-length")) 1167 case False{}: 1168 Bool.pick(Maybe<&2, Rb>, has_header(h, "content-length"), rb.cl(parse_u32(header(h, "content-length"))), Some{RbClose{}}) 1169 1170 # RFC 9112 §6.3: how the body of this response ends. None: it cannot be framed. 1171 def rb.of(+h: Map<&2, List<&2, String>>, nobody: Bool) -> Maybe<&2, Rb>: 1172 match nobody: 1173 case True{}: 1174 Some{RbDone{}} 1175 case False{}: 1176 rb.len(h, has_header(h, "transfer-encoding")) 1177 1178 # rb: how the body goes on. parts: body bytes so far, newest first. rest: bytes past a Done body. 1179 type Bf is Type: 1180 Bf{rb: Rb, parts: List<&1, Bytes.Bytes>, rest: Bytes.Bytes} 1181 1182 def rb.len.cut(parts: List<&1, Bytes.Bytes>, +len: U32, +left: U32, r: Bytes.Bytes & Bytes.Bytes) -> Bf: 1183 (piece, data) = r 1184 Bf{RbDone{}, Con{data, parts}, bytes.snd(Bytes.slice(piece, left, (len - left : U32)))} 1185 1186 def rb.len.have(short: Bool, +left: U32, parts: List<&1, Bytes.Bytes>, +len: U32, piece: Bytes.Bytes) -> Bf: 1187 match short: 1188 case True{}: 1189 Bf{RbLen{(left - len : U32)}, Con{piece, parts}, Bytes.new(0)} 1190 case False{}: 1191 rb.len.cut(parts, len, left, Bytes.slice(piece, 0, left)) 1192 1193 def rb.chunk(c: Ck) -> Bf: 1194 Ck{st, parts, rest} = c 1195 match st: 1196 case DcDone{}: 1197 Bf{RbDone{}, parts, rest} 1198 case DcBad{}: 1199 Bf{RbBad{}, parts, rest} 1200 case DcSize{a, b}: 1201 Bf{RbChunk{DcSize{a, b}}, parts, rest} 1202 case DcExt{x}: 1203 Bf{RbChunk{DcExt{x}}, parts, rest} 1204 case DcSizeLf{x}: 1205 Bf{RbChunk{DcSizeLf{x}}, parts, rest} 1206 case DcData{k}: 1207 Bf{RbChunk{DcData{k}}, parts, rest} 1208 case DcDataCr{}: 1209 Bf{RbChunk{DcDataCr{}}, parts, rest} 1210 case DcDataLf{}: 1211 Bf{RbChunk{DcDataLf{}}, parts, rest} 1212 case DcTr{j}: 1213 Bf{RbChunk{DcTr{j}}, parts, rest} 1214 1215 # The body bytes in piece, added to parts. 1216 def rb.feed(rb: Rb, parts: List<&1, Bytes.Bytes>, piece: Bytes.Bytes) -> Bf: 1217 match rb: 1218 case RbLen{+left}: 1219 Bytes.Bytes{+len, buf} = piece 1220 rb.len.have(U32.is_lt(len, left), left, parts, len, Bytes.Bytes{len, buf}) 1221 case RbChunk{st}: 1222 rb.chunk(ck(piece, st, parts)) 1223 case RbClose{}: 1224 Bf{RbClose{}, Con{piece, parts}, Bytes.new(0)} 1225 case RbDone{}: 1226 Bf{RbDone{}, parts, piece} 1227 case RbBad{}: 1228 Bf{RbBad{}, parts, piece} 1229 1230 # RFC 9112 §8: only a close-delimited body may end at close. 1231 def rb.eof(rb: Rb) -> Bool: 1232 match rb: 1233 case RbClose{}: 1234 True{} 1235 case RbDone{}: 1236 True{} 1237 case RbLen{n}: 1238 False{} 1239 case RbChunk{st}: 1240 False{} 1241 case RbBad{}: 1242 False{} 1243 1244 def rb.body(parts: List<&1, Bytes.Bytes>) -> Bytes.Bytes: 1245 Bytes.concat(List.reverse(&1, Bytes.Bytes, parts)) 1246 1247 # The head scan. Seek: look for the blank line from byte from; k interim heads are behind. 1248 # Wait: no blank line yet. Res: the final head, how its body ends, and the bytes after it. 1249 type Hs is Type: 1250 HsSeek{buf: Bytes.Bytes, from: U32, k: U32} 1251 HsWait{buf: Bytes.Bytes, k: U32} 1252 HsBad{} 1253 HsRes{res: Res, rb: Rb, rest: Bytes.Bytes} 1254 1255 def hs.final(+status: U32, +h: Map<&2, List<&2, String>>, rest: Bytes.Bytes, m: Maybe<&2, Rb>) -> Hs: 1256 match m: 1257 case None{}: 1258 HsBad{} 1259 case Some{rb}: 1260 HsRes{Res{status, h, Bytes.new(0)}, rb, rest} 1261 1262 def hs.skip(more: Bool, +k: U32, rest: Bytes.Bytes) -> Hs: 1263 match more: 1264 case True{}: 1265 HsSeek{rest, 0, (k + 1 : U32)} 1266 case False{}: 1267 HsBad{} 1268 1269 def hs.pick(interim: Bool, +head: Bool, +k: U32, +status: U32, +h: Map<&2, List<&2, String>>, rest: Bytes.Bytes) -> Hs: 1270 match interim: 1271 case True{}: 1272 hs.skip(U32.is_lt(k, 8), k, rest) 1273 case False{}: 1274 hs.final(status, h, rest, rb.of(h, frame.nobody(status, head))) 1275 1276 def hs.res(+head: Bool, +k: U32, rest: Bytes.Bytes, res: Res) -> Hs: 1277 Res{+status, +h, body} = res 1278 hs.pick(frame.interim(status), head, k, status, h, rest) 1279 1280 # The head alone, framed as a HEAD response so it stops at the blank line. 1281 def hs.parsed(+head: Bool, +k: U32, rest: Bytes.Bytes, f: Frame) -> Hs: 1282 match f: 1283 case FrameDone{res}: 1284 hs.res(head, k, rest, res) 1285 case FrameBad{}: 1286 HsBad{} 1287 case FrameMore{}: 1288 HsBad{} 1289 1290 def hs.split(+head: Bool, +k: U32, +hlen: U32, +len: U32, r: Bytes.Bytes & Bytes.Bytes) -> Hs: 1291 (buf, hb) = r 1292 hs.parsed(head, k, bytes.snd(Bytes.slice(buf, hlen, (len - hlen : U32))), parse.res.of(split_at_blank(Bytes.to_string(hb)), False{}, True{})) 1293 1294 def hs.hit(+head: Bool, +k: U32, +len: U32, buf: Bytes.Bytes, at: Maybe<&2, U32>) -> Hs: 1295 match at: 1296 case None{}: 1297 HsWait{buf, k} 1298 case Some{+i}: 1299 +hlen = (i + 4 : U32) 1300 hs.split(head, k, hlen, len, Bytes.slice(buf, 0, hlen)) 1301 1302 def hs.seek.of(+head: Bool, +k: U32, +len: U32, +from: U32, buf: Bytes.Bytes, r: Bytes.Bytes & Maybe<&2, U32>) -> Hs: 1303 (tail, m) = r 1304 hs.hit(head, k, len, buf, found.at(from, m)) 1305 1306 def hs.seek(+head: Bool, +k: U32, +len: U32, +from: U32, r: Bytes.Bytes & Bytes.Bytes) -> Hs: 1307 (buf, tail) = r 1308 hs.seek.of(head, k, len, from, buf, Bytes.find(tail, "\r\n\r\n")) 1309 1310 def hs.step(+head: Bool, st: Hs) -> Hs: 1311 match st: 1312 case HsSeek{buf, +from, +k}: 1313 Bytes.Bytes{+len, b} = buf 1314 hs.seek(head, k, len, from, Bytes.slice(Bytes.Bytes{len, b}, from, (len - from : U32))) 1315 case HsWait{buf, k}: 1316 HsWait{buf, k} 1317 case HsBad{}: 1318 HsBad{} 1319 case HsRes{res, rb, rest}: 1320 HsRes{res, rb, rest} 1321 1322 # Each step ends the scan or passes one interim head, and at most 8 interim heads pass. 1323 def hs.scan(fuel: Nat, +head: Bool, st: Hs) -> Hs: 1324 match fuel: 1325 case 0n: 1326 match st: 1327 case HsSeek{buf, from, k}: 1328 HsBad{} 1329 case HsWait{buf, k}: 1330 HsWait{buf, k} 1331 case HsBad{}: 1332 HsBad{} 1333 case HsRes{res, rb, rest}: 1334 HsRes{res, rb, rest} 1335 case 1n+f: 1336 hs.scan(f, head, hs.step(head, st)) 1337 1338 # piece joins buf; only the new bytes, and the 3 before them, can finish the blank line. 1339 def hs.more(+head: Bool, +k: U32, buf: Bytes.Bytes, piece: Bytes.Bytes) -> Hs: 1340 Bytes.Bytes{+ol, ob} = buf 1341 hs.scan(10n, head, HsSeek{Bytes.append(Bytes.Bytes{ol, ob}, piece), Bool.pick(U32, U32.is_lt(ol, 3), 0, (ol - 3 : U32)), k}) 1342 1343 # Untrusted servers must not grow the buffer without bound. 1344 def fetch.max() -> Nat: 1345 U32.to_nat(16777216) 1346 1347 # A response read in pieces. Head: the head is not whole yet. Body: it is, and parts is the body so far. 1348 # Len: a Content-Length body in one buffer of that size, with left bytes still to come. 1349 type Rv is Type: 1350 RvHead{buf: Bytes.Bytes, k: U32} 1351 RvBody{res: Res, rb: Rb, parts: List<&1, Bytes.Bytes>} 1352 RvLen{res: Res, left: U32, body: Bytes.Bytes} 1353 1354 # More: read on. Done: the response, and the bytes after it when the body is self-delimited. 1355 type Rt is Type: 1356 RtMore{rv: Rv} 1357 RtBad{} 1358 RtDone{res: Res, rest: Maybe<&1, Bytes.Bytes>} 1359 1360 def rv.start() -> Rv: 1361 RvHead{Bytes.new(0), 0} 1362 1363 def rv.whole(res: Res, b: Bytes.Bytes, rest: Maybe<&1, Bytes.Bytes>) -> Rt: 1364 Res{status, headers, body} = res 1365 RtDone{Res{status, headers, b}, rest} 1366 1367 def rv.done(res: Res, parts: List<&1, Bytes.Bytes>, rest: Maybe<&1, Bytes.Bytes>) -> Rt: 1368 rv.whole(res, rb.body(parts), rest) 1369 1370 def rl.put.of(+total: U32, +len: U32, r: Array & Array) -> Bytes.Bytes & Bytes.Bytes: 1371 (src, dst) = r 1372 (Bytes.Bytes{total, dst}, Bytes.Bytes{len, src}) 1373 1374 # The first m bytes of piece, written into body after the total - left bytes already there. 1375 def rl.put(+m: U32, +left: U32, body: Bytes.Bytes, piece: Bytes.Bytes) -> Bytes.Bytes & Bytes.Bytes: 1376 Bytes.Bytes{+total, dst} = body 1377 Bytes.Bytes{+len, src} = piece 1378 rl.put.of(total, len, Bytes.copy(m, src, dst, 0, (total - left : U32))) 1379 1380 def rl.fed(short: Bool, res: Res, +left: U32, +len: U32, r: Bytes.Bytes & Bytes.Bytes) -> Rt: 1381 match short: 1382 case True{}: 1383 (body, piece) = r 1384 RtMore{RvLen{res, (left - len : U32), body}} 1385 case False{}: 1386 (body, piece) = r 1387 rv.whole(res, body, Some{bytes.snd(Bytes.slice(piece, left, (len - left : U32)))}) 1388 1389 def rl.feed(res: Res, +left: U32, body: Bytes.Bytes, piece: Bytes.Bytes) -> Rt: 1390 Bytes.Bytes{+len, buf} = piece 1391 rl.fed(U32.is_lt(len, left), res, left, len, rl.put(U32.min(len, left), left, body, Bytes.Bytes{len, buf})) 1392 1393 def rv.fed(res: Res, bf: Bf) -> Rt: 1394 Bf{rb, parts, rest} = bf 1395 match rb: 1396 case RbDone{}: 1397 rv.done(res, parts, Some{rest}) 1398 case RbBad{}: 1399 RtBad{} 1400 case RbLen{n}: 1401 RtMore{RvBody{res, RbLen{n}, parts}} 1402 case RbChunk{st}: 1403 RtMore{RvBody{res, RbChunk{st}, parts}} 1404 case RbClose{}: 1405 RtMore{RvBody{res, RbClose{}, parts}} 1406 1407 # Some{left}: a Content-Length body small enough to allocate up front. Larger ones fail at the fetch cap. 1408 def rb.buf(rb: Rb) -> Maybe<&2, U32>: 1409 match rb: 1410 case RbLen{+left}: 1411 Bool.pick(Maybe<&2, U32>, Nat.is_lt(fetch.max(), U32.to_nat(left)), None{}, Some{left}) 1412 case RbChunk{st}: 1413 None{} 1414 case RbClose{}: 1415 None{} 1416 case RbDone{}: 1417 None{} 1418 case RbBad{}: 1419 None{} 1420 1421 def rv.body(m: Maybe<&2, U32>, res: Res, rb: Rb, rest: Bytes.Bytes) -> Rt: 1422 match m: 1423 case None{}: 1424 rv.fed(res, rb.feed(rb, Nil{}, rest)) 1425 case Some{+left}: 1426 rl.feed(res, left, Bytes.new(left), rest) 1427 1428 def rv.head(hs: Hs) -> Rt: 1429 match hs: 1430 case HsWait{buf, k}: 1431 RtMore{RvHead{buf, k}} 1432 case HsBad{}: 1433 RtBad{} 1434 case HsSeek{buf, from, k}: 1435 RtBad{} 1436 case HsRes{res, +rb, rest}: 1437 rv.body(rb.buf(rb), res, rb, rest) 1438 1439 def rv.eof(res: Res, parts: List<&1, Bytes.Bytes>, ok: Bool) -> Rt: 1440 match ok: 1441 case True{}: 1442 rv.done(res, parts, None{}) 1443 case False{}: 1444 RtBad{} 1445 1446 # One read: piece, or the server closed (RFC 9112 §8: a message cut short is not a message). 1447 def rv.step(closed: Bool, +head: Bool, rv: Rv, piece: Bytes.Bytes) -> Rt: 1448 match closed: 1449 case True{}: 1450 match rv: 1451 case RvHead{buf, k}: 1452 RtBad{} 1453 case RvBody{res, rb, parts}: 1454 rv.eof(res, parts, rb.eof(rb)) 1455 case RvLen{res, left, body}: 1456 RtBad{} 1457 case False{}: 1458 match rv: 1459 case RvHead{buf, +k}: 1460 rv.head(hs.more(head, k, buf, piece)) 1461 case RvBody{res, rb, parts}: 1462 rv.fed(res, rb.feed(rb, parts, piece)) 1463 case RvLen{res, +left, body}: 1464 rl.feed(res, left, body, piece) 1465 1466 def rv.end(closed: Bool, +head: Bool, t: Rt) -> Rt: 1467 match closed: 1468 case False{}: 1469 t 1470 case True{}: 1471 match t: 1472 case RtMore{rv}: 1473 rv.step(True{}, head, rv, Bytes.new(0)) 1474 case RtBad{}: 1475 RtBad{} 1476 case RtDone{res, rest}: 1477 RtDone{res, rest} 1478 1479 def rv.go(xs: List<&2, String>, closed: Bool, +head: Bool, t: Rt) -> Rt: 1480 match xs: 1481 case Nil{}: 1482 rv.end(closed, head, t) 1483 case Con{+x, rest}: 1484 match t: 1485 case RtMore{rv}: 1486 rv.go(rest, closed, head, rv.step(False{}, head, rv, Bytes.from_string(x))) 1487 case RtBad{}: 1488 RtBad{} 1489 case RtDone{res, r}: 1490 RtDone{res, r} 1491 1492 def rt.frame(t: Rt) -> Frame: 1493 match t: 1494 case RtMore{rv}: 1495 FrameMore{} 1496 case RtBad{}: 1497 FrameBad{} 1498 case RtDone{res, rest}: 1499 FrameDone{res} 1500 1501 # What the read loop makes of these pieces, then a close when closed is set. 1502 def res.frame(xs: List<&2, String>, closed: Bool, +head: Bool) -> Frame: 1503 rt.frame(rv.go(xs, closed, head, RtMore{rv.start()})) 1504 1505 def rt.rest(t: Rt) -> Maybe<&1, Bytes.Bytes>: 1506 match t: 1507 case RtMore{rv}: 1508 None{} 1509 case RtBad{}: 1510 None{} 1511 case RtDone{res, rest}: 1512 rest 1513 1514 def after.of(m: Maybe<&1, Bytes.Bytes>) -> Maybe<&2, String>: 1515 match m: 1516 case None{}: 1517 None{} 1518 case Some{b}: 1519 Some{Bytes.to_string(b)} 1520 1521 # Bytes after a complete self-delimited message. None if it is not one yet. 1522 def after(+raw: String, +head: Bool) -> Maybe<&2, String>: 1523 after.of(rt.rest(rv.go([raw], False{}, head, RtMore{rv.start()}))) 1524 1525 def req.v11.ver(mpv: String & String & String) -> Bool: 1526 (+method, path, v) = mpv 1527 String.eq(v, "HTTP/1.1") 1528 1529 def req.v11.line(+line: String) -> Bool: 1530 req.v11.ver(start_line(line)) 1531 1532 def req.v11.lines(xs: List<&2, String>) -> Bool: 1533 match xs: 1534 case Nil{}: 1535 False{} 1536 case Con{line, headers}: 1537 req.v11.line(line) 1538 1539 def req.v11.pick(+head: String, ok: Bool) -> Bool: 1540 match ok: 1541 case False{}: 1542 False{} 1543 case True{}: 1544 req.v11.lines(String.lines(head)) 1545 1546 def req.v11.of(hb: String & String) -> Bool: 1547 (+head, rest) = hb 1548 req.v11.pick(head, blank_end(head)) 1549 1550 def req.v11(+raw: String) -> Bool: 1551 req.v11.of(split_at_blank(raw)) 1552 1553 def req.conn_close(+headers: Map<&2, List<&2, String>>) -> Bool: 1554 String.eq(String.to_lower(header(headers, "connection")), "close") 1555 1556 def res.conn_close(+headers: Map<&2, List<&2, String>>) -> Bool: 1557 String.eq(String.to_lower(header(headers, "connection")), "close") 1558 1559 def serve.fail_close(+status: U32) -> Bool: 1560 Bool.or(U32.is_eq(status, 400), Bool.or(U32.is_eq(status, 413), U32.is_eq(status, 431))) 1561 1562 def serve.keep.ok(fail: Bool, res_close: Bool) -> Bool: 1563 match fail: 1564 case True{}: 1565 False{} 1566 case False{}: 1567 Bool.not(res_close) 1568 1569 def serve.keep.v11(v11: Bool, +req_close: Bool, +status: U32, res_close: Bool) -> Bool: 1570 match v11: 1571 case False{}: 1572 False{} 1573 case True{}: 1574 match req_close: 1575 case True{}: 1576 False{} 1577 case False{}: 1578 serve.keep.ok(serve.fail_close(status), res_close) 1579 1580 def serve.keep(+headers: Map<&2, List<&2, String>>, v11: Bool, +status: U32, +res: Map<&2, List<&2, String>>) -> Bool: 1581 serve.keep.v11(v11, req.conn_close(headers), status, res.conn_close(res)) 1582 1583 1584 def again.keep(close: Bool, x: Bytes.Bytes) -> Maybe<&1, Bytes.Bytes>: 1585 match close: 1586 case True{}: 1587 None{} 1588 case False{}: 1589 Some{x} 1590 1591 def again.rest(close: Bool, rest: Maybe<&1, Bytes.Bytes>) -> Maybe<&1, Bytes.Bytes>: 1592 match rest: 1593 case None{}: 1594 None{} 1595 case Some{x}: 1596 again.keep(close, x) 1597 1598 # The bytes past the response when the socket can take another request: the request did not 1599 # ask to close, the response is self-delimited, and the peer did not say close. 1600 def again(close: Bool, +h: Map<&2, List<&2, String>>, rest: Maybe<&1, Bytes.Bytes>) -> Maybe<&1, Bytes.Bytes>: 1601 match close: 1602 case True{}: 1603 None{} 1604 case False{}: 1605 again.rest(String.eq(String.to_lower(header(h, "connection")), "close"), rest) 1606 1607 def res_body.res(res: Res) -> Bytes.Bytes: 1608 Res{status, headers, body} = res 1609 body 1610 1611 def res_body(m: Maybe<&1, Res>) -> Bytes.Bytes: 1612 match m: 1613 case None{}: 1614 Bytes.new(0) 1615 case Some{res}: 1616 res_body.res(res) 1617 1618 def text(res: Res) -> String: 1619 Enc.utf8.decode(res_body.res(res)) 1620 1621 # UTF-8 is checked, not repaired: bad UTF-8 in a string gives None (RFC 8259 §8.1). 1622 def json(res: Res) -> Maybe<&1, Json.Val>: 1623 Json.parse.bytes(res_body.res(res)) 1624 1625 def got_body.req(req: Req) -> Bytes.Bytes: 1626 Req{method, path, headers, body} = req 1627 body 1628 1629 def got_body(m: Maybe<&1, Req>) -> Bytes.Bytes: 1630 match m: 1631 case None{}: 1632 Bytes.new(0) 1633 case Some{req}: 1634 got_body.req(req) 1635 1636 def body.of(hb: String & String) -> String: 1637 (h, b) = hb 1638 b 1639 1640 def body(s: String) -> String: 1641 body.of(split_at_blank.go(s, False{}, 0, SNil{})) 1642 1643 def headers_lines(+k: String, vs: List<&2, String>, acc: String) -> String: 1644 match vs: 1645 case Nil{}: 1646 acc 1647 case Con{v, t}: 1648 headers_lines(k, t, acc ++ ("\r\n" ++ sanitize(k) ++ ": " ++ sanitize(v))) 1649 1650 def headers_block.go(xs: List<&2, Sigma<&2, &2, String, _ => List<&2, String>>>, acc: String) -> String: 1651 match xs: 1652 case Nil{}: 1653 acc 1654 case (k, vs) <> t: 1655 headers_block.go(t, headers_lines(k, vs, acc)) 1656 1657 def headers_block(h: Map<&2, List<&2, String>>) -> String: 1658 headers_block.go(Map.to_list(&2, List<&2, String>, h), "") 1659 1660 # RFC 9110 §15. An unknown code gets an empty reason, which RFC 9112 §4 allows. 1661 type Reason is Data: 1662 Reason{code: U32, text: String} 1663 1664 def reasons() -> List<&2, Reason>: 1665 [Reason{100, "Continue"}, Reason{101, "Switching Protocols"}, Reason{103, "Early Hints"}, 1666 Reason{200, "OK"}, Reason{201, "Created"}, Reason{202, "Accepted"}, Reason{203, "Non-Authoritative Information"}, Reason{204, "No Content"}, Reason{205, "Reset Content"}, Reason{206, "Partial Content"}, 1667 Reason{300, "Multiple Choices"}, Reason{301, "Moved Permanently"}, Reason{302, "Found"}, Reason{303, "See Other"}, Reason{304, "Not Modified"}, Reason{307, "Temporary Redirect"}, Reason{308, "Permanent Redirect"}, 1668 Reason{400, "Bad Request"}, Reason{401, "Unauthorized"}, Reason{402, "Payment Required"}, Reason{403, "Forbidden"}, Reason{404, "Not Found"}, Reason{405, "Method Not Allowed"}, Reason{406, "Not Acceptable"}, Reason{407, "Proxy Authentication Required"}, Reason{408, "Request Timeout"}, Reason{409, "Conflict"}, Reason{410, "Gone"}, Reason{411, "Length Required"}, Reason{412, "Precondition Failed"}, Reason{413, "Content Too Large"}, Reason{414, "URI Too Long"}, Reason{415, "Unsupported Media Type"}, Reason{416, "Range Not Satisfiable"}, Reason{417, "Expectation Failed"}, Reason{421, "Misdirected Request"}, Reason{422, "Unprocessable Content"}, Reason{426, "Upgrade Required"}, Reason{428, "Precondition Required"}, Reason{429, "Too Many Requests"}, Reason{431, "Request Header Fields Too Large"}, 1669 Reason{500, "Internal Server Error"}, Reason{501, "Not Implemented"}, Reason{502, "Bad Gateway"}, Reason{503, "Service Unavailable"}, Reason{504, "Gateway Timeout"}, Reason{505, "HTTP Version Not Supported"}] 1670 1671 def reason.go(xs: List<&2, Reason>, +status: U32) -> String: 1672 match xs: 1673 case Nil{}: 1674 "" 1675 case Con{Reason{k, v}, t}: 1676 Bool.pick(String, U32.is_eq(k, status), v, reason.go(t, status)) 1677 1678 def reason(+status: U32) -> String: 1679 reason.go(reasons(), status) 1680 1681 def response(+status: U32, headers: Map<&2, List<&2, String>>, body: String) -> String: 1682 ("HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ headers_block(headers)) ++ ("\r\n\r\n" ++ body) 1683 1684 def encode.body(head: Bool, top: Bytes.Bytes, body: Bytes.Bytes) -> Bytes.Bytes: 1685 match head: 1686 case True{}: 1687 top 1688 case False{}: 1689 Bytes.append(top, body) 1690 1691 def encode.len(+status: U32, headers: Map<&2, List<&2, String>>, head: Bool, body: Bytes.Bytes) -> Bytes.Bytes: 1692 Bytes.Bytes{+len, buf} = body 1693 encode.body(head, Bytes.from_string(response(status, set(headers, "content-length", U32.show(len)), "")), Bytes.Bytes{len, buf}) 1694 1695 # RFC 9110 §6.4.1, §8.6: 1xx, 204 and 304 have no body; HEAD gets the length but no body. 1696 def encode.pick(+status: U32, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes, head: Bool, nobody: Bool) -> Bytes.Bytes: 1697 match nobody: 1698 case True{}: 1699 Bytes.from_string(response(status, headers, "")) 1700 case False{}: 1701 encode.len(status, headers, head, body) 1702 1703 def encode.on(r: Res, head: Bool) -> Bytes.Bytes: 1704 Res{+status, headers, body} = r 1705 encode.pick(status, headers, body, head, frame.nobody(status, False{})) 1706 1707 def encode(r: Res) -> Bytes.Bytes: 1708 encode.on(r, False{}) 1709 1710 def request(method: String, path: String, headers: Map<&2, List<&2, String>>, body: String) -> String: 1711 (method ++ " " ++ path ++ " HTTP/1.1" ++ headers_block(headers)) ++ ("\r\n\r\n" ++ body) 1712 1713 def req.reserved(+k: String) -> Bool: 1714 Bool.or(Bool.or(String.eq(k, "host"), String.eq(k, "connection")), Bool.or(String.eq(k, "content-length"), String.eq(k, "transfer-encoding"))) 1715 1716 def req.put.one(m: Map<&2, List<&2, String>>, +k: String, vs: List<&2, String>, skip: Bool) -> Map<&2, List<&2, String>>: 1717 match skip: 1718 case True{}: 1719 m 1720 case False{}: 1721 Map.set(&2, List<&2, String>, m, k, vs) 1722 1723 def req.put(xs: List<&2, Sigma<&2, &2, String, _ => List<&2, String>>>, m: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>: 1724 match xs: 1725 case Nil{}: 1726 m 1727 case (k, vs) <> t: 1728 +lk = String.to_lower(k) 1729 req.put(t, req.put.one(m, lk, vs, req.reserved(lk))) 1730 1731 # RFC 9110 §8.6: no Content-Length on a bodiless request whose method gives a body no meaning. 1732 def req.cl(+m: Map<&2, List<&2, String>>, +method: String, +len: U32) -> Map<&2, List<&2, String>>: 1733 +bodied = Bool.or(Bool.not(U32.is_zero(len)), Bool.or(String.eq(method, "POST"), Bool.or(String.eq(method, "PUT"), String.eq(method, "PATCH")))) 1734 Bool.pick(Map<&2, List<&2, String>>, bodied, set(m, "content-length", U32.show(len)), m) 1735 1736 # Caller headers are lowercased so they cannot duplicate ours; host, connection, 1737 # content-length and transfer-encoding are always ours (no smuggling through them). 1738 def encode_req.word(close: Bool) -> String: 1739 match close: 1740 case True{}: 1741 "close" 1742 case False{}: 1743 "keep-alive" 1744 1745 def encode_req.on(+method: String, target: String, host: String, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes, close: Bool) -> Bytes.Bytes: 1746 Bytes.Bytes{+len, buf} = body 1747 +h = set(set(req.put(Map.to_list(&2, List<&2, String>, headers), empty()), "host", host), "connection", encode_req.word(close)) 1748 Bytes.append(Bytes.from_string(request(method, target, req.cl(h, method, len), "")), Bytes.Bytes{len, buf}) 1749 1750 def encode_req(+method: String, target: String, host: String, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes) -> Bytes.Bytes: 1751 encode_req.on(method, target, host, headers, body, True{}) 1752 1753 # Redirects (RFC 9110 §15.4, as WHATWG fetch applies them) 1754 # -------------------------------------------------------- 1755 1756 # One request to make: headers are already lowercased (see req.put). 1757 type Hop is Type: 1758 Hop{method: String, url: Url.Abs, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes} 1759 1760 def redirect.code(+s: U32) -> Bool: 1761 Bool.or(Bool.or(U32.is_eq(s, 301), U32.is_eq(s, 302)), Bool.or(U32.is_eq(s, 303), Bool.or(U32.is_eq(s, 307), U32.is_eq(s, 308)))) 1762 1763 def redirect.origin(a: Url.Abs) -> String: 1764 Url.Abs{scheme, host, port, target} = a 1765 scheme ++ "://" ++ host ++ ":" ++ U32.show(port) 1766 1767 def redirect.del(ks: List<&2, String>, m: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>: 1768 match ks: 1769 case Nil{}: 1770 m 1771 case Con{k, t}: 1772 redirect.del(t, Map.del(&2, List<&2, String>, m, k)) 1773 1774 def redirect.hdrs(+h: Map<&2, List<&2, String>>, drop: Bool, cross: Bool) -> Map<&2, List<&2, String>>: 1775 +h2 = Bool.pick(Map<&2, List<&2, String>>, drop, redirect.del(["content-type", "content-encoding", "content-language", "content-location"], h), h) 1776 Bool.pick(Map<&2, List<&2, String>>, cross, redirect.del(["authorization", "cookie", "proxy-authorization"], h2), h2) 1777 1778 # 303 turns any method but HEAD into GET; 301 and 302 turn POST into GET; 1779 # 307 and 308 keep method and body. A GET carries no body or body headers. 1780 # Credentials do not follow a hop to another origin. 1781 def redirect.body(drop: Bool, body: Bytes.Bytes) -> Bytes.Bytes: 1782 match drop: 1783 case True{}: 1784 Bytes.new(0) 1785 case False{}: 1786 body 1787 1788 def redirect.to(+status: U32, +method: String, +from: Url.Abs, headers: Map<&2, List<&2, String>>, body: Bytes.Bytes, next: Maybe<&2, Url.Abs>) -> Maybe<&1, Hop>: 1789 match next: 1790 case None{}: 1791 None{} 1792 case Some{+to}: 1793 +drop = Bool.or(Bool.and(U32.is_eq(status, 303), Bool.not(String.eq(method, "HEAD"))), Bool.and(Bool.or(U32.is_eq(status, 301), U32.is_eq(status, 302)), String.eq(method, "POST"))) 1794 Some{Hop{Bool.pick(String, drop, "GET", method), to, redirect.hdrs(headers, drop, Bool.not(String.eq(redirect.origin(from), redirect.origin(to)))), redirect.body(drop, body)}} 1795 1796 def redirect.hop(status: U32, h: Hop, loc: String) -> Maybe<&1, Hop>: 1797 Hop{method, +url, headers, body} = h 1798 redirect.to(status, method, url, headers, body, Url.resolve(url, loc)) 1799 1800 def redirect.if(status: U32, h: Hop, loc: String, ok: Bool) -> Maybe<&1, Hop>: 1801 match ok: 1802 case False{}: 1803 None{} 1804 case True{}: 1805 redirect.hop(status, h, loc) 1806 1807 # The next request after a response, or None when it is not a redirect we follow. 1808 def redirect(+status: U32, +headers: Map<&2, List<&2, String>>, h: Hop) -> Maybe<&1, Hop>: 1809 redirect.if(status, h, header(headers, "location"), Bool.and(redirect.code(status), has_header(headers, "location"))) 1810 1811 # Cookies (RFC 6265bis §5) 1812 # ------------------------ 1813 1814 # A stored cookie. domain is lowercase, with no leading dot; host_only sends it to that host alone. 1815 # expiry None is a session cookie. http_only and same_site ("strict", "lax", "none" or "default") 1816 # are kept, not enforced: fetch has no browsing client, so its requests are same-site (§5.2). 1817 type Cookie is Data: 1818 Cookie{name: String, value: String, domain: String, host_only: Bool, path: String, secure: Bool, http_only: Bool, same_site: String, expiry: Maybe<&2, Time.Instant>} 1819 1820 # The cookies, oldest first, and the caller's clock. No cookie in it has expired by now. 1821 type Jar is Data: 1822 Jar{now: Time.Instant, cookies: List<&2, Cookie>} 1823 1824 def jar.new(now: Time.Instant) -> Jar: 1825 Jar{now, Nil{}} 1826 1827 def jar.cookies(j: Jar) -> List<&2, Cookie>: 1828 Jar{now, cs} = j 1829 cs 1830 1831 def cookie.live.at(+now: Time.Instant, e: Maybe<&2, Time.Instant>) -> Bool: 1832 match e: 1833 case None{}: 1834 True{} 1835 case Some{t}: 1836 Cmp.is_lt(Time.Instant.cmp(now, t)) 1837 1838 # Alive at now: no expiry, or an expiry after now. 1839 def cookie.live(+now: Time.Instant, c: Cookie) -> Bool: 1840 Cookie{name, value, domain, host_only, path, secure, http_only, same_site, expiry} = c 1841 cookie.live.at(now, expiry) 1842 1843 def jar.live(cs: List<&2, Cookie>, +now: Time.Instant) -> List<&2, Cookie>: 1844 match cs: 1845 case Nil{}: 1846 Nil{} 1847 case Con{+c, t}: 1848 List.filter.put(Cookie, c, jar.live(t, now), cookie.live(now, c)) 1849 1850 # The jar at a later time: cookies expired by now are evicted (§5.7). 1851 def jar.at(j: Jar, +now: Time.Instant) -> Jar: 1852 Jar{old, cs} = j 1853 Jar{now, jar.live(cs, now)} 1854 1855 def cookie.cut.found(r: String & String) -> String & String & Bool: 1856 (a, b) = r 1857 (a, b, True{}) 1858 1859 # hit: the last char pushed onto acc was c. 1860 def cookie.cut.go(s: String, +c: U32, acc: String, hit: Bool) -> String & String & Bool: 1861 match s: 1862 case SNil{}: 1863 match hit: 1864 case True{}: 1865 cookie.cut.found(take.cut(acc, SNil{})) 1866 case False{}: 1867 (String.reverse(acc), SNil{}, False{}) 1868 case SCon{Chr{+x}, t}: 1869 match hit: 1870 case True{}: 1871 cookie.cut.found(take.cut(acc, SCon{Chr{x}, t})) 1872 case False{}: 1873 cookie.cut.go(t, c, SCon{Chr{x}, acc}, U32.is_eq(x, c)) 1874 1875 # s split at its first c: before, after, and whether c was there. 1876 def cookie.cut(s: String, +c: U32) -> String & String & Bool: 1877 cookie.cut.go(s, c, SNil{}, False{}) 1878 1879 def cookie.fst(r: String & String & Bool) -> String: 1880 (a, b, found) = r 1881 a 1882 1883 # The path of an origin-form target: the part before "?". 1884 def cookie.target_path(t: String) -> String: 1885 cookie.fst(cookie.cut(t, 63)) 1886 1887 # Longer than n octets. 1888 def cookie.over(s: String, +n: U32) -> Bool: 1889 match s: 1890 case SNil{}: 1891 False{} 1892 case SCon{c, t}: 1893 Bool.or(U32.is_eq(n, 0), cookie.over(t, (n - 1 : U32))) 1894 1895 # %x00-08 / %x0A-1F / %x7F: control characters other than HTAB. 1896 def cookie.ctl(s: String) -> Bool: 1897 match s: 1898 case SNil{}: 1899 False{} 1900 case SCon{Chr{+c}, t}: 1901 Bool.or(Bool.or(U32.is_lt(c, 9), Bool.and(U32.is_gt(c, 9), U32.is_lt(c, 32))), Bool.or(U32.is_eq(c, 127), cookie.ctl(t))) 1902 1903 def cookie.ascii(s: String) -> Bool: 1904 match s: 1905 case SNil{}: 1906 True{} 1907 case SCon{Chr{+c}, t}: 1908 Bool.and(U32.is_lt(c, 128), cookie.ascii(t)) 1909 1910 def cookie.ip4(s: String) -> Bool: 1911 match s: 1912 case SNil{}: 1913 True{} 1914 case SCon{Chr{+c}, t}: 1915 Bool.and(Bool.or(parse_u32.digit(c), U32.is_eq(c, 46)), cookie.ip4(t)) 1916 1917 # An IP literal rather than a host name: IPv6 has a colon, IPv4 is digits and dots. 1918 def cookie.ip(+h: String) -> Bool: 1919 Bool.or(String.contains(h, ":"), cookie.ip4(h)) 1920 1921 # §5.1.3: s is d, or a host name under d on a label boundary. 1922 def cookie.domain_match(+s: String, +d: String) -> Bool: 1923 Bool.or(String.eq(s, d), Bool.and(String.ends_with(s, "." ++ d), Bool.not(cookie.ip(s)))) 1924 1925 # A reversed path with its last segment and slash dropped. 1926 def cookie.up(rs: String) -> String: 1927 match rs: 1928 case SNil{}: 1929 SNil{} 1930 case SCon{Chr{47}, t}: 1931 t 1932 case SCon{Chr{c}, t}: 1933 cookie.up(t) 1934 1935 # §5.1.4: the request path up to its last "/", or "/". 1936 def cookie.default_path(+p: String) -> String: 1937 +up = String.reverse(cookie.up(String.reverse(p))) 1938 Bool.pick(String, Bool.or(Bool.not(String.starts_with(p, "/")), String.is_empty(up)), "/", up) 1939 1940 # §5.1.4: request path p path-matches cookie path c. 1941 def cookie.path_match(+p: String, +c: String) -> Bool: 1942 Bool.or(String.eq(p, c), Bool.and(String.starts_with(p, c), Bool.or(String.ends_with(c, "/"), String.starts_with(String.drop(p, String.length(c)), "/")))) 1943 1944 type Hms is Data: 1945 Hms{h: U32, m: U32, s: U32} 1946 1947 # §5.1.1 cookie-date: the first time, day, month and year tokens found. 1948 type Cdate is Data: 1949 Cdate{time: Maybe<&2, Hms>, day: Maybe<&2, U32>, month: Maybe<&2, U32>, year: Maybe<&2, U32>} 1950 1951 def cookie.date.delim(+c: U32) -> Bool: 1952 Bool.or(U32.is_eq(c, 9), Bool.or(Bool.and(U32.is_ge(c, 32), U32.is_le(c, 47)), Bool.or(Bool.and(U32.is_ge(c, 59), U32.is_le(c, 64)), Bool.or(Bool.and(U32.is_ge(c, 91), U32.is_le(c, 96)), Bool.and(U32.is_ge(c, 123), U32.is_le(c, 126)))))) 1953 1954 def cookie.date.push(acc: String, ts: List<&2, String>) -> List<&2, String>: 1955 match acc: 1956 case SNil{}: 1957 ts 1958 case SCon{h, t}: 1959 Con{String.reverse(SCon{h, t}), ts} 1960 1961 def cookie.date.step(d: Bool, +c: U32, acc: String, ts: List<&2, String>) -> String & List<&2, String>: 1962 match d: 1963 case True{}: 1964 (SNil{}, cookie.date.push(acc, ts)) 1965 case False{}: 1966 (SCon{Chr{c}, acc}, ts) 1967 1968 # The date-tokens, last first. st: the token so far (reversed) and the tokens before it. 1969 def cookie.date.tokens(s: String, st: String & List<&2, String>) -> List<&2, String>: 1970 match s: 1971 case SNil{}: 1972 (acc, ts) = st 1973 cookie.date.push(acc, ts) 1974 case SCon{Chr{+c}, t}: 1975 (acc, ts) = st 1976 cookie.date.tokens(t, cookie.date.step(cookie.date.delim(c), c, acc, ts)) 1977 1978 def cookie.date.head(+s: String) -> Bool: 1979 match s: 1980 case SNil{}: 1981 False{} 1982 case SCon{Chr{+c}, t}: 1983 parse_u32.digit(c) 1984 1985 # The digits at the start of s: (count, value, rest). d: s starts with a digit. 1986 def cookie.date.run(s: String, d: Bool, +n: U32, +acc: U32) -> U32 & U32 & String: 1987 match s: 1988 case SNil{}: 1989 (n, acc, SNil{}) 1990 case SCon{Chr{+c}, +t}: 1991 match d: 1992 case True{}: 1993 cookie.date.run(t, cookie.date.head(t), (n + 1 : U32), (acc * 10 + (c - 48 : U32) : U32)) 1994 case False{}: 1995 (n, acc, SCon{Chr{c}, t}) 1996 1997 def cookie.date.within(r: U32 & U32 & String, +lo: U32, +hi: U32) -> Maybe<&1, U32 & String>: 1998 (+n, v, rest) = r 1999 Bool.pick(Maybe<&1, U32 & String>, Bool.and(U32.is_ge(n, lo), U32.is_le(n, hi)), Some{(v, rest)}, None{}) 2000 Lines 1-2000 of 6701. More: https://jend.leeeet.dev/read.txt?path=bend-kit-http@0.31.1.0/http/http.bend&start_line=2001