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