1 # ezhttp/src/http: HTTP/1.1 message text for client and server — request and 2 # response formatting/parsing — with no sockets. Semantics follow RFC 9110; 3 # message syntax and framing follow RFC 9112. TLS is outside this module. 4 import Base 5 import ./body.bend as Body 6 7 # a string divided at the first occurrence of a pattern, or not divided at all 8 type Cut is Data: 9 NoCut{} 10 Cut{before: String, after: String} 11 12 # one header field name and value (RFC 9110 §5 / RFC 9112 §5) 13 type Header is Data: 14 H{name: String, value: String} 15 16 # what a response turned out to be: status, headers, framed body — or why not 17 type Reply is Data: 18 Torn{why: String} 19 Reply{status: U32, headers: List<&2, Header>, body: String} 20 21 # the first of a list of pieces, "" when there are none 22 def first(ps: List<&2, String>) -> String: 23 match ps: 24 case []: 25 "" 26 case h <> t: 27 h 28 29 # the second of a list of pieces, "" when there is no second 30 def second(ps: List<&2, String>) -> String: 31 match ps: 32 case []: 33 "" 34 case h <> t: 35 first(t) 36 37 # one char of the divide, put back on the front of what the rest found 38 def divide.push(h: Char, r: Cut) -> Cut: 39 match r: 40 case NoCut{}: 41 NoCut{} 42 case Cut{before, after}: 43 Cut{SCon{h, before}, after} 44 45 # one step of the walk; rest is a thunk so Bool.pick does not force recursion 46 def divide.step(here: Bool, h: Char, t: String, n: Nat, 47 rest: Unit -> Cut) -> Cut: 48 match here: 49 case True{}: 50 Cut{"", String.drop(SCon{h, t}, n)} 51 case False{}: 52 divide.push(h, rest(Unit{})) 53 54 # the walk: stop at the first hit 55 def divide.go(s: String, +pat: String, +n: Nat) -> Cut: 56 match s: 57 case SNil{}: 58 NoCut{} 59 case SCon{+h, +t}: 60 divide.step(String.starts_with(SCon{h, t}, pat), h, t, n, 61 _u => divide.go(t, pat, n)) 62 63 # a string in two halves around the first occurrence of a pattern 64 def divide(s: String, +pat: String) -> Cut: 65 divide.go(s, pat, String.length(pat)) 66 67 # the value of one hex digit's code point, or 16 when it is not one 68 def hex.of(+u: U32) -> U32: 69 Bool.pick(U32, Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)), U32.sub(u, 48), 70 Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), U32.sub(u, 87), 71 Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), U32.sub(u, 55), 72 16))) 73 74 # the value of one hex digit, or 16 when the char is not one 75 def hex.val(c: Char) -> U32: 76 hex.of(Char.to_u32(c)) 77 78 # one step of the hex read 79 def hex.step(done: Bool, acc: Nat, rest: Unit -> Nat) -> Nat: 80 match done: 81 case True{}: 82 acc 83 case False{}: 84 rest(Unit{}) 85 86 # the hex digits at the front of a string, read until one is not a digit 87 def hex.go(s: String, +acc: Nat) -> Nat: 88 match s: 89 case SNil{}: 90 acc 91 case SCon{+h, t}: 92 +v = hex.val(h) 93 hex.step(U32.is_ge(v, 16), acc, 94 _u => hex.go(t, Nat.add(Nat.mul(acc, 16n), U32.to_nat(v)))) 95 96 # chunk-size from a chunk header (RFC 9112 §7.1); extensions after `;` ignored 97 def hexlen(s: String) -> Nat: 98 hex.go(String.trim(first(String.split(s, ';'))), 0n) 99 100 # whether a character is a hex digit 101 def hex.digit(c: Char) -> Bool: 102 U32.is_lt(hex.val(c), 16) 103 104 # the rest of a hex token once the first digit is known 105 def hex.rest(token: String, good: Bool) -> Bool: 106 match token good: 107 case _ False{}: 108 False{} 109 case SNil{} True{}: 110 True{} 111 case SCon{h, t} True{}: 112 hex.rest(t, hex.digit(h)) 113 114 # a chunk-size token is one or more hex digits (RFC 9112 §7.1) 115 def hex.well(token: String) -> Bool: 116 match token: 117 case SNil{}: 118 False{} 119 case SCon{h, t}: 120 hex.rest(t, hex.digit(h)) 121 122 # the chunk-size token, extensions after `;` removed 123 def chunks.token(before: String) -> String: 124 String.trim(first(String.split(before, ';'))) 125 126 # octets one code point takes in UTF-8, for a text written onto the wire 127 # (RFC 3629 §3). Content-Length counts octets (RFC 9110 §8.6); a Bend String 128 # counts code points. 129 def utf8.width(+u: U32) -> Nat: 130 Bool.pick(Nat, U32.is_lt(u, 128), 1n, 131 Bool.pick(Nat, U32.is_lt(u, 2048), 2n, 132 Bool.pick(Nat, U32.is_lt(u, 65536), 3n, 4n))) 133 134 # how many octets of UTF-8 a text is 135 def utf8.len(s: String) -> Nat: 136 match s: 137 case SNil{}: 138 0n 139 case SCon{h, t}: 140 Nat.add(utf8.width(Char.to_u32(h)), utf8.len(t)) 141 142 # What is received is octets, one character each (SPEC.md HTTP-BODY-1): the 143 # octs.* walks below count a character as one octet. 144 145 # octets one received character stands for: one 146 def octs.width(+_u: U32) -> Nat: 147 1n 148 149 # how many octets a received text is 150 def octs.len(s: String) -> Nat: 151 match s: 152 case SNil{}: 153 0n 154 case SCon{h, t}: 155 Nat.add(octs.width(Char.to_u32(h)), octs.len(t)) 156 157 # one step of the take 158 def octs.take.step(short: Bool, h: Char, rest: Unit -> String) -> String: 159 match short: 160 case True{}: 161 SNil{} 162 case False{}: 163 SCon{h, rest(Unit{})} 164 165 # the front of a string that is this many bytes of UTF-8 166 def octs.take(s: String, +n: Nat) -> String: 167 match s: 168 case SNil{}: 169 SNil{} 170 case SCon{+h, t}: 171 +w = octs.width(Char.to_u32(h)) 172 octs.take.step(Nat.is_lt(n, w), h, _u => octs.take(t, Nat.sub(n, w))) 173 174 # one step of the drop 175 def octs.drop.step(short: Bool, h: Char, t: String, 176 rest: Unit -> String) -> String: 177 match short: 178 case True{}: 179 SCon{h, t} 180 case False{}: 181 rest(Unit{}) 182 183 # the rest of a string after this many bytes of UTF-8 184 def octs.drop(s: String, +n: Nat) -> String: 185 match s: 186 case SNil{}: 187 SNil{} 188 case SCon{+h, +t}: 189 +w = octs.width(Char.to_u32(h)) 190 octs.drop.step(Nat.is_lt(n, w), h, t, _u => octs.drop(t, Nat.sub(n, w))) 191 192 # a decimal digit's value, or 10 when the character is not a digit 193 def digits.val.of(+u: U32) -> U32: 194 Bool.pick(U32, Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)), U32.sub(u, 48), 10) 195 196 # a decimal digit's value, or 10 when the character is not a digit 197 def digits.val(c: Char) -> U32: 198 digits.val.of(Char.to_u32(c)) 199 200 # accept one digit and keep reading, or stop when it is not a digit 201 def digits.step(ok: Bool, next: Unit -> Maybe<&2, Nat>) -> Maybe<&2, Nat>: 202 match ok: 203 case False{}: 204 None{} 205 case True{}: 206 next(Unit{}) 207 208 # read a decimal token without building Base's Nat.read ceiling 209 def digits.go(s: String, +acc: Nat, left: Nat) -> Maybe<&2, Nat>: 210 match s left: 211 case _ 0n: 212 None{} 213 case SNil{} 1n+k: 214 Some{acc} 215 case SCon{+h, t} 1n+k: 216 +d = digits.val(h) 217 digits.step(U32.is_lt(d, 10), 218 _u => digits.go(t, Nat.add(Nat.mul(acc, 10n), U32.to_nat(d)), k)) 219 220 # a short decimal (Content-Length, max-age). Empty is none. At most 9 digits. 221 def digits.read(s: String) -> Maybe<&2, Nat>: 222 match s: 223 case SNil{}: 224 None{} 225 case SCon{h, t}: 226 digits.go(SCon{h, t}, 0n, 9n) 227 228 # one step of the count: a character wider than what is still asked for 229 # means the string is longer 230 def octs.has.step(short: Bool, rest: Unit -> Bool) -> Bool: 231 match short: 232 case True{}: 233 True{} 234 case False{}: 235 rest(Unit{}) 236 237 # whether a string is at least this many bytes of UTF-8, walking no further 238 # than that many (octs.len walks all of it) 239 def octs.has(s: String, +n: Nat) -> Bool: 240 match s: 241 case SNil{}: 242 Bool.not(Nat.is_lt(0n, n)) 243 case SCon{+h, t}: 244 +w = octs.width(Char.to_u32(h)) 245 octs.has.step(Nat.is_lt(n, w), _u => octs.has(t, Nat.sub(n, w))) 246 247 # a non-zero chunk has its data and the following CRLF 248 def chunks.ready(+after: String, +n: Nat) -> Bool: 249 Bool.and(octs.has(after, Nat.add(n, 2n)), 250 String.starts_with(octs.drop(after, n), "\r\n")) 251 252 # the next scan step, or why this chunk header is not usable 253 type Next is Data: 254 Stop{} 255 Go{cut: Cut} 256 Reject{why: String} 257 258 # one chunk header: reject non-hex sizes, stop on the 0 chunk, else continue 259 def chunks.next.of(zero: Bool, ready: Bool, +after: String, +n: Nat) -> Next: 260 match zero ready: 261 case True{} _: 262 Stop{} 263 case False{} False{}: 264 Reject{"the chunked body stopped short"} 265 case False{} True{}: 266 Go{divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")} 267 268 # hex check, then the length 269 def chunks.next.hex(+before: String, +after: String, ok: Bool) -> Next: 270 match ok: 271 case False{}: 272 Reject{"chunk size is not hexadecimal"} 273 case True{}: 274 +n = hexlen(before) 275 chunks.next.of(Nat.is_eq(n, 0n), chunks.ready(after, n), after, n) 276 277 # classify one chunk header (RFC 9112 §7.1) 278 def chunks.next(+before: String, +after: String) -> Next: 279 chunks.next.hex(before, after, hex.well(chunks.token(before))) 280 281 # apply one scan step 282 def chunks.scan.step(step: Next, rest: Cut -> Maybe<&2, String>) 283 -> Maybe<&2, String>: 284 match step: 285 case Stop{}: 286 None{} 287 case Reject{why}: 288 Some{why} 289 case Go{cut}: 290 rest(cut) 291 292 # None when every chunk size is hex and the 0 chunk is reached 293 def chunks.scan(fuel: Nat, c: Cut) -> Maybe<&2, String>: 294 match fuel c: 295 case 0n _: 296 Some{"the chunked body is truncated"} 297 case 1n+f NoCut{}: 298 None{} 299 case 1n+f Cut{before, +after}: 300 chunks.scan.step(chunks.next(before, after), cut => chunks.scan(f, cut)) 301 302 # a zero chunk ends the body; anything after it is a trailer (RFC 9112 §7.1) 303 def chunks.more(+n: Nat, +after: String, rest: Unit -> String, zero: Bool) 304 -> String: 305 match zero: 306 case True{}: 307 "" 308 case False{}: 309 octs.take(after, n) 310 ++ rest(Unit{}) 311 312 # a chunked body put back together (RFC 9112 §7.1) 313 def chunks(fuel: Nat, c: Cut) -> String: 314 match fuel c: 315 case 0n _: 316 "" 317 case 1n+f NoCut{}: 318 "" 319 case 1n+f Cut{before, +after}: 320 +n = hexlen(before) 321 chunks.more(n, after, 322 _u => chunks(f, divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")), 323 Nat.is_eq(n, 0n)) 324 325 # a Transfer-Encoding: chunked body as the octets it stands for 326 def dechunk(+s: String) -> String: 327 chunks(String.length(s), divide(s, "\r\n")) 328 329 # whether a head line names this header (field names are case-insensitive: 330 # RFC 9110 §5.1) 331 def head.hit(h: String, +name: String) -> Bool: 332 String.starts_with(String.to_lower(h), name ++ ":") 333 334 # this line's value when it is the header asked for 335 def head.pick(hit: Bool, h: String, name: String, rest: String) -> String: 336 match hit: 337 case True{}: 338 String.trim(String.drop(h, Nat.add(String.length(name), 1n))) 339 case False{}: 340 rest 341 342 # the value a head gave a header, "" when it gave none 343 def head.go(ls: List<&2, String>, +name: String) -> String: 344 match ls: 345 case []: 346 "" 347 case +h <> t: 348 head.pick(head.hit(h, name), h, name, head.go(t, name)) 349 350 # this line's trimmed field value 351 def head.value(h: String, name: String) -> String: 352 String.trim(String.drop(h, Nat.add(String.length(name), 1n))) 353 354 # cons a value when the line is the header asked for 355 def head.values.cons(hit: Bool, value: String, rest: Unit -> List<&2, String>) 356 -> List<&2, String>: 357 match hit: 358 case False{}: 359 rest(Unit{}) 360 case True{}: 361 value <> rest(Unit{}) 362 363 # every value a head gave a header, in order (RFC 9112 §6.3 lists Content-Length) 364 def head.values(ls: List<&2, String>, +name: String) -> List<&2, String>: 365 match ls: 366 case []: 367 [] 368 case +h <> t: 369 head.values.cons(head.hit(h, name), head.value(h, name), 370 _u => head.values(t, name)) 371 372 # a field value still holding a line break is obs-fold, which we refuse 373 def field.folded(+value: String) -> Bool: 374 Bool.or(String.contains(value, "\n"), String.contains(value, "\r")) 375 376 # a trimmed field value, or none when it still contains obs-fold 377 def field.keep(name: String, +value: String, folded: Bool) -> Maybe<&2, Header>: 378 match folded: 379 case True{}: 380 None{} 381 case False{}: 382 Some{H{name, value}} 383 384 # name and trimmed value from a cut at the first colon 385 def head.from_kept(before: String, +after: String) -> Maybe<&2, Header>: 386 +value = String.trim(after) 387 field.keep(before, value, field.folded(value)) 388 389 # one header line as a Header, or none when it has no colon 390 def head.from_cut(c: Cut) -> Maybe<&2, Header>: 391 match c: 392 case NoCut{}: 393 None{} 394 case Cut{before, after}: 395 head.from_kept(before, after) 396 397 # one header line as a Header, or none when it has no colon 398 def head.line(h: String) -> Maybe<&2, Header>: 399 head.from_cut(divide(h, ":")) 400 401 # prepend a parsed header when present 402 def head.cons(m: Maybe<&2, Header>, rest: List<&2, Header>) -> List<&2, Header>: 403 match m: 404 case None{}: 405 rest 406 case Some{hdr}: 407 hdr <> rest 408 409 # header lines after the status line as a list of fields 410 def head.list(ls: List<&2, String>) -> List<&2, Header>: 411 match ls: 412 case []: 413 [] 414 case h <> t: 415 head.cons(head.line(h), head.list(t)) 416 417 # one request header line 418 def header.line(h: Header) -> String: 419 match h: 420 case H{name, value}: 421 name ++ ": " ++ value ++ "\r\n" 422 423 # the value when this field is the one asked for 424 def headers.find.pick(value: String, hit: Bool, rest: Unit -> String) -> String: 425 match hit: 426 case True{}: 427 value 428 case False{}: 429 rest(Unit{}) 430 431 # case-insensitive field lookup (RFC 9110 §5.1) 432 def headers.find.go(hs: List<&2, Header>, +name: String) -> String: 433 match hs: 434 case []: 435 "" 436 case H{n, v} <> t: 437 headers.find.pick(v, String.eq(String.to_lower(n), name), 438 _u => headers.find.go(t, name)) 439 440 # the value of a field, "" when it is absent 441 def headers.find(hs: List<&2, Header>, +name: String) -> String: 442 headers.find.go(hs, String.to_lower(name)) 443 444 # extra headers rendered in order 445 def headers.lines(hs: List<&2, Header>) -> String: 446 match hs: 447 case []: 448 "" 449 case h <> t: 450 header.line(h) ++ headers.lines(t) 451 452 # the Content-Length field when the body is not empty (RFC 9110 §8.6) 453 def clen.field(+n: Nat, empty: Bool) -> List<&2, Header>: 454 match empty: 455 case True{}: 456 [] 457 case False{}: 458 [H{"Content-Length", Nat.show(n)}] 459 460 # Content-Length line when the body is not empty (RFC 9110 §8.6) 461 def clen.line(+n: Nat, empty: Bool) -> String: 462 headers.lines(clen.field(n, empty)) 463 464 # the fields a request names: the ones ezhttp writes a default for, the ones 465 # it always computes, and every other (SPEC.md HTTP-CLI-8) 466 type Field is Data: 467 FHost{} 468 FAgent{} 469 FAccept{} 470 FConn{} 471 FClen{} 472 FOther{} 473 474 # a field's place in the order above 475 def field.rank(f: Field) -> U32: 476 match f: 477 case FHost{}: 478 0 479 case FAgent{}: 480 1 481 case FAccept{}: 482 2 483 case FConn{}: 484 3 485 case FClen{}: 486 4 487 case FOther{}: 488 5 489 490 # whether two fields are the same one 491 def field.same(a: Field, b: Field) -> Bool: 492 U32.is_eq(field.rank(a), field.rank(b)) 493 494 # a lower-case name that is not content-length 495 def field.of.clen(hit: Bool) -> Field: 496 match hit: 497 case True{}: 498 FClen{} 499 case False{}: 500 FOther{} 501 502 # a lower-case name that is not connection 503 def field.of.conn(hit: Bool, +l: String) -> Field: 504 match hit: 505 case True{}: 506 FConn{} 507 case False{}: 508 field.of.clen(String.eq(l, "content-length")) 509 510 # a lower-case name that is not accept 511 def field.of.accept(hit: Bool, +l: String) -> Field: 512 match hit: 513 case True{}: 514 FAccept{} 515 case False{}: 516 field.of.conn(String.eq(l, "connection"), l) 517 518 # a lower-case name that is not user-agent 519 def field.of.agent(hit: Bool, +l: String) -> Field: 520 match hit: 521 case True{}: 522 FAgent{} 523 case False{}: 524 field.of.accept(String.eq(l, "accept"), l) 525 526 # a lower-case name that is not host 527 def field.of.host(hit: Bool, +l: String) -> Field: 528 match hit: 529 case True{}: 530 FHost{} 531 case False{}: 532 field.of.agent(String.eq(l, "user-agent"), l) 533 534 # the field a lower-case name names 535 def field.of(+l: String) -> Field: 536 field.of.host(String.eq(l, "host"), l) 537 538 # the field a name names, compared case-insensitively (RFC 9110 §5.1) 539 def field.name(name: String) -> Field: 540 field.of(String.to_lower(name)) 541 542 # the field a header line names 543 def header.field(h: Header) -> Field: 544 match h: 545 case H{name, value}: 546 field.name(name) 547 548 # keep a header in front of the rest, or not 549 def fields.put(keep: Bool, h: Header, rest: List<&2, Header>) 550 -> List<&2, Header>: 551 match keep: 552 case True{}: 553 h <> rest 554 case False{}: 555 rest 556 557 # the headers that name one field, in order 558 def fields.of(+f: Field, hs: List<&2, Header>) -> List<&2, Header>: 559 match hs: 560 case []: 561 [] 562 case +h <> t: 563 fields.put(field.same(f, header.field(h)), h, fields.of(f, t)) 564 565 # the caller's fields of a name, or the default when it gave none 566 def fields.or(given: List<&2, Header>, dflt: List<&2, Header>) 567 -> List<&2, Header>: 568 match given: 569 case []: 570 dflt 571 case h <> t: 572 h <> t 573 574 # a request's header fields for a body of n octets: Host, User-Agent and 575 # Accept (the caller's, or the default), Connection: close, the caller's other 576 # fields, Content-Length. 577 # A caller's Connection or Content-Length is not sent: Connection: close lets 578 # the connection end frame the body when no Content-Length / chunked framing 579 # applies (RFC 9112 §9.6 / §6.3), and the body's length is computed. 580 def request.fields.n(host: String, +hs: List<&2, Header>, +n: Nat) 581 -> List<&2, Header>: 582 List.append(&2, Header, fields.or(fields.of(FHost{}, hs), [H{"Host", host}]), 583 List.append(&2, Header, 584 fields.or(fields.of(FAgent{}, hs), [H{"User-Agent", "ezhttp"}]), 585 List.append(&2, Header, 586 fields.or(fields.of(FAccept{}, hs), [H{"Accept", "*/*"}]), 587 H{"Connection", "close"} 588 <> List.append(&2, Header, fields.of(FOther{}, hs), 589 clen.field(n, Nat.is_eq(n, 0n)))))) 590 591 # a request's header fields for a text body: the fields for its UTF-8 octets 592 def request.fields(host: String, +hs: List<&2, Header>, +body: String) 593 -> List<&2, Header>: 594 request.fields.n(host, hs, utf8.len(body)) 595 596 # a request-line and headers (RFC 9112 §3, §5). Absolute-path request-target 597 # (RFC 9112 §3.2.1); the fields are request.fields'. 598 def request(method: String, host: String, path: String, 599 headers: List<&2, Header>, +body: String) -> String: 600 method ++ " " ++ path ++ " HTTP/1.1\r\n" 601 ++ headers.lines(request.fields(host, headers, body)) 602 ++ "\r\n" 603 ++ body 604 605 # the port a scheme implies: 443 for a secure one, 80 otherwise (RFC 9110 606 # §4.2.1, §4.2.2) 607 def port.dflt(secure: Bool) -> U32: 608 match secure: 609 case True{}: 610 443 611 case False{}: 612 80 613 614 # the Host field value once it is known whether the port is the default 615 def authority.of(host: String, +port: U32, dflt: Bool) -> String: 616 match dflt: 617 case True{}: 618 host 619 case False{}: 620 host ++ ":" ++ U32.show(port) 621 622 # the Host field value: the host, and the port after a colon unless it is the 623 # scheme's default (RFC 9110 §7.2) 624 def authority(secure: Bool, host: String, +port: U32) -> String: 625 authority.of(host, port, U32.is_eq(port, port.dflt(secure))) 626 627 # the request a client sends to host and port, over TLS when secure 628 def request.at(method: String, secure: Bool, host: String, port: U32, 629 path: String, headers: List<&2, Header>, body: String) -> String: 630 request(method, authority(secure, host, port), path, headers, body) 631 632 # the request text for a body of n octets that is sent after it as its own 633 # octets: the request line, the fields for n octets, and the blank line 634 def request.head(method: String, host: String, path: String, 635 headers: List<&2, Header>, +n: Nat) -> String: 636 method ++ " " ++ path ++ " HTTP/1.1\r\n" 637 ++ headers.lines(request.fields.n(host, headers, n)) 638 ++ "\r\n" 639 640 # the request text a client sends to host and port ahead of n octets 641 def request.head.at(method: String, secure: Bool, host: String, port: U32, 642 path: String, headers: List<&2, Header>, n: Nat) -> String: 643 request.head(method, authority(secure, host, port), path, headers, n) 644 645 # GET with optional headers 646 def get_req(host: String, path: String, headers: List<&2, Header>) -> String: 647 request("GET", host, path, headers, "") 648 649 # HEAD with optional headers (RFC 9110 §9.3.2): same as GET, no body 650 def head_req(host: String, path: String, headers: List<&2, Header>) -> String: 651 request("HEAD", host, path, headers, "") 652 653 # POST with body and optional headers 654 def post_req(host: String, path: String, headers: List<&2, Header>, 655 body: String) -> String: 656 request("POST", host, path, headers, body) 657 658 # PUT with body and optional headers (RFC 9110 §9.3.4) 659 def put_req(host: String, path: String, headers: List<&2, Header>, 660 body: String) -> String: 661 request("PUT", host, path, headers, body) 662 663 # DELETE with body and optional headers (RFC 9110 §9.3.5) 664 def delete_req(host: String, path: String, headers: List<&2, Header>, 665 body: String) -> String: 666 request("DELETE", host, path, headers, body) 667 668 # OPTIONS with optional headers (RFC 9110 §9.3.7). No request body. 669 def options_req(host: String, path: String, headers: List<&2, Header>) -> String: 670 request("OPTIONS", host, path, headers, "") 671 672 # GET, HEAD, and OPTIONS are safe (RFC 9110 §9.2.1). Tokens are case-sensitive. 673 def method.safe(+method: String) -> Bool: 674 Bool.or(String.eq(method, "GET"), 675 Bool.or(String.eq(method, "HEAD"), String.eq(method, "OPTIONS"))) 676 677 # safe methods, plus PUT and DELETE, are idempotent (RFC 9110 §9.2.2) 678 def method.idempotent(+method: String) -> Bool: 679 Bool.or(method.safe(method), 680 Bool.or(String.eq(method, "PUT"), String.eq(method, "DELETE"))) 681 682 # the rest of a token once this character is known 683 def token.ok.step(space: Bool, rest: Unit -> Bool) -> Bool: 684 match space: 685 case True{}: 686 False{} 687 case False{}: 688 rest(Unit{}) 689 690 # a bearer token has no whitespace (RFC 6750 §2.1) 691 def token.ok(s: String) -> Bool: 692 match s: 693 case SNil{}: 694 True{} 695 case SCon{h, t}: 696 token.ok.step(Char.is_space(h), _u => token.ok(t)) 697 698 # origin-form request-target: absolute-path, optional query (RFC 9112 §3.2.1) 699 def target.origin(path: String) -> Bool: 700 String.starts_with(path, "/") 701 702 # asterisk-form is only `*` and only for OPTIONS (RFC 9112 §3.2.4) 703 def target.asterisk(method: String, star: Bool) -> Bool: 704 match star: 705 case False{}: 706 False{} 707 case True{}: 708 String.eq(method, "OPTIONS") 709 710 # origin-form, or OPTIONS asterisk-form 711 def target.form(method: String, path: String, absolute: Bool) -> Bool: 712 match absolute: 713 case True{}: 714 True{} 715 case False{}: 716 target.asterisk(method, String.eq(path, "*")) 717 718 # a body against the length its head promised 719 def parse.short(status: U32, headers: List<&2, Header>, n: Nat, body: String, 720 short: Bool) -> Reply: 721 match short: 722 case True{}: 723 Torn{"the response body stopped before its Content-Length"} 724 case False{}: 725 Reply{status, headers, octs.take(body, n)} 726 727 # a body framed by Content-Length (RFC 9110 §8.6 / RFC 9112 §6.3) 728 def parse.clen(status: U32, headers: List<&2, Header>, +n: Nat, +body: String) 729 -> Reply: 730 parse.short(status, headers, n, body, Nat.is_lt(octs.len(body), n)) 731 732 # Content-Length when present; else the rest of the connection is the body 733 def parse.len(status: U32, headers: List<&2, Header>, m: Maybe<&2, Nat>, 734 body: String) -> Reply: 735 match m: 736 case None{}: 737 Reply{status, headers, body} 738 case Some{n}: 739 parse.clen(status, headers, n, body) 740 741 # every Content-Length text equals the first 742 def clen.agree.step(same: Bool, rest: Unit -> Bool) -> Bool: 743 match same: 744 case False{}: 745 False{} 746 case True{}: 747 rest(Unit{}) 748 749 # the tail of a Content-Length list against the first value 750 def clen.agree.go(rest: List<&2, String>, +h: String) -> Bool: 751 match rest: 752 case []: 753 True{} 754 case x <> r: 755 clen.agree.step(String.eq(x, h), _u => clen.agree.go(r, h)) 756 757 # duplicate Content-Length fields must be the same digits (RFC 9112 §6.3) 758 def clen.agree(vs: List<&2, String>) -> Bool: 759 match vs: 760 case []: 761 True{} 762 case h <> t: 763 clen.agree.go(t, h) 764 765 # whether any Content-Length field was present 766 def clen.any(vs: List<&2, String>) -> Bool: 767 match vs: 768 case []: 769 False{} 770 case h <> t: 771 True{} 772 773 # a chunked body, or why the chunk framing was rejected 774 def parse.chunked(status: U32, headers: List<&2, Header>, body: String, 775 bad: Maybe<&2, String>) -> Reply: 776 match bad: 777 case None{}: 778 Reply{status, headers, dechunk(body)} 779 case Some{why}: 780 Torn{why} 781 782 # scan chunk sizes, then decode when they are hex and closed by 0 783 def parse.chunks(status: U32, headers: List<&2, Header>, +body: String) -> Reply: 784 parse.chunked(status, headers, body, 785 chunks.scan(String.length(body), divide(body, "\r\n"))) 786 787 # Transfer-Encoding together with Content-Length is a bad message 788 def parse.te.clash(status: U32, headers: List<&2, Header>, body: String, 789 present: Bool) -> Reply: 790 match present: 791 case True{}: 792 Torn{"Content-Length and Transfer-Encoding conflict"} 793 case False{}: 794 parse.chunks(status, headers, body) 795 796 # chunked framing, or Content-Length, or the rest of the connection 797 def parse.te(status: U32, headers: List<&2, Header>, +lens: List<&2, String>, 798 +body: String, chunked: Bool) -> Reply: 799 match chunked: 800 case False{}: 801 parse.len(status, headers, digits.read(first(lens)), body) 802 case True{}: 803 parse.te.clash(status, headers, body, clen.any(lens)) 804 805 # disagreeing Content-Length values are torn before framing 806 def parse.agree(status: U32, headers: List<&2, Header>, te: String, 807 +lens: List<&2, String>, body: String, ok: Bool) -> Reply: 808 match ok: 809 case False{}: 810 Torn{"Content-Length values disagree"} 811 case True{}: 812 parse.te(status, headers, lens, body, 813 String.contains(String.to_lower(te), "chunked")) 814 815 # the headers that decide the framing (RFC 9112 §6.3) 816 def parse.frame(status: U32, headers: List<&2, Header>, te: String, 817 +lens: List<&2, String>, body: String) -> Reply: 818 parse.agree(status, headers, te, lens, body, clen.agree(lens)) 819 820 # a head whose status line has been read (RFC 9112 §4) 821 def parse.status(m: Maybe<&2, U32>, +ls: List<&2, String>, body: String) 822 -> Reply: 823 match m: 824 case None{}: 825 Torn{"the response has no status line"} 826 case Some{n}: 827 +hs = head.list(ls) 828 parse.frame(n, hs, head.go(ls, "transfer-encoding"), 829 head.values(ls, "content-length"), body) 830 831 # the status code a head's first line names, if it names one (RFC 9112 §4) 832 def status.of(ls: List<&2, String>) -> Maybe<&2, U32>: 833 U32.read(second(String.split(first(ls), ' '))) 834 835 # the head's lines, with the status line read off the first 836 def parse.head(+ls: List<&2, String>, body: String) -> Reply: 837 parse.status(status.of(ls), ls, body) 838 839 # a response divided into its head and everything after the blank line 840 # (RFC 9112 §2.1: header section ends at empty line) 841 def parse.cut(c: Cut) -> Reply: 842 match c: 843 case NoCut{}: 844 Torn{"the response head has no blank line after it"} 845 case Cut{before, after}: 846 parse.head(String.lines(before), after) 847 848 # the status, headers, and body of a raw response 849 def parse(raw: String) -> Reply: 850 parse.cut(divide(raw, "\r\n\r\n")) 851 852 # a parsed response on one line, for a test to read 853 def show(r: Reply) -> String: 854 match r: 855 case Torn{why}: 856 "torn " ++ why 857 case Reply{status, headers, body}: 858 U32.show(status) ++ " " ++ body 859 860 # --- inbound request (server) and outbound response (server) --- 861 862 # a request that came apart, or the reason it would not (RFC 9112 §3) 863 type Request is Data: 864 Bad{why: String} 865 Request{method: String, target: String, headers: List<&2, Header>, 866 body: String} 867 868 # a status code and its reason-phrase 869 type Phrase is Data: 870 P{code: U32, text: String} 871 872 # the reason-phrases RFC 9110 §15 names (RFC 6585 §4 for 429) 873 def reasons() -> List<&2, Phrase>: 874 [P{200, "OK"}, P{201, "Created"}, P{202, "Accepted"}, P{204, "No Content"}, 875 P{301, "Moved Permanently"}, P{302, "Found"}, P{303, "See Other"}, 876 P{304, "Not Modified"}, P{307, "Temporary Redirect"}, 877 P{308, "Permanent Redirect"}, P{400, "Bad Request"}, 878 P{401, "Unauthorized"}, P{403, "Forbidden"}, P{404, "Not Found"}, 879 P{405, "Method Not Allowed"}, P{409, "Conflict"}, P{410, "Gone"}, 880 P{413, "Content Too Large"}, P{415, "Unsupported Media Type"}, 881 P{422, "Unprocessable Content"}, P{429, "Too Many Requests"}, 882 P{500, "Internal Server Error"}, P{501, "Not Implemented"}, 883 P{502, "Bad Gateway"}, P{503, "Service Unavailable"}, 884 P{504, "Gateway Timeout"}] 885 886 # this phrase when its code is the one asked for, else the rest's 887 def reason.pick(text: String, hit: Bool, rest: Unit -> String) -> String: 888 match hit: 889 case True{}: 890 text 891 case False{}: 892 rest(Unit{}) 893 894 # the phrase a list gives a code, "" when it gives none 895 def reason.in(ps: List<&2, Phrase>, +code: U32) -> String: 896 match ps: 897 case []: 898 "" 899 case P{c, text} <> t: 900 reason.pick(text, U32.is_eq(code, c), _u => reason.in(t, code)) 901 902 # the reason-phrase for a status code (RFC 9112 §4); "" for one not listed 903 def reason(+code: U32) -> String: 904 reason.in(reasons(), code) 905 906 # status-line + headers + body (RFC 9112 §4, §5). Connection: close ends the 907 # exchange after one response. 908 def respond(+status: U32, headers: List<&2, Header>, +body: String) -> String: 909 +n = utf8.len(body) 910 "HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ "\r\n" 911 ++ "Connection: close\r\n" 912 ++ headers.lines(headers) 913 ++ clen.line(n, Nat.is_eq(n, 0n)) 914 ++ "\r\n" 915 ++ body 916 917 # 204 and 304 are terminated by the header section (RFC 9110 §15.3.5, §15.4.5) 918 def status.nocontent(+code: U32) -> Bool: 919 Bool.or(U32.is_eq(code, 204), U32.is_eq(code, 304)) 920 921 # drop a 204/304 body; any other status keeps it 922 def respond.of.body(+status: U32, headers: List<&2, Header>, body: String, 923 drop: Bool) -> String: 924 match drop: 925 case True{}: 926 respond(status, headers, "") 927 case False{}: 928 respond(status, headers, body) 929 930 # format a structured Reply as a response message 931 def respond.of(r: Reply) -> String: 932 match r: 933 case Torn{why}: 934 respond(500, [], why) 935 case Reply{+status, headers, body}: 936 respond.of.body(status, headers, body, status.nocontent(status)) 937 938 # HEAD carries no message body; any other method keeps the body (RFC 9110 §9.3.2) 939 def reply.for.pick(reply: Reply, head: Bool) -> Reply: 940 match reply head: 941 case Torn{why} _: 942 Torn{why} 943 case Reply{status, headers, body} False{}: 944 Reply{status, headers, body} 945 case Reply{status, headers, body} True{}: 946 Reply{status, headers, ""} 947 948 # drop a HEAD response body before it is written 949 def reply.for(method: String, reply: Reply) -> Reply: 950 reply.for.pick(reply, String.eq(method, "HEAD")) 951 952 # the method token, or "" when the request did not parse 953 def method.of(req: Request) -> String: 954 match req: 955 case Bad{why}: 956 "" 957 case Request{method, target, headers, body}: 958 method 959 960 # request body against Content-Length 961 def ask.short(method: String, target: String, headers: List<&2, Header>, 962 n: Nat, body: String, short: Bool) -> Request: 963 match short: 964 case True{}: 965 Bad{"the request body stopped before its Content-Length"} 966 case False{}: 967 Request{method, target, headers, octs.take(body, n)} 968 969 # a request body framed by Content-Length (RFC 9110 §8.6 / RFC 9112 §6.3) 970 def ask.clen(method: String, target: String, headers: List<&2, Header>, 971 +n: Nat, +body: String) -> Request: 972 ask.short(method, target, headers, n, body, Nat.is_lt(octs.len(body), n)) 973 974 # Content-Length when present; else the rest of the connection is the body 975 def ask.len(method: String, target: String, headers: List<&2, Header>, 976 m: Maybe<&2, Nat>, body: String) -> Request: 977 match m: 978 case None{}: 979 Request{method, target, headers, body} 980 case Some{n}: 981 ask.clen(method, target, headers, n, body) 982 983 # a chunked request body, or why the chunk framing was rejected 984 def ask.chunked(method: String, target: String, headers: List<&2, Header>, 985 body: String, bad: Maybe<&2, String>) -> Request: 986 match bad: 987 case None{}: 988 Request{method, target, headers, dechunk(body)} 989 case Some{why}: 990 Bad{why} 991 992 # scan chunk sizes, then decode when they are hex and closed by 0 993 def ask.chunks(method: String, target: String, headers: List<&2, Header>, 994 +body: String) -> Request: 995 ask.chunked(method, target, headers, body, 996 chunks.scan(String.length(body), divide(body, "\r\n"))) 997 998 # Transfer-Encoding together with Content-Length is a bad request 999 def ask.te.clash(method: String, target: String, headers: List<&2, Header>, 1000 body: String, present: Bool) -> Request: 1001 match present: 1002 case True{}: 1003 Bad{"Content-Length and Transfer-Encoding conflict"} 1004 case False{}: 1005 ask.chunks(method, target, headers, body) 1006 1007 # chunked framing, or Content-Length, or the rest of the connection 1008 def ask.te(method: String, target: String, headers: List<&2, Header>, 1009 +lens: List<&2, String>, +body: String, chunked: Bool) -> Request: 1010 match chunked: 1011 case False{}: 1012 ask.len(method, target, headers, digits.read(first(lens)), body) 1013 case True{}: 1014 ask.te.clash(method, target, headers, body, clen.any(lens)) 1015 1016 # disagreeing Content-Length values are bad before framing 1017 def ask.agree(method: String, target: String, headers: List<&2, Header>, 1018 te: String, +lens: List<&2, String>, body: String, ok: Bool) -> Request: 1019 match ok: 1020 case False{}: 1021 Bad{"Content-Length values disagree"} 1022 case True{}: 1023 ask.te(method, target, headers, lens, body, 1024 String.contains(String.to_lower(te), "chunked")) 1025 1026 # the headers that decide request framing (RFC 9112 §6.3) 1027 def ask.frame(method: String, target: String, headers: List<&2, Header>, 1028 te: String, +lens: List<&2, String>, body: String) -> Request: 1029 ask.agree(method, target, headers, te, lens, body, clen.agree(lens)) 1030 1031 # origin-form, or a bad request when the target is not an absolute-path 1032 def ask.origin(method: String, target: String, +ls: List<&2, String>, 1033 body: String, ok: Bool) -> Request: 1034 match ok: 1035 case False{}: 1036 Bad{"the request-target is not in origin-form"} 1037 case True{}: 1038 +hs = head.list(ls) 1039 ask.frame(method, target, hs, head.go(ls, "transfer-encoding"), 1040 head.values(ls, "content-length"), body) 1041 1042 # request-line: method SP request-target SP HTTP-version (RFC 9112 §3) 1043 def ask.line(+method: String, +target: String, +ls: List<&2, String>, 1044 body: String) -> Request: 1045 ask.origin(method, target, ls, body, 1046 target.form(method, target, target.origin(target))) 1047 1048 # three fields of the request-line, or torn 1049 def ask.parts(ps: List<&2, String>, +ls: List<&2, String>, body: String) 1050 -> Request: 1051 match ps: 1052 case []: 1053 Bad{"the request has no request line"} 1054 case m <> rest: 1055 match rest: 1056 case []: 1057 Bad{"the request line has no target"} 1058 case t <> _ver: 1059 ask.line(m, t, ls, body) 1060 1061 # the head's lines, with the request-line read off the first 1062 def ask.head(+ls: List<&2, String>, body: String) -> Request: 1063 match ls: 1064 case []: 1065 Bad{"the request has no request line"} 1066 case _: 1067 ask.parts(String.split(first(ls), ' '), ls, body) 1068 1069 # a request divided into its head and everything after the blank line 1070 def ask.cut(c: Cut) -> Request: 1071 match c: 1072 case NoCut{}: 1073 Bad{"the request head has no blank line after it"} 1074 case Cut{before, after}: 1075 ask.head(String.lines(before), after) 1076 1077 # the method, target, headers, and body of a raw request 1078 def ask(raw: String) -> Request: 1079 ask.cut(divide(raw, "\r\n\r\n")) 1080 1081 # a parsed request on one line, for a test to read 1082 def ask.show(r: Request) -> String: 1083 match r: 1084 case Bad{why}: 1085 "bad " ++ why 1086 case Request{method, target, headers, body}: 1087 method ++ " " ++ target ++ " " ++ body 1088 1089 # --- when a response has all arrived (RFC 9112 §6.3) --- 1090 1091 # how a response's body is framed 1092 type Frame is Data: 1093 Bare{} 1094 Sized{n: Nat} 1095 Chunked{} 1096 Closed{} 1097 1098 # 204 and 304 carry no body; a status line that did not parse is framed by 1099 # its headers 1100 def status.bare(m: Maybe<&2, U32>) -> Bool: 1101 match m: 1102 case None{}: 1103 False{} 1104 case Some{n}: 1105 status.nocontent(n) 1106 1107 # a 1xx status is an interim response (RFC 9110 §15.2); a status line that 1108 # did not parse is not 1109 def status.interim(m: Maybe<&2, U32>) -> Bool: 1110 match m: 1111 case None{}: 1112 False{} 1113 case Some{+n}: 1114 Bool.and(U32.is_le(100, n), U32.is_lt(n, 200)) 1115 1116 # a Content-Length that reads as a number, or the close when none does 1117 def frame.sized(m: Maybe<&2, Nat>) -> Frame: 1118 match m: 1119 case None{}: 1120 Closed{} 1121 case Some{n}: 1122 Sized{n} 1123 1124 # Content-Length values that agree frame the body; any others leave the close 1125 def frame.agree(+lens: List<&2, String>, ok: Bool) -> Frame: 1126 match ok: 1127 case False{}: 1128 Closed{} 1129 case True{}: 1130 frame.sized(digits.read(first(lens))) 1131 1132 # chunked framing wins over Content-Length (RFC 9112 §6.3 item 4) 1133 def frame.te(+lens: List<&2, String>, chunked: Bool) -> Frame: 1134 match chunked: 1135 case True{}: 1136 Chunked{} 1137 case False{}: 1138 frame.agree(lens, clen.agree(lens)) 1139 1140 # the framing of a response: none when bare (a reply to HEAD, a 204 or a 304), 1141 # else Transfer-Encoding chunked, else Content-Length, else the close 1142 def frame(bare: Bool, te: String, +lens: List<&2, String>) -> Frame: 1143 match bare: 1144 case True{}: 1145 Bare{} 1146 case False{}: 1147 frame.te(lens, String.contains(String.to_lower(te), "chunked")) 1148 1149 # a zero chunk's trailer section has ended: at once, or after its fields 1150 def chunks.trailer(+after: String) -> Bool: 1151 Bool.or(String.starts_with(after, "\r\n"), String.contains(after, "\r\n\r\n")) 1152 1153 # a chunk that has all arrived: on to the next chunk header, else not yet 1154 def chunks.ended.data(+after: String, +n: Nat, ready: Bool, 1155 rest: Cut -> Bool) -> Bool: 1156 match ready: 1157 case False{}: 1158 False{} 1159 case True{}: 1160 rest(divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")) 1161 1162 # a chunk of size n: the last one when n is 0 1163 def chunks.ended.size(+after: String, +n: Nat, zero: Bool, 1164 rest: Cut -> Bool) -> Bool: 1165 match zero: 1166 case True{}: 1167 chunks.trailer(after) 1168 case False{}: 1169 chunks.ended.data(after, n, chunks.ready(after, n), rest) 1170 1171 # a chunk header: a size that is not hex ends the read (the parse rejects it) 1172 def chunks.ended.hex(+before: String, +after: String, ok: Bool, 1173 rest: Cut -> Bool) -> Bool: 1174 match ok: 1175 case False{}: 1176 True{} 1177 case True{}: 1178 +n = hexlen(before) 1179 chunks.ended.size(after, n, Nat.is_eq(n, 0n), rest) 1180 1181 # a chunk header that has arrived, or not yet 1182 def chunks.ended.at(c: Cut, rest: Cut -> Bool) -> Bool: 1183 match c: 1184 case NoCut{}: 1185 False{} 1186 case Cut{+before, +after}: 1187 chunks.ended.hex(before, after, hex.well(chunks.token(before)), rest) 1188 1189 # whether a chunked body has reached the end of its last chunk 1190 def chunks.ended(fuel: Nat, c: Cut) -> Bool: 1191 match fuel: 1192 case 0n: 1193 False{} 1194 case 1n+f: 1195 chunks.ended.at(c, cut => chunks.ended(f, cut)) 1196 1197 # whether a body has all arrived under its framing 1198 def whole.of(f: Frame, +body: String) -> Bool: 1199 match f: 1200 case Bare{}: 1201 True{} 1202 case Sized{n}: 1203 Bool.not(Nat.is_lt(octs.len(body), n)) 1204 case Chunked{}: 1205 chunks.ended(String.length(body), divide(body, "\r\n")) 1206 case Closed{}: 1207 False{} 1208 1209 # a head that is interim: whole when what follows is; else whole under the 1210 # framing it gives 1211 def whole.step(asked: Bool, +ls: List<&2, String>, +after: String, 1212 interim: Bool, rest: Cut -> Bool) -> Bool: 1213 match interim: 1214 case True{}: 1215 rest(divide(after, "\r\n\r\n")) 1216 case False{}: 1217 whole.of(frame(Bool.or(asked, status.bare(status.of(ls))), 1218 head.go(ls, "transfer-encoding"), head.values(ls, "content-length")), 1219 after) 1220 1221 # a response whose head has ended, or not yet 1222 def whole.at(asked: Bool, c: Cut, rest: Cut -> Bool) -> Bool: 1223 match c: 1224 case NoCut{}: 1225 False{} 1226 case Cut{before, after}: 1227 +ls = String.lines(before) 1228 whole.step(asked, ls, after, status.interim(status.of(ls)), rest) 1229 1230 # the heads in turn, at most `fuel` of them 1231 def whole.go(fuel: Nat, +asked: Bool, c: Cut) -> Bool: 1232 match fuel: 1233 case 0n: 1234 False{} 1235 case 1n+f: 1236 whole.at(asked, c, cut => whole.go(f, asked, cut)) 1237 1238 # whether the text received so far is a whole response to a request with 1239 # this method (RFC 9112 §6.3), interim 1xx heads read past (RFC 9110 §15.2); 1240 # a response framed by the close never is 1241 def whole(+method: String, +raw: String) -> Bool: 1242 whole.go(String.length(raw), String.eq(method, "HEAD"), 1243 divide(raw, "\r\n\r\n")) 1244 1245 # --- a response read by the request's method (RFC 9110 §15.2, §6.4.1) --- 1246 1247 # a 1xx, 204 or 304 response has no body (RFC 9110 §6.4.1) 1248 def status.empty(+m: Maybe<&2, U32>) -> Bool: 1249 Bool.or(status.interim(m), status.bare(m)) 1250 1251 # an interim head is skipped when more text follows it 1252 def interim.skip(m: Maybe<&2, U32>, after: String) -> Bool: 1253 Bool.and(status.interim(m), Bool.not(String.is_empty(after))) 1254 1255 # the final head with no body: its status and fields, an empty body 1256 def parse.to.bare(m: Maybe<&2, U32>, ls: List<&2, String>) -> Reply: 1257 match m: 1258 case None{}: 1259 Torn{"the response has no status line"} 1260 case Some{n}: 1261 Reply{n, head.list(ls), ""} 1262 1263 # the final head: with no body, or framed as parse frames it 1264 def parse.to.final(m: Maybe<&2, U32>, +ls: List<&2, String>, after: String, 1265 bare: Bool) -> Reply: 1266 match bare: 1267 case True{}: 1268 parse.to.bare(m, ls) 1269 case False{}: 1270 parse.status(m, ls, after) 1271 1272 # an interim head is skipped, any other is the final one 1273 def parse.to.step(head: Bool, +ls: List<&2, String>, +m: Maybe<&2, U32>, 1274 +after: String, skip: Bool, rest: Cut -> Reply) -> Reply: 1275 match skip: 1276 case True{}: 1277 rest(divide(after, "\r\n\r\n")) 1278 case False{}: 1279 parse.to.final(m, ls, after, Bool.or(head, status.empty(m))) 1280 1281 # a head that has ended, or the reason it has not 1282 def parse.to.at(head: Bool, c: Cut, rest: Cut -> Reply) -> Reply: 1283 match c: 1284 case NoCut{}: 1285 Torn{"the response head has no blank line after it"} 1286 case Cut{before, +after}: 1287 +ls = String.lines(before) 1288 +m = status.of(ls) 1289 parse.to.step(head, ls, m, after, interim.skip(m, after), rest) 1290 1291 # the heads in turn, at most `fuel` of them 1292 def parse.to.go(fuel: Nat, +head: Bool, c: Cut) -> Reply: 1293 match fuel: 1294 case 0n: 1295 Torn{"the response head has no blank line after it"} 1296 case 1n+f: 1297 parse.to.at(head, c, cut => parse.to.go(f, head, cut)) 1298 1299 # the response to a request with this method (RFC 9112 §6.3): interim 1xx 1300 # heads skipped, and no body for HEAD or a 1xx, 204 or 304 final status 1301 def parse.to(+method: String, +raw: String) -> Reply: 1302 parse.to.go(String.length(raw), String.eq(method, "HEAD"), 1303 divide(raw, "\r\n\r\n")) 1304 1305 # --- when a response has all arrived, judged read by read (SPEC.md HTTP-CLI-12) --- 1306 # 1307 # The client judges the text it has received after every read. Judging the 1308 # whole text each time (whole) costs the whole text each time; the state 1309 # below carries the judgement from one read to the next, so a read costs what 1310 # it brought (and the head, while no final head has ended). hear.more( 1311 # hear.of(method, a), b) is hear.of(method, a ++ b), and hear.done of it is 1312 # whole(method, a ++ b). 1313 1314 # bytes of UTF-8 walked off the front of a string: all of it, with this many 1315 # still owed, or a stop at the first character that does not fit in what was 1316 # left, with what was left and the text from that character on 1317 type Skip is Data: 1318 Short{owed: Nat} 1319 Over{left: Nat, rest: String} 1320 1321 # one step of the skip: a character wider than what is left stops it 1322 def octs.skip.step(short: Bool, +n: Nat, h: Char, t: String, 1323 rest: Unit -> Skip) -> Skip: 1324 match short: 1325 case True{}: 1326 Over{n, SCon{h, t}} 1327 case False{}: 1328 rest(Unit{}) 1329 1330 # walk n bytes of UTF-8 off a string, as octs.drop does, saying how it ended 1331 def octs.skip(s: String, +n: Nat) -> Skip: 1332 match s: 1333 case SNil{}: 1334 Short{n} 1335 case SCon{+h, +t}: 1336 +w = octs.width(Char.to_u32(h)) 1337 octs.skip.step(Nat.is_lt(n, w), n, h, t, _u => octs.skip(t, Nat.sub(n, w))) 1338 1339 # where a chunked body's walk stands: ended; waiting at a chunk header (or the 1340 # last chunk's trailer) that has not all arrived; inside a chunk's data with 1341 # octets still owed; or at the end of a chunk's data, at what should be its 1342 # CRLF. `fuel` is what chunks.ended would have left there. 1343 type Walk is Data: 1344 Ended{} 1345 Hold{fuel: Nat, rest: String} 1346 Owed{fuel: Nat, owed: Nat} 1347 Tail{fuel: Nat, left: Nat, rest: String} 1348 1349 # a chunk's data skipped: whether its CRLF has arrived, and on to the next 1350 # chunk header when it has 1351 def walk.crlf(g: Nat, +left: Nat, +rest: String, ready: Bool, 1352 go: String -> Walk) -> Walk: 1353 match ready: 1354 case True{}: 1355 go(octs.drop(rest, Nat.add(left, 2n))) 1356 case False{}: 1357 Tail{g, left, rest} 1358 1359 # a chunk's data: all of what arrived, with octets still owed, or its end 1360 def walk.data(+g: Nat, s: Skip, go: String -> Walk) -> Walk: 1361 match s: 1362 case Short{owed}: 1363 Owed{g, owed} 1364 case Over{+left, +rest}: 1365 walk.crlf(g, left, rest, Bool.and(octs.has(rest, Nat.add(left, 2n)), 1366 String.starts_with(rest, "\r\n")), go) 1367 1368 # the last chunk: ended once its trailer section has 1369 def walk.trail(g: Nat, rest: String, ended: Bool) -> Walk: 1370 match ended: 1371 case True{}: 1372 Ended{} 1373 case False{}: 1374 Hold{1n+g, rest} 1375 1376 # a chunk of size n: the last one when n is 0 1377 def walk.size(+g: Nat, rest: String, +after: String, +n: Nat, zero: Bool, 1378 go: String -> Walk) -> Walk: 1379 match zero: 1380 case True{}: 1381 walk.trail(g, rest, chunks.trailer(after)) 1382 case False{}: 1383 walk.data(g, octs.skip(after, n), go) 1384 1385 # a chunk header: a size that is not hex ends the walk (the parse rejects it) 1386 def walk.hex(+g: Nat, rest: String, +before: String, +after: String, ok: Bool, 1387 go: String -> Walk) -> Walk: 1388 match ok: 1389 case False{}: 1390 Ended{} 1391 case True{}: 1392 +n = hexlen(before) 1393 walk.size(g, rest, after, n, Nat.is_eq(n, 0n), go) 1394 1395 # a chunk header that has arrived, or not yet 1396 def walk.at(+g: Nat, rest: String, c: Cut, go: String -> Walk) -> Walk: 1397 match c: 1398 case NoCut{}: 1399 Hold{1n+g, rest} 1400 case Cut{+before, +after}: 1401 walk.hex(g, rest, before, after, hex.well(chunks.token(before)), go) 1402 1403 # the walk of chunks.ended from the chunk header at the front of a text 1404 def walk(fuel: Nat, +rest: String) -> Walk: 1405 match fuel: 1406 case 0n: 1407 Hold{0n, rest} 1408 case 1n+f: 1409 +g = f 1410 walk.at(g, rest, divide(rest, "\r\n"), s => walk(g, s)) 1411 1412 # whether the walk has ended 1413 def walk.done(w: Walk) -> Bool: 1414 match w: 1415 case Ended{}: 1416 True{} 1417 case Hold{fuel, rest}: 1418 False{} 1419 case Owed{fuel, owed}: 1420 False{} 1421 case Tail{fuel, left, rest}: 1422 False{} 1423 1424 # the walk once m more characters, b, have arrived 1425 def walk.more(w: Walk, +m: Nat, b: String) -> Walk: 1426 match w: 1427 case Ended{}: 1428 Ended{} 1429 case Hold{fuel, rest}: 1430 walk(Nat.add(fuel, m), rest ++ b) 1431 case Owed{g, owed}: 1432 +f = Nat.add(g, m) 1433 walk.data(f, octs.skip(b, owed), s => walk(f, s)) 1434 case Tail{g, left, rest}: 1435 +f = Nat.add(g, m) 1436 walk.data(f, Over{left, rest ++ b}, s => walk(f, s)) 1437 1438 # where a response stands: no final head yet (with the text from the head 1439 # being read on, and the fuel whole.go would have there); no body, so whole; 1440 # framed by the close, so never whole; sized, with the octets that have 1441 # arrived; or chunked, with its walk 1442 type Hear is Data: 1443 Heads{fuel: Nat, asked: Bool, raw: String} 1444 Whole{} 1445 Never{} 1446 Count{n: Nat, have: Nat} 1447 Walking{w: Walk} 1448 1449 # a final head's body under its framing 1450 def hear.body(f: Frame, +after: String) -> Hear: 1451 match f: 1452 case Bare{}: 1453 Whole{} 1454 case Sized{n}: 1455 Count{n, octs.len(after)} 1456 case Chunked{}: 1457 Walking{walk(String.length(after), after)} 1458 case Closed{}: 1459 Never{} 1460 1461 # an interim head: on to the next; a final one: its body 1462 def hear.step(asked: Bool, +ls: List<&2, String>, +after: String, 1463 interim: Bool, go: String -> Hear) -> Hear: 1464 match interim: 1465 case True{}: 1466 go(after) 1467 case False{}: 1468 hear.body(frame(Bool.or(asked, status.bare(status.of(ls))), 1469 head.go(ls, "transfer-encoding"), head.values(ls, "content-length")), 1470 after) 1471 1472 # a head that has ended, or not yet 1473 def hear.at(+g: Nat, +asked: Bool, raw: String, c: Cut, go: String -> Hear) 1474 -> Hear: 1475 match c: 1476 case NoCut{}: 1477 Heads{1n+g, asked, raw} 1478 case Cut{before, after}: 1479 +ls = String.lines(before) 1480 hear.step(asked, ls, after, status.interim(status.of(ls)), go) 1481 1482 # the heads in turn, as whole.go reads them 1483 def hear.go(fuel: Nat, +asked: Bool, +raw: String) -> Hear: 1484 match fuel: 1485 case 0n: 1486 Heads{0n, asked, raw} 1487 case 1n+f: 1488 +g = f 1489 hear.at(g, asked, raw, divide(raw, "\r\n\r\n"), s => hear.go(g, asked, s)) 1490 1491 # where a response to a request with this method stands, read from the 1492 # whole text received so far 1493 def hear.of(+method: String, +raw: String) -> Hear: 1494 hear.go(String.length(raw), String.eq(method, "HEAD"), raw) 1495 1496 # whether the response has all arrived 1497 def hear.done(h: Hear) -> Bool: 1498 match h: 1499 case Heads{fuel, asked, raw}: 1500 False{} 1501 case Whole{}: 1502 True{} 1503 case Never{}: 1504 False{} 1505 case Count{n, have}: 1506 Bool.not(Nat.is_lt(have, n)) 1507 case Walking{w}: 1508 walk.done(w) 1509 1510 # where a response stands once more text, b, has arrived 1511 def hear.more(h: Hear, +b: String) -> Hear: 1512 match h: 1513 case Heads{fuel, asked, raw}: 1514 hear.go(Nat.add(fuel, String.length(b)), asked, raw ++ b) 1515 case Whole{}: 1516 Whole{} 1517 case Never{}: 1518 Never{} 1519 case Count{n, have}: 1520 Count{n, Nat.add(have, octs.len(b))} 1521 case Walking{w}: 1522 Walking{walk.more(w, String.length(b), b)} 1523 1524 # --- a response's reason phrase (RFC 9112 §4) --- 1525 1526 # a reason phrase with the CR its line ended with removed; a reason-phrase 1527 # itself holds no CR (RFC 9112 §4) 1528 def phrase.chop.at( 1529 cr: Bool, 1530 ch: Char, 1531 tl: String, 1532 rest: Unit -> String 1533 ) -> String: 1534 match cr tl: 1535 case True{} SNil{}: 1536 SNil{} 1537 case True{} SCon{x, xt}: 1538 SCon{ch, rest(Unit{})} 1539 case False{} _: 1540 SCon{ch, rest(Unit{})} 1541 1542 # the text of a reason phrase, a final CR removed 1543 def phrase.chop(text: String) -> String: 1544 match text: 1545 case SNil{}: 1546 SNil{} 1547 case SCon{+ch, +tl}: 1548 phrase.chop.at(Char.is_eq(ch, '\r'), ch, tl, _u => phrase.chop(tl)) 1549 1550 # what follows the status code's SP: the reason phrase; none without that SP 1551 def phrase.code(cut: Cut) -> String: 1552 match cut: 1553 case NoCut{}: 1554 "" 1555 case Cut{before, after}: 1556 phrase.chop(after) 1557 1558 # what follows the version's SP: the status code, an SP and the reason 1559 def phrase.after(cut: Cut) -> String: 1560 match cut: 1561 case NoCut{}: 1562 "" 1563 case Cut{before, after}: 1564 phrase.code(divide(after, " ")) 1565 1566 # a status line's reason phrase (RFC 9112 §4: HTTP-version SP status-code SP 1567 # [ reason-phrase ]), empty when the line has fewer than two SPs 1568 def phrase.line(line: String) -> String: 1569 phrase.after(divide(line, " ")) 1570 1571 # an interim head is skipped, any other gives the reason on its first line 1572 def phrase.to.step( 1573 +ls: List<&2, String>, 1574 +after: String, 1575 skip: Bool, 1576 rest: Cut -> String 1577 ) -> String: 1578 match skip: 1579 case True{}: 1580 rest(divide(after, "\r\n\r\n")) 1581 case False{}: 1582 phrase.line(first(ls)) 1583 1584 # a head that has ended, or none 1585 def phrase.to.at(cut: Cut, rest: Cut -> String) -> String: 1586 match cut: 1587 case NoCut{}: 1588 "" 1589 case Cut{before, +after}: 1590 +ls = String.lines(before) 1591 phrase.to.step(ls, after, interim.skip(status.of(ls), after), rest) 1592 1593 # the heads in turn, at most `fuel` of them 1594 def phrase.to.go(fuel: Nat, cut: Cut) -> String: 1595 match fuel: 1596 case 0n: 1597 "" 1598 case 1n+f: 1599 phrase.to.at(cut, next => phrase.to.go(f, next)) 1600 1601 # the reason phrase of the head parse.to stops at: interim 1xx heads with 1602 # text after them skipped, empty when no head ends 1603 def phrase.to(+raw: String) -> String: 1604 phrase.to.go(String.length(raw), divide(raw, "\r\n\r\n"))