1 # ezhttp/src/wire: one request out and one response back. TLS and DNS stay in 2 # the effect (OpenSSL via dlopen); this module does not re-specify TLS. 3 # 4 # Spec: scheme, host, port each on their own line; everything after the third 5 # newline is the request verbatim; the scheme line may carry a timeout 6 # (wirespec.bend). Answer: status on the first line, then text 7 # ("0" and the raw response, or a non-zero code and the reason). 8 # 9 # The effect opens a connection and sends the request, then answers one read 10 # at a time; when the response is whole is wirespec.bend's decision 11 # (wire.read.next), so a server that keeps the connection open after a 12 # complete response is not waited on (SPEC.md HTTP-CLI-5, HTTP-CLI-6). 13 import Base 14 import ./wirespec.bend as Spec 15 16 # connect, send the request a spec carries, and answer a connection number 17 def ezwire.open(spec: String) -> IO(String): 18 import "./effs/wire.c" 19 import "./effs/wire.js" 20 21 # the next octets a connection has received, "" once the server has closed 22 def ezwire.read(conn: U32) -> IO(String): 23 import "./effs/wire.c" 24 import "./effs/wire.js" 25 26 # close a connection 27 def ezwire.close(conn: U32) -> IO(Unit): 28 import "./effs/wire.c" 29 import "./effs/wire.js" 30 31 # the spec the effect reads (wirespec.bend) 32 def wire.spec(secure: Bool, host: String, port: U32, req: String) -> String: 33 Spec.wire.spec(secure, host, port, req) 34 35 # the most reads one exchange makes 36 def wire.reads() -> Nat: 37 65536n 38 39 # a read's outcome: read again, or the answer 40 def wire.loop.on(r: Spec.Read, more: String -> IO(String)) -> IO(String): 41 match r: 42 case Spec.Again{acc}: 43 more(acc) 44 case Spec.Heard{answer}: 45 IO.pure(String, answer) 46 47 # read until wirespec.bend says the answer is whole 48 def wire.loop(fuel: Nat, +conn: U32, +method: String, acc: String) 49 -> IO(String): 50 match fuel: 51 case 0n: 52 IO.pure(String, Spec.wire.spent()) 53 case 1n+p: 54 do IO: 55 +got : String <- ezwire.read(conn) 56 wire.loop.on(Spec.wire.read.next(method, acc, got), 57 next => wire.loop(p, conn, method, next)) 58 59 # read the answer on an open connection, then close it 60 def wire.hear(+conn: U32, method: String) -> IO(String): 61 do IO: 62 +answer : String <- wire.loop(wire.reads(), conn, method, "") 63 _done : Unit <- ezwire.close(conn) 64 IO.pure(String, answer) 65 66 # an open's outcome: read from the connection, or the answer 67 def wire.after(o: Spec.Opened, method: String) -> IO(String): 68 match o: 69 case Spec.Refused{answer}: 70 IO.pure(String, answer) 71 case Spec.Open{conn}: 72 wire.hear(conn, method) 73 74 # a request with this method, sent as a spec says, and its whole answer 75 def wire.run(method: String, spec: String) -> IO(String): 76 do IO: 77 +got : String <- ezwire.open(spec) 78 wire.after(Spec.wire.opened(got), method) 79 80 # a request sent to a host and its whole answer read back 81 def wire.talk(secure: Bool, host: String, port: U32, method: String, 82 req: String) -> IO(String): 83 wire.run(method, wire.spec(secure, host, port, req)) 84 85 # the same exchange, bounded by a timeout in milliseconds 86 def wire.talk.timed(secure: Bool, ms: U32, host: String, port: U32, 87 method: String, req: String) -> IO(String): 88 wire.run(method, Spec.wire.spec.timed(secure, ms, host, port, req))