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