src/server.bend
emerging-ezhttp@0.9.0.0 · 359 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, one connection at a time. The handler returns IO; a pure
4# one is lifted. Every decision after an effect is step.bend's, so a failed
5# accept, recv or send loses that connection, logs one line and the server
6# takes the next one (HTTP-SRV-4).
7import Base
8import emerging-ezhttp@0.9.0.0/src/http.bend as Http
9import emerging-ezhttp@0.9.0.0/src/cors.bend as Cors
10import emerging-ezhttp@0.9.0.0/src/step.bend as Step
11
12# default recv budget for one request message
13def server.max() -> U32:
14 65536
15
16# listen on a host address and port, e.g. "127.0.0.1" or "0.0.0.0"
17# (RFC 9112 connection establishment stays at the socket)
18def server.listen(host: String, port: U32) -> IO(Listener):
19 IO.try(Listener, TCP.listen(host, port))
20
21# accept one connection; a failed accept is a lost connection
22def server.accept.un(
23 pair: Listener & Result<&1, &1, U32 & String, Socket>
24) -> IO(Listener & Step.Got<Socket>):
25 (lst, res) = pair
26 IO.pure(Listener & Step.Got<Socket>, (lst, Step.step.got(Socket, "accept", res)))
27
28def server.accept(listener: Listener) -> IO(Listener & Step.Got<Socket>):
29 IO.bind(
30 Listener & Result<&1, &1, U32 & String, Socket>,
31 Listener & Step.Got<Socket>,
32 TCP.accept(listener),
33 server.accept.un)
34
35# the short-body refusal, so a partial recv can continue
36def server.read.why() -> String:
37 "the request body stopped before its Content-Length"
38
39# whether this refusal is a body that has not arrived yet
40def server.read.short(why: String) -> Bool:
41 String.eq(why, server.read.why())
42
43# a parsed request still wants more bytes
44def server.read.need(req: Http.Request) -> Bool:
45 match req:
46 case Http.Bad{why}:
47 server.read.short(why)
48 case Http.Request{method, target, headers, body}:
49 False{}
50
51# how many extra recvs to chase a Content-Length
52def server.read.fuel() -> Nat:
53 64n
54
55# parse what has arrived
56def server.read.done(sock: Socket, got: String) -> IO(Socket & Step.Got<Http.Request>):
57 IO.pure(Socket & Step.Got<Http.Request>, (sock, Step.Kept{Http.ask(got)}))
58
59# stop on a finished message, or ask for another recv
60def server.read.next(
61 sock: Socket,
62 got: String,
63 req: Http.Request,
64 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>),
65 more: Bool
66) -> IO(Socket & Step.Got<Http.Request>):
67 match more:
68 case False{}:
69 IO.pure(Socket & Step.Got<Http.Request>, (sock, Step.Kept{req}))
70 case True{}:
71 rest(sock)(got)
72
73# append a chunk and decide whether it completes the request
74def server.read.grew(
75 sock: Socket,
76 +got: String,
77 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>)
78) -> IO(Socket & Step.Got<Http.Request>):
79 +req = Http.ask(got)
80 server.read.next(sock, got, req, rest, server.read.need(req))
81
82# peer closed, or the chunk extends the buffer
83def server.read.empty(
84 sock: Socket,
85 got: String,
86 chunk: String,
87 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>),
88 closed: Bool
89) -> IO(Socket & Step.Got<Http.Request>):
90 match closed:
91 case True{}:
92 server.read.done(sock, got)
93 case False{}:
94 server.read.grew(sock, got ++ chunk, rest)
95
96# one chunk of the request message
97def server.read.use(
98 sock: Socket,
99 got: String,
100 +chunk: String,
101 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>)
102) -> IO(Socket & Step.Got<Http.Request>):
103 server.read.empty(sock, got, chunk, rest, String.eq(chunk, ""))
104
105# None{} is the peer closing its write side; Some is the bytes that arrived
106def server.read.got(
107 sock: Socket,
108 got: String,
109 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>),
110 chunk: Maybe<&1, String>
111) -> IO(Socket & Step.Got<Http.Request>):
112 match chunk:
113 case None{}:
114 server.read.done(sock, got)
115 case Some{text}:
116 server.read.use(sock, got, text, rest)
117
118# the bytes from one recv; a failed recv loses the connection
119def server.read.pass(
120 sock: Socket,
121 got: String,
122 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>),
123 chunk: Step.Got<Maybe<&1, String>>
124) -> IO(Socket & Step.Got<Http.Request>):
125 match chunk:
126 case Step.Lost{line}:
127 IO.pure(Socket & Step.Got<Http.Request>, (sock, Step.Lost{line}))
128 case Step.Kept{text}:
129 server.read.got(sock, got, rest, text)
130
131# unpack recv's socket and result
132def server.read.un(
133 got: String,
134 rest: Socket -> String -> IO(Socket & Step.Got<Http.Request>),
135 pair: Socket & Result<&1, &1, U32 & String, Maybe<&1, String>>
136) -> IO(Socket & Step.Got<Http.Request>):
137 (sock, r) = pair
138 server.read.pass(sock, got, rest,
139 Step.step.got(Maybe<&1, String>, "read", r))
140
141# recv until Content-Length is met, the peer closes, or fuel runs out.
142# fuel is first so the recursive call is a decreasing self-call.
143def server.read.go(fuel: Nat, sock: Socket, got: String) -> IO(Socket & Step.Got<Http.Request>):
144 match fuel:
145 case 0n:
146 server.read.done(sock, got)
147 case 1n+f:
148 IO.bind(
149 Socket & Result<&1, &1, U32 & String, Maybe<&1, String>>,
150 Socket & Step.Got<Http.Request>,
151 TCP.recv(sock, server.max()),
152 pair => server.read.un(got, s => g => server.read.go(f, s, g), pair))
153
154# recv then parse an HTTP request (RFC 9112 §3)
155def server.read(sock: Socket) -> IO(Socket & Step.Got<Http.Request>):
156 server.read.go(server.read.fuel(), sock, "")
157
158# write a structured response; a failed send loses the connection
159def server.write.un(
160 pair: Socket & Result<&1, &1, (U32 & String) & String, Unit>
161) -> IO(Socket & Step.Got<Unit>):
162 (sock, res) = pair
163 IO.pure(Socket & Step.Got<Unit>, (sock, Step.step.sent(res)))
164
165def server.write(sock: Socket, reply: Http.Reply) -> IO(Socket & Step.Got<Unit>):
166 IO.bind(
167 Socket & Result<&1, &1, (U32 & String) & String, Unit>,
168 Socket & Step.Got<Unit>,
169 TCP.send(sock, Http.respond.of(reply)),
170 server.write.un)
171
172# turn a bad request into 400; otherwise call the handler
173def server.dispatch(~handle: Http.Request -> Http.Reply, req: Http.Request)
174 -> Http.Reply:
175 match req:
176 case Http.Bad{why}:
177 Http.Reply{400, [], why}
178 case Http.Request{method, target, headers, body}:
179 handle(Http.Request{method, target, headers, body})
180
181# apply HEAD body stripping once the method token is known
182def server.reply.built(~handle: Http.Request -> Http.Reply, method: String,
183 req: Http.Request) -> Http.Reply:
184 Http.reply.for(method, server.dispatch(handle, req))
185
186# HEAD responses are written with an empty body (RFC 9110 §9.3.2)
187def server.reply(~handle: Http.Request -> Http.Reply, req: Http.Request)
188 -> Http.Reply:
189 match req:
190 case Http.Bad{why}:
191 server.dispatch(handle, Http.Bad{why})
192 case Http.Request{+method, target, headers, body}:
193 server.reply.built(handle, method,
194 Http.Request{method, target, headers, body})
195
196# CORS preflight or simple headers, after HEAD bodies are dropped
197def server.cors_reply(
198 ~handle: Http.Request -> Http.Reply,
199 cfg: Cors.Cfg,
200 +req: Http.Request
201) -> Http.Reply:
202 Cors.cors.on(cfg, req, server.reply(handle, req))
203
204# a lost connection logs its line and closes
205def server.lose(sock: Socket, line: String) -> IO(Unit):
206 IO.bind(Unit, Unit, IO.print_err(line), _u => Socket.close(sock))
207
208# after the write: close, logging first when the send failed
209def server.sent.got(sock: Socket, got: Step.Got<Unit>) -> IO(Unit):
210 match got:
211 case Step.Lost{line}:
212 server.lose(sock, line)
213 case Step.Kept{_u}:
214 Socket.close(sock)
215
216def server.sent(pair: Socket & Step.Got<Unit>) -> IO(Unit):
217 (sock, got) = pair
218 server.sent.got(sock, got)
219
220# write a reply shaped for the request's method (HTTP-SRV-3), then close
221def server.answer(sock: Socket, method: String, reply: Http.Reply) -> IO(Unit):
222 IO.bind(Socket & Step.Got<Unit>, Unit,
223 server.write(sock, Http.reply.for(method, reply)), server.sent)
224
225# answer now, or call the handler: step.bend decides which (HTTP-SRV-1, 2)
226def server.step(
227 ~handle: Http.Request -> IO(Http.Reply),
228 sock: Socket,
229 +method: String,
230 step: Step.Step
231) -> IO(Unit):
232 match step:
233 case Step.Answer{reply}:
234 server.answer(sock, method, reply)
235 case Step.Call{req}:
236 IO.bind(Http.Reply, Unit, handle(req),
237 reply => server.answer(sock, method, reply))
238
239# after the read: a lost connection, or a request to step
240def server.exchange.got(
241 ~handle: Http.Request -> IO(Http.Reply),
242 sock: Socket,
243 got: Step.Got<Http.Request>
244) -> IO(Unit):
245 match got:
246 case Step.Lost{line}:
247 server.lose(sock, line)
248 case Step.Kept{+req}:
249 server.step(handle, sock, Http.method.of(req), Step.step.of(req))
250
251def server.exchange.un(
252 ~handle: Http.Request -> IO(Http.Reply),
253 pair: Socket & Step.Got<Http.Request>
254) -> IO(Unit):
255 (sock, got) = pair
256 server.exchange.got(handle, sock, got)
257
258# one connection: read, decide, maybe handle, write, close
259def server.exchange(
260 ~handle: Http.Request -> IO(Http.Reply),
261 sock: Socket
262) -> IO(Unit):
263 IO.bind(Socket & Step.Got<Http.Request>, Unit, server.read(sock),
264 pair => server.exchange.un(handle, pair))
265
266# how long a failed accept waits before the next, so EMFILE does not spin
267def server.pause() -> U32:
268 100
269
270# after accept: serve the connection, or log a failed accept and pause, then
271# call `again` with the listener either way. `again` is a runtime closure so
272# it can carry the fuel still left.
273def server.loop.got(
274 ~handle: Http.Request -> IO(Http.Reply),
275 again: Listener -> IO(Unit),
276 lst: Listener,
277 got: Step.Got<Socket>
278) -> IO(Unit):
279 match got:
280 case Step.Lost{line}:
281 IO.bind(Unit, Unit, IO.print_err(line), _u =>
282 IO.bind(Unit, Unit, IO.sleep(server.pause()), _v => again(lst)))
283 case Step.Kept{sock}:
284 IO.bind(Unit, Unit, server.exchange(handle, sock), _u => again(lst))
285
286def server.loop.cont(
287 ~handle: Http.Request -> IO(Http.Reply),
288 again: Listener -> IO(Unit),
289 pair: Listener & Step.Got<Socket>
290) -> IO(Unit):
291 (lst, got) = pair
292 server.loop.got(handle, again, lst, got)
293
294# fuel-bounded accept loop (self-recursive); a failed accept spends fuel too
295def server.loop.go(
296 ~handle: Http.Request -> IO(Http.Reply),
297 fuel: Nat,
298 listener: Listener
299) -> IO(Unit):
300 match fuel:
301 case 0n:
302 Listener.close(listener)
303 case 1n+f:
304 IO.bind(Listener & Step.Got<Socket>, Unit, server.accept(listener),
305 pair => server.loop.cont(handle,
306 lst => server.loop.go(handle, f, lst), pair))
307
308# listen and serve up to `limit` connections with an IO handler
309def server.serve_io(
310 ~handle: Http.Request -> IO(Http.Reply),
311 host: String,
312 port: U32,
313 limit: U32
314) -> IO(Unit):
315 IO.bind(Listener, Unit, server.listen(host, port),
316 lst => server.loop.go(handle, U32.to_nat(limit), lst))
317
318# a pure handler as an IO one
319def server.lift(
320 ~handle: Http.Request -> Http.Reply,
321 req: Http.Request
322) -> IO(Http.Reply):
323 IO.pure(Http.Reply, handle(req))
324
325# listen and serve up to `limit` connections
326def server.serve(
327 ~handle: Http.Request -> Http.Reply,
328 host: String,
329 port: U32,
330 limit: U32
331) -> IO(Unit):
332 server.serve_io(~(req => server.lift(handle, req)), host, port, limit)
333
334# serve exactly one connection then stop
335def server.once(
336 ~handle: Http.Request -> Http.Reply,
337 host: String,
338 port: U32
339) -> IO(Unit):
340 server.serve(handle, host, port, 1)
341
342# the accept loop with no fuel: it runs until the process is stopped
343# (HTTP-TRUST-2)
344@unsafe def server.forever.go(
345 ~handle: Http.Request -> IO(Http.Reply),
346 listener: Listener
347) -> IO(Unit):
348 IO.bind(Listener & Step.Got<Socket>, Unit, server.accept(listener),
349 pair => server.loop.cont(handle,
350 lst => server.forever.go(handle, lst), pair))
351
352# listen and serve with an IO handler until the process is stopped
353@unsafe def server.serve_forever(
354 ~handle: Http.Request -> IO(Http.Reply),
355 host: String,
356 port: U32
357) -> IO(Unit):
358 IO.bind(Listener, Unit, server.listen(host, port),
359 lst => server.forever.go(handle, lst))