src/server.bend
emerging-ezhttp@0.8.0.0 · 274 lines · 0 definitions
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.
4import Base
5import emerging-ezhttp@0.8.0.0/src/http.bend as Http
6import emerging-ezhttp@0.8.0.0/src/cors.bend as Cors
7
8# default recv budget for one request message
9def 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)
14def 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
18def server.accept.of(
19 lst: Listener,
20 res: Result<&1, &1, U32 & String, Socket>
21) -> IO(Listener & Socket):
22 do IO<Listener & Socket>:
23 sock : Socket <- IO.pass(Socket, res)
24 IO.pure(Listener & Socket, (lst, sock))
25
26def 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
31def 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
39def 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
43def server.read.short(why: String) -> Bool:
44 String.eq(why, server.read.why())
45
46# a parsed request still wants more bytes
47def 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
55def server.read.fuel() -> Nat:
56 64n
57
58# parse what has arrived
59def 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
63def 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
77def 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
86def 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
100def 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
109def 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
119def 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.
129def 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)
141def 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
145def 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
149def 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
154def 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
162def 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
171def 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)
176def 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
186def 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
194def 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
202def 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
210def 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.
217def 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)
227def 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)
241def 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
249def 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
259def 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
267def 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)))