1 # ezhttp/src/step: what the server decides for one connection, kept pure so 2 # a law can state it. server.bend runs the effects and asks this module what 3 # each outcome means; it decides nothing itself. 4 import Base 5 import ./http.bend as Http 6 import ./qs.bend as Qs 7 8 # what a request asks of the server: answer it now, or call the handler 9 type Step is Data: 10 Answer{reply: Http.Reply} 11 Call{req: Http.Request} 12 13 # an effect's outcome on one connection: the value, or the line to log for 14 # a connection that is lost 15 type Got<-A: Type> is Type: 16 Kept{value: A} 17 Lost{line: String} 18 19 # a request received as octets, as text: its method, target, header names 20 # and values and body each decoded as UTF-8 (SPEC.md HTTP-SRV-2); a bad one 21 # keeps its reason 22 def step.text(req: Http.Request) -> Http.Request: 23 match req: 24 case Http.Bad{why}: 25 Http.Bad{why} 26 case Http.Request{method, target, headers, body}: 27 Http.Request{Qs.octets.text(method), Qs.octets.text(target), 28 Qs.octets.headers(headers), Qs.octets.text(body)} 29 30 # a bad request is answered 400 with its reason and never reaches the 31 # handler; a good one goes to the handler as it was parsed 32 def step.of(req: Http.Request) -> Step: 33 match req: 34 case Http.Bad{why}: 35 Answer{Http.Reply{400, [], why}} 36 case Http.Request{method, target, headers, body}: 37 Call{Http.Request{method, target, headers, body}} 38 39 # the line logged for a failed effect: `ezhttp: : ` 40 def step.line(phase: String, why: String) -> String: 41 "ezhttp: " ++ phase ++ ": " ++ why 42 43 # a failed accept or recv loses the connection instead of ending the server 44 def step.got( 45 -A: Type, 46 phase: String, 47 res: Result<&1, &1, U32 & String, A> 48 ) -> Got: 49 match res: 50 case Fail{(code, why)}: 51 Lost{step.line(phase, why)} 52 case Done{value}: 53 Kept{value} 54 55 # a failed send loses the connection too; the bytes it did not send are dropped 56 def step.sent(res: Result<&1, &1, (U32 & String) & List<&2, U32>, Unit>) 57 -> Got: 58 match res: 59 case Fail{((code, why), left)}: 60 Lost{step.line("write", why)} 61 case Done{value}: 62 Kept{value} 63 64 # --- the replies and the handler's request (SPEC.md HTTP-SRV-8, 9) --- 65 66 # whether a reply is written with its head only: a reply to HEAD, or a 204 67 # or 304 (RFC 9110 §9.3.2, §15.3.5, §15.4.5) 68 def step.bare(+method: String, +status: U32) -> Bool: 69 Bool.or(String.eq(method, "HEAD"), Http.status.nocontent(status)) 70 71 # the body octets of a request, as they arrived; none for a bad one 72 def step.octets(req: Http.Request) -> String: 73 match req: 74 case Http.Bad{why}: 75 "" 76 case Http.Request{method, target, headers, body}: 77 body 78 79 # what an accept with a deadline brought: a connection, nothing before the 80 # deadline, or a lost accept with its line (SPEC.md HTTP-SRV-4) 81 type Accept<-A: Type> is Type: 82 Took{value: A} 83 Idle{} 84 Missed{line: String} 85 86 # a failed accept once it is known whether its deadline passed 87 def step.missed(-A: Type, +why: String, late: Bool) -> Accept: 88 match late: 89 case True{}: 90 Idle{} 91 case False{}: 92 Missed{step.line("accept", why)} 93 94 # an accept's outcome: ETIMEDOUT (110) is a deadline that passed 95 def step.accepted(-A: Type, res: Result<&1, &1, U32 & String, A>) 96 -> Accept: 97 match res: 98 case Fail{(+code, why)}: 99 step.missed(A, why, U32.is_eq(code, 110)) 100 case Done{value}: 101 Took{value} 102 103 # --- when a request has all arrived (SPEC.md HTTP-TRUST-2) --- 104 105 # a Content-Length that reads, or none 106 def step.clen(m: Maybe<&2, Nat>) -> Nat: 107 match m: 108 case None{}: 109 0n 110 case Some{n}: 111 n 112 113 # octets a request needs in all once its head has ended: the head, its blank 114 # line and the octets its Content-Length names (0 when it names none) 115 def step.need.of(c: Http.Cut) -> Maybe<&2, Nat>: 116 match c: 117 case Http.NoCut{}: 118 None{} 119 case Http.Cut{+before, after}: 120 Some{Nat.add(Nat.add(String.length(before), 4n), 121 step.clen(Http.digits.read(Http.first( 122 Http.head.values(String.lines(before), "content-length")))))} 123 124 # octets a request needs in all, none while its head has not ended 125 def step.need(+text: String) -> Maybe<&2, Nat>: 126 step.need.of(Http.divide(text, "\r\n\r\n"))