1 # ezhttp/src/server: listen, accept, parse one request, write one response. 2 # Uses Base TCP (no TLS in v0). One exchange per connection with 3 # Connection: close. Handler is a pure Request -> Reply function. 4 import Base 5 import ./http.bend as Http 6 import ./cors.bend as Cors 7 8 # default recv budget for one request message 9 def server.max() -> U32: 10 65536 11 12 # listen on a host address and port, e.g. "127.0.0.1" or "0.0.0.0" 13 # (RFC 9112 connection establishment stays at the socket) 14 def server.listen(host: String, port: U32) -> IO(Listener): 15 IO.try(Listener, TCP.listen(host, port)) 16 17 # unpack accept: listener stays, socket from the Result 18 def server.accept.of( 19 lst: Listener, 20 res: Result<&1, &1, U32 & String, Socket> 21 ) -> IO(Listener & Socket): 22 do IO: 23 sock : Socket <- IO.pass(Socket, res) 24 IO.pure(Listener & Socket, (lst, sock)) 25 26 def server.accept.un(pair: Listener & Result<&1, &1, U32 & String, Socket>) 27 -> IO(Listener & Socket): 28 (lst, res) = pair 29 server.accept.of(lst, res) 30 31 def server.accept(listener: Listener) -> IO(Listener & Socket): 32 IO.bind( 33 Listener & Result<&1, &1, U32 & String, Socket>, 34 Listener & Socket, 35 TCP.accept(listener), 36 server.accept.un) 37 38 # the short-body refusal, so a partial recv can continue 39 def server.read.why() -> String: 40 "the request body stopped before its Content-Length" 41 42 # whether this refusal is a body that has not arrived yet 43 def server.read.short(why: String) -> Bool: 44 String.eq(why, server.read.why()) 45 46 # a parsed request still wants more bytes 47 def server.read.need(req: Http.Request) -> Bool: 48 match req: 49 case Http.Bad{why}: 50 server.read.short(why) 51 case Http.Request{method, target, headers, body}: 52 False{} 53 54 # how many extra recvs to chase a Content-Length 55 def server.read.fuel() -> Nat: 56 64n 57 58 # parse what has arrived 59 def server.read.done(sock: Socket, got: String) -> IO(Socket & Http.Request): 60 IO.pure(Socket & Http.Request, (sock, Http.ask(got))) 61 62 # stop on a finished message, or ask for another recv 63 def server.read.next( 64 sock: Socket, 65 got: String, 66 req: Http.Request, 67 rest: Socket -> String -> IO(Socket & Http.Request), 68 more: Bool 69 ) -> IO(Socket & Http.Request): 70 match more: 71 case False{}: 72 IO.pure(Socket & Http.Request, (sock, req)) 73 case True{}: 74 rest(sock)(got) 75 76 # append a chunk and decide whether it completes the request 77 def server.read.grew( 78 sock: Socket, 79 +got: String, 80 rest: Socket -> String -> IO(Socket & Http.Request) 81 ) -> IO(Socket & Http.Request): 82 +req = Http.ask(got) 83 server.read.next(sock, got, req, rest, server.read.need(req)) 84 85 # peer closed, or the chunk extends the buffer 86 def server.read.empty( 87 sock: Socket, 88 got: String, 89 chunk: String, 90 rest: Socket -> String -> IO(Socket & Http.Request), 91 closed: Bool 92 ) -> IO(Socket & Http.Request): 93 match closed: 94 case True{}: 95 server.read.done(sock, got) 96 case False{}: 97 server.read.grew(sock, got ++ chunk, rest) 98 99 # one chunk of the request message 100 def server.read.use( 101 sock: Socket, 102 got: String, 103 +chunk: String, 104 rest: Socket -> String -> IO(Socket & Http.Request) 105 ) -> IO(Socket & Http.Request): 106 server.read.empty(sock, got, chunk, rest, String.eq(chunk, "")) 107 108 # the bytes from one recv 109 def server.read.pass( 110 sock: Socket, 111 got: String, 112 rest: Socket -> String -> IO(Socket & Http.Request), 113 r: Result<&1, &1, U32 & String, String> 114 ) -> IO(Socket & Http.Request): 115 IO.bind(String, Socket & Http.Request, IO.pass(String, r), 116 chunk => server.read.use(sock, got, chunk, rest)) 117 118 # unpack recv's socket and result 119 def server.read.un( 120 got: String, 121 rest: Socket -> String -> IO(Socket & Http.Request), 122 pair: Socket & Result<&1, &1, U32 & String, String> 123 ) -> IO(Socket & Http.Request): 124 (sock, r) = pair 125 server.read.pass(sock, got, rest, r) 126 127 # recv until Content-Length is met, the peer closes, or fuel runs out. 128 # fuel is first so the recursive call is a decreasing self-call. 129 def server.read.go(fuel: Nat, sock: Socket, got: String) -> IO(Socket & Http.Request): 130 match fuel: 131 case 0n: 132 server.read.done(sock, got) 133 case 1n+f: 134 IO.bind( 135 Socket & Result<&1, &1, U32 & String, String>, 136 Socket & Http.Request, 137 TCP.recv(sock, server.max()), 138 pair => server.read.un(got, s => g => server.read.go(f, s, g), pair)) 139 140 # recv then parse an HTTP request (RFC 9112 §3) 141 def server.read(sock: Socket) -> IO(Socket & Http.Request): 142 server.read.go(server.read.fuel(), sock, "") 143 144 # write a structured response and keep the socket 145 def server.write.res(sock: Socket, r: Result<&1, &1, U32 & String, Unit>) 146 -> IO(Socket): 147 IO.bind(Unit, Socket, IO.pass(Unit, r), _u => IO.pure(Socket, sock)) 148 149 def server.write.un(pair: Socket & Result<&1, &1, U32 & String, Unit>) 150 -> IO(Socket): 151 (s, r) = pair 152 server.write.res(s, r) 153 154 def server.write(sock: Socket, reply: Http.Reply) -> IO(Socket): 155 IO.bind( 156 Socket & Result<&1, &1, U32 & String, Unit>, 157 Socket, 158 TCP.send(sock, Http.respond.of(reply)), 159 server.write.un) 160 161 # turn a bad request into 400; otherwise call the handler 162 def server.dispatch(~handle: Http.Request -> Http.Reply, req: Http.Request) 163 -> Http.Reply: 164 match req: 165 case Http.Bad{why}: 166 Http.Reply{400, [], why} 167 case Http.Request{method, target, headers, body}: 168 handle(Http.Request{method, target, headers, body}) 169 170 # apply HEAD body stripping once the method token is known 171 def server.reply.built(~handle: Http.Request -> Http.Reply, method: String, 172 req: Http.Request) -> Http.Reply: 173 Http.reply.for(method, server.dispatch(handle, req)) 174 175 # HEAD responses are written with an empty body (RFC 9110 §9.3.2) 176 def server.reply(~handle: Http.Request -> Http.Reply, req: Http.Request) 177 -> Http.Reply: 178 match req: 179 case Http.Bad{why}: 180 server.dispatch(handle, Http.Bad{why}) 181 case Http.Request{+method, target, headers, body}: 182 server.reply.built(handle, method, 183 Http.Request{method, target, headers, body}) 184 185 # CORS preflight or simple headers, after HEAD bodies are dropped 186 def server.cors_reply( 187 ~handle: Http.Request -> Http.Reply, 188 cfg: Cors.Cfg, 189 +req: Http.Request 190 ) -> Http.Reply: 191 Cors.cors.on(cfg, req, server.reply(handle, req)) 192 193 # after read: write the handler's reply and close 194 def server.exchange.go( 195 ~handle: Http.Request -> Http.Reply, 196 sock: Socket, 197 req: Http.Request 198 ) -> IO(Unit): 199 IO.bind(Socket, Unit, server.write(sock, server.reply(handle, req)), 200 Socket.close) 201 202 def server.exchange.un( 203 ~handle: Http.Request -> Http.Reply, 204 pair: Socket & Http.Request 205 ) -> IO(Unit): 206 (s, req) = pair 207 server.exchange.go(handle, s, req) 208 209 # one connection: read, handle, write, close 210 def server.exchange(~handle: Http.Request -> Http.Reply, sock: Socket) 211 -> IO(Unit): 212 IO.bind(Socket & Http.Request, Unit, server.read(sock), 213 pair => server.exchange.un(handle, pair)) 214 215 # continue after accept: exchange, then call `again` with the listener. 216 # `again` is a runtime closure so it can carry the fuel still left. 217 def server.loop.cont( 218 ~handle: Http.Request -> Http.Reply, 219 again: Listener -> IO(Unit), 220 pair: Listener & Socket 221 ) -> IO(Unit): 222 (lst, sock) = pair 223 IO.bind(Unit, Unit, server.exchange(handle, sock), 224 _u => again(lst)) 225 226 # fuel-bounded accept loop (self-recursive) 227 def server.loop.go( 228 ~handle: Http.Request -> Http.Reply, 229 fuel: Nat, 230 listener: Listener 231 ) -> IO(Unit): 232 match fuel: 233 case 0n: 234 IO.pure(Unit, Unit{}) 235 case 1n+f: 236 IO.bind(Listener & Socket, Unit, server.accept(listener), 237 pair => server.loop.cont(handle, 238 lst => server.loop.go(handle, f, lst), pair)) 239 240 # listen and serve up to `limit` connections (fuel from the limit) 241 def server.loop( 242 ~handle: Http.Request -> Http.Reply, 243 listener: Listener, 244 limit: U32 245 ) -> IO(Unit): 246 server.loop.go(handle, U32.to_nat(limit), listener) 247 248 # listen and serve up to `limit` connections 249 def server.serve( 250 ~handle: Http.Request -> Http.Reply, 251 host: String, 252 port: U32, 253 limit: U32 254 ) -> IO(Unit): 255 IO.bind(Listener, Unit, server.listen(host, port), 256 lst => server.loop(handle, lst, limit)) 257 258 # serve exactly one connection then stop 259 def server.once.cont( 260 ~handle: Http.Request -> Http.Reply, 261 pair: Listener & Socket 262 ) -> IO(Unit): 263 (lst, sock) = pair 264 IO.bind(Unit, Unit, server.exchange(handle, sock), 265 _u => Listener.close(lst)) 266 267 def server.once( 268 ~handle: Http.Request -> Http.Reply, 269 host: String, 270 port: U32 271 ) -> IO(Unit): 272 IO.bind(Listener, Unit, server.listen(host, port), lst => 273 IO.bind(Listener & Socket, Unit, server.accept(lst), 274 pair => server.once.cont(handle, pair)))