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