| 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 | |
| 24 | runnableExamples("-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 | |
| 49 | import std/[asyncnet, asyncdispatch, parseutils, uri, strutils] |
| 50 | import std/httpcore |
| 51 | from std/nativesockets import getLocalAddr, Domain, AF_INET, AF_INET6 |
| 52 | import std/private/since |
| 53 | |
| 54 | when defined(nimPreviewSlimSystem): |
| 55 | import std/assertions |
| 56 | |
| 57 | export httpcore except parseHeader |
| 58 | |
| 59 | const |
| 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. |
| 67 | type |
| 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 | |
| 84 | proc 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 | |
| 97 | proc 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 | |
| 102 | proc addHeaders(msg: var string, headers: HttpHeaders) = |
| 103 | for k, v in headers: |
| 104 | msg.add(k & ": " & v & "\c\L") |
| 105 | |
| 106 | proc 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 | |
| 112 | proc 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 | |
| 146 | proc 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 | |
| 155 | proc 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 | |
| 166 | proc sendStatus(client: AsyncSocket, status: string): Future[void] = |
| 167 | client.send("HTTP/1.1 " & status & "\c\L\c\L") |
| 168 | |
| 169 | func 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 | |
| 180 | proc 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 | |
| 372 | proc 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 | |
| 389 | const |
| 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 | |
| 395 | proc 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 | |
| 410 | proc 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 | |
| 418 | proc 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 | |
| 425 | proc 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 | |
| 452 | proc close*(server: AsyncHttpServer) = |
| 453 | ## Terminates the async http server instance. |
| 454 | server.socket.close() |
| 455 | |