carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1#
2#
3# Nim's Runtime Library
4# (c) Copyright 2015 Dominik Picheta
5#
6# See the file "copying.txt", included in this
7# distribution, for details about the copyright.
8#
9
10## todo-nim's copy of Nim 2.2.12's `std/asynchttpserver`, changed in one
11## place: a chunked request body is held to `maxBody` like a
12## `Content-Length` one. Upstream passes each chunk's declared size straight
13## to `recv`, which allocates that much before any data arrives, and never
14## limits the total, so one chunked POST could exhaust the instance's
15## memory. Look for "todo-nim" below.
16##
17## This module implements a high performance asynchronous HTTP server.
18##
19## This HTTP server has not been designed to be used in production, but
20## for testing applications locally. Because of this, when deploying your
21## application in production you should use a reverse proxy (for example nginx)
22## instead of allowing users to connect directly to this server.
23
24runnableExamples("-r:off"):
25 # This example will create an HTTP server on an automatically chosen port.
26 # It will respond to all requests with a `200 OK` response code and "Hello World"
27 # as the response body.
28 import std/asyncdispatch
29 proc main {.async.} =
30 var server = newAsyncHttpServer()
31 proc cb(req: Request) {.async.} =
32 echo (req.reqMethod, req.url, req.headers)
33 let headers = {"Content-type": "text/plain; charset=utf-8"}
34 await req.respond(Http200, "Hello World", headers.newHttpHeaders())
35
36 server.listen(Port(0)) # or Port(8080) to hardcode the standard HTTP port.
37 let port = server.getPort
38 echo "test this with: curl localhost:" & $port.uint16 & "/"
39 while true:
40 if server.shouldAcceptRequest():
41 await server.acceptRequest(cb)
42 else:
43 # too many concurrent connections, `maxFDs` exceeded
44 # wait 500ms for FDs to be closed
45 await sleepAsync(500)
46
47 waitFor main()
48
49import std/[asyncnet, asyncdispatch, parseutils, uri, strutils]
50import std/httpcore
51from std/nativesockets import getLocalAddr, Domain, AF_INET, AF_INET6
52import std/private/since
53
54when defined(nimPreviewSlimSystem):
55 import std/assertions
56
57export httpcore except parseHeader
58
59const
60 maxLine = 8*1024
61
62# TODO: If it turns out that the decisions that asynchttpserver makes
63# explicitly, about whether to close the client sockets or upgrade them are
64# wrong, then add a return value which determines what to do for the callback.
65# Also, maybe move `client` out of `Request` object and into the args for
66# the proc.
67type
68 Request* = object
69 client*: AsyncSocket # TODO: Separate this into a Response object?
70 reqMethod*: HttpMethod
71 headers*: HttpHeaders
72 protocol*: tuple[orig: string, major, minor: int]
73 url*: Uri
74 hostname*: string ## The hostname of the client that made the request.
75 body*: string
76
77 AsyncHttpServer* = ref object
78 socket: AsyncSocket
79 reuseAddr: bool
80 reusePort: bool
81 maxBody: int ## The maximum content-length that will be read for the body.
82 maxFDs: int
83
84proc getPort*(self: AsyncHttpServer): Port {.since: (1, 5, 1).} =
85 ## Returns the port `self` was bound to.
86 ##
87 ## Useful for identifying what port `self` is bound to, if it
88 ## was chosen automatically, for example via `listen(Port(0))`.
89 runnableExamples:
90 from std/nativesockets import Port
91 let server = newAsyncHttpServer()
92 server.listen(Port(0))
93 assert server.getPort.uint16 > 0
94 server.close()
95 result = getLocalAddr(self.socket)[1]
96
97proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
98 maxBody = 8388608): AsyncHttpServer =
99 ## Creates a new `AsyncHttpServer` instance.
100 result = AsyncHttpServer(reuseAddr: reuseAddr, reusePort: reusePort, maxBody: maxBody)
101
102proc addHeaders(msg: var string, headers: HttpHeaders) =
103 for k, v in headers:
104 msg.add(k & ": " & v & "\c\L")
105
106proc sendHeaders*(req: Request, headers: HttpHeaders): Future[void] =
107 ## Sends the specified headers to the requesting client.
108 var msg = ""
109 addHeaders(msg, headers)
110 return req.client.send(msg)
111
112proc respond*(req: Request, code: HttpCode, content: string,
113 headers: HttpHeaders = nil): Future[void] =
114 ## Responds to the request with the specified `HttpCode`, headers and
115 ## content.
116 ##
117 ## This procedure will **not** close the client socket.
118 ##
119 ## Example:
120 ## ```Nim
121 ## import std/json
122 ## proc handler(req: Request) {.async.} =
123 ## if req.url.path == "/hello-world":
124 ## let msg = %* {"message": "Hello World"}
125 ## let headers = newHttpHeaders([("Content-Type","application/json")])
126 ## await req.respond(Http200, $msg, headers)
127 ## else:
128 ## await req.respond(Http404, "Not Found")
129 ## ```
130 var msg = "HTTP/1.1 " & $code & "\c\L"
131
132 if headers != nil:
133 msg.addHeaders(headers)
134
135 # If the headers did not contain a Content-Length use our own
136 if headers.isNil() or not headers.hasKey("Content-Length"):
137 msg.add("Content-Length: ")
138 # this particular way saves allocations:
139 msg.addInt content.len
140 msg.add "\c\L"
141
142 msg.add "\c\L"
143 msg.add(content)
144 result = req.client.send(msg)
145
146proc respondError(req: Request, code: HttpCode): Future[void] =
147 ## Responds to the request with the specified `HttpCode`.
148 let content = $code
149 var msg = "HTTP/1.1 " & content & "\c\L"
150
151 msg.add("Content-Length: " & $content.len & "\c\L\c\L")
152 msg.add(content)
153 result = req.client.send(msg)
154
155proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
156 result = default(tuple[orig: string, major, minor: int])
157 var i = protocol.skipIgnoreCase("HTTP/")
158 if i != 5:
159 raise newException(ValueError, "Invalid request protocol. Got: " &
160 protocol)
161 result.orig = protocol
162 i.inc protocol.parseSaturatedNatural(result.major, i)
163 if i < protocol.len: inc i # Skip .
164 i.inc protocol.parseSaturatedNatural(result.minor, i)
165
166proc sendStatus(client: AsyncSocket, status: string): Future[void] =
167 client.send("HTTP/1.1 " & status & "\c\L\c\L")
168
169func hasChunkedEncoding(request: Request): bool =
170 ## Searches for a chunked transfer encoding
171 const transferEncoding = "Transfer-Encoding"
172
173 if request.headers.hasKey(transferEncoding):
174 for encoding in seq[string](request.headers[transferEncoding]):
175 if "chunked" == encoding.strip:
176 # Returns true if it is both an HttpPost and has chunked encoding
177 return request.reqMethod == HttpPost
178 return false
179
180proc processRequest(
181 server: AsyncHttpServer,
182 req: FutureVar[Request],
183 client: AsyncSocket,
184 address: sink string,
185 lineFut: FutureVar[string],
186 callback: proc (request: Request): Future[void] {.closure, gcsafe.},
187): Future[bool] {.async.} =
188
189 # Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
190 template request(): Request =
191 req.mget()
192
193 # GET /path HTTP/1.1
194 # Header: val
195 # \n
196 request.headers.clear()
197 request.body = ""
198 when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
199 request.hostname = address
200 else:
201 request.hostname.shallowCopy(address)
202 assert client != nil
203 request.client = client
204
205 # We should skip at least one empty line before the request
206 # https://tools.ietf.org/html/rfc7230#section-3.5
207 for i in 0..1:
208 lineFut.mget().setLen(0)
209 lineFut.clean()
210 await client.recvLineInto(lineFut, maxLength = maxLine) # TODO: Timeouts.
211
212 if lineFut.mget == "":
213 client.close()
214 return false
215
216 if lineFut.mget.len > maxLine:
217 await request.respondError(Http413)
218 client.close()
219 return false
220 if lineFut.mget != "\c\L":
221 break
222
223 # First line - GET /path HTTP/1.1
224 var i = 0
225 for linePart in lineFut.mget.split(' '):
226 case i
227 of 0:
228 case linePart
229 of "GET": request.reqMethod = HttpGet
230 of "POST": request.reqMethod = HttpPost
231 of "HEAD": request.reqMethod = HttpHead
232 of "PUT": request.reqMethod = HttpPut
233 of "DELETE": request.reqMethod = HttpDelete
234 of "PATCH": request.reqMethod = HttpPatch
235 of "OPTIONS": request.reqMethod = HttpOptions
236 of "CONNECT": request.reqMethod = HttpConnect
237 of "TRACE": request.reqMethod = HttpTrace
238 else:
239 asyncCheck request.respondError(Http400)
240 return true # Retry processing of request
241 of 1:
242 try:
243 parseUri(linePart, request.url)
244 except ValueError:
245 asyncCheck request.respondError(Http400)
246 return true
247 of 2:
248 try:
249 request.protocol = parseProtocol(linePart)
250 except ValueError:
251 asyncCheck request.respondError(Http400)
252 return true
253 else:
254 await request.respondError(Http400)
255 return true
256 inc i
257
258 # Headers
259 while true:
260 i = 0
261 lineFut.mget.setLen(0)
262 lineFut.clean()
263 await client.recvLineInto(lineFut, maxLength = maxLine)
264
265 if lineFut.mget == "":
266 client.close(); return false
267 if lineFut.mget.len > maxLine:
268 await request.respondError(Http413)
269 client.close(); return false
270 if lineFut.mget == "\c\L": break
271 let (key, value) = parseHeader(lineFut.mget)
272 request.headers[key] = value
273 # Ensure the client isn't trying to DoS us.
274 if request.headers.len > headerLimit:
275 await client.sendStatus("400 Bad Request")
276 request.client.close()
277 return false
278
279 if request.reqMethod == HttpPost:
280 # Check for Expect header
281 if request.headers.hasKey("Expect"):
282 if "100-continue" in request.headers["Expect"]:
283 await client.sendStatus("100 Continue")
284 else:
285 await client.sendStatus("417 Expectation Failed")
286
287 # Read the body
288 # - Check for Content-length header
289 if request.headers.hasKey("Content-Length"):
290 var contentLength = 0
291 if parseSaturatedNatural(request.headers["Content-Length"], contentLength) == 0:
292 await request.respond(Http400, "Bad Request. Invalid Content-Length.")
293 return true
294 else:
295 if contentLength > server.maxBody:
296 await request.respondError(Http413)
297 return false
298 request.body = await client.recv(contentLength)
299 if request.body.len != contentLength:
300 await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
301 return true
302 elif hasChunkedEncoding(request):
303 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding
304 var sizeOrData = 0
305 var bytesToRead = 0
306 request.body = ""
307
308 while true:
309 lineFut.mget.setLen(0)
310 lineFut.clean()
311
312 # The encoding format alternates between specifying a number of bytes to read
313 # and the data to be read, of the previously specified size
314 if sizeOrData mod 2 == 0:
315 # Expect a number of chars to read
316 await client.recvLineInto(lineFut, maxLength = maxLine)
317 try:
318 bytesToRead = lineFut.mget.parseHexInt
319 except ValueError:
320 # Malformed request
321 await request.respond(Http411, ("Invalid chunked transfer encoding - " &
322 "chunk data size must be hex encoded"))
323 return true
324 # todo-nim: check the size before reading (and so allocating) the
325 # chunk, against what the body may still grow by. A negative size
326 # means the hex overflowed.
327 if bytesToRead < 0 or bytesToRead > server.maxBody - request.body.len:
328 await request.respondError(Http413)
329 return false
330 else:
331 if bytesToRead == 0:
332 # Done reading chunked data
333 break
334
335 # Read bytesToRead and add to body
336 let chunk = await client.recv(bytesToRead)
337 request.body.add(chunk)
338 # Skip \r\n (chunk terminating bytes per spec)
339 let separator = await client.recv(2)
340 if separator != "\r\n":
341 await request.respond(Http400, "Bad Request. Encoding separator must be \\r\\n")
342 return true
343
344 inc sizeOrData
345 elif request.reqMethod == HttpPost:
346 await request.respond(Http411, "Content-Length required.")
347 return true
348
349 # Call the user's callback.
350 await callback(request)
351
352 if "upgrade" in request.headers.getOrDefault("connection"):
353 return false
354
355 # The request has been served, from this point on returning `true` means the
356 # connection will not be closed and will be kept in the connection pool.
357
358 # Persistent connections
359 if (request.protocol == HttpVer11 and
360 cmpIgnoreCase(request.headers.getOrDefault("connection"), "close") != 0) or
361 (request.protocol == HttpVer10 and
362 cmpIgnoreCase(request.headers.getOrDefault("connection"), "keep-alive") == 0):
363 # In HTTP 1.1 we assume that connection is persistent. Unless connection
364 # header states otherwise.
365 # In HTTP 1.0 we assume that the connection should not be persistent.
366 # Unless the connection header states otherwise.
367 return true
368 else:
369 request.client.close()
370 return false
371
372proc processClient(server: AsyncHttpServer, client: AsyncSocket, address: string,
373 callback: proc (request: Request):
374 Future[void] {.closure, gcsafe.}) {.async.} =
375 var request = newFutureVar[Request]("asynchttpserver.processClient")
376 request.mget().url = initUri()
377 request.mget().headers = newHttpHeaders()
378 var lineFut = newFutureVar[string]("asynchttpserver.processClient")
379 lineFut.mget() = newStringOfCap(80)
380
381 while not client.isClosed:
382 let retry = await processRequest(
383 server, request, client, address, lineFut, callback
384 )
385 if not retry:
386 client.close()
387 break
388
389const
390 nimMaxDescriptorsFallback* {.intdefine.} = 16_000 ## fallback value for \
391 ## when `maxDescriptors` is not available.
392 ## This can be set on the command line during compilation
393 ## via `-d:nimMaxDescriptorsFallback=N`
394
395proc listen*(server: AsyncHttpServer; port: Port; address = ""; domain = AF_INET) =
396 ## Listen to the given port and address.
397 when declared(maxDescriptors):
398 server.maxFDs = try: maxDescriptors() except: nimMaxDescriptorsFallback
399 else:
400 server.maxFDs = nimMaxDescriptorsFallback
401 server.socket = newAsyncSocket(domain)
402 if server.reuseAddr:
403 server.socket.setSockOpt(OptReuseAddr, true)
404 when not defined(nuttx):
405 if server.reusePort:
406 server.socket.setSockOpt(OptReusePort, true)
407 server.socket.bindAddr(port, address)
408 server.socket.listen()
409
410proc shouldAcceptRequest*(server: AsyncHttpServer;
411 assumedDescriptorsPerRequest = 5): bool {.inline.} =
412 ## Returns true if the process's current number of opened file
413 ## descriptors is still within the maximum limit and so it's reasonable to
414 ## accept yet another request.
415 result = assumedDescriptorsPerRequest < 0 or
416 (activeDescriptors() + assumedDescriptorsPerRequest < server.maxFDs)
417
418proc acceptRequest*(server: AsyncHttpServer,
419 callback: proc (request: Request): Future[void] {.closure, gcsafe.}) {.async.} =
420 ## Accepts a single request. Write an explicit loop around this proc so that
421 ## errors can be handled properly.
422 var (address, client) = await server.socket.acceptAddr()
423 asyncCheck processClient(server, client, address, callback)
424
425proc serve*(server: AsyncHttpServer, port: Port,
426 callback: proc (request: Request): Future[void] {.closure, gcsafe.},
427 address = "";
428 assumedDescriptorsPerRequest = -1;
429 domain = AF_INET) {.async.} =
430 ## Starts the process of listening for incoming HTTP connections on the
431 ## specified address and port.
432 ##
433 ## When a request is made by a client the specified callback will be called.
434 ##
435 ## If `assumedDescriptorsPerRequest` is 0 or greater the server cares about
436 ## the process's maximum file descriptor limit. It then ensures that the
437 ## process still has the resources for `assumedDescriptorsPerRequest`
438 ## file descriptors before accepting a connection.
439 ##
440 ## You should prefer to call `acceptRequest` instead with a custom server
441 ## loop so that you're in control over the error handling and logging.
442 listen server, port, address, domain
443 while true:
444 if shouldAcceptRequest(server, assumedDescriptorsPerRequest):
445 var (address, client) = await server.socket.acceptAddr()
446 asyncCheck processClient(server, client, address, callback)
447 else:
448 poll()
449 #echo(f.isNil)
450 #echo(f.repr)
451
452proc close*(server: AsyncHttpServer) =
453 ## Terminates the async http server instance.
454 server.socket.close()
455