diff options
Diffstat (limited to 'lib/pure/asynchttpserver.nim')
-rw-r--r-- | lib/pure/asynchttpserver.nim | 236 |
1 files changed, 171 insertions, 65 deletions
diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim index afd452b01..39e945d5e 100644 --- a/lib/pure/asynchttpserver.nim +++ b/lib/pure/asynchttpserver.nim @@ -11,27 +11,41 @@ ## ## This HTTP server has not been designed to be used in production, but ## for testing applications locally. Because of this, when deploying your -## application you should use a reverse proxy (for example nginx) instead of -## allowing users to connect directly to this server. -## -## Basic usage -## =========== -## -## This example will create an HTTP server on port 8080. The server will -## respond to all requests with a ``200 OK`` response code and "Hello World" -## as the response body. -## -## .. code-block::nim -## import asynchttpserver, asyncdispatch -## -## var server = newAsyncHttpServer() -## proc cb(req: Request) {.async.} = -## await req.respond(Http200, "Hello World") -## -## waitFor server.serve(Port(8080), cb) +## application in production you should use a reverse proxy (for example nginx) +## instead of allowing users to connect directly to this server. + +runnableExamples("-r:off"): + # This example will create an HTTP server on an automatically chosen port. + # It will respond to all requests with a `200 OK` response code and "Hello World" + # as the response body. + import std/asyncdispatch + proc main {.async.} = + var server = newAsyncHttpServer() + proc cb(req: Request) {.async.} = + echo (req.reqMethod, req.url, req.headers) + let headers = {"Content-type": "text/plain; charset=utf-8"} + await req.respond(Http200, "Hello World", headers.newHttpHeaders()) + + server.listen(Port(0)) # or Port(8080) to hardcode the standard HTTP port. + let port = server.getPort + echo "test this with: curl localhost:" & $port.uint16 & "/" + while true: + if server.shouldAcceptRequest(): + await server.acceptRequest(cb) + else: + # too many concurrent connections, `maxFDs` exceeded + # wait 500ms for FDs to be closed + await sleepAsync(500) + + waitFor main() -import asyncnet, asyncdispatch, parseutils, uri, strutils -import httpcore +import std/[asyncnet, asyncdispatch, parseutils, uri, strutils] +import std/httpcore +from std/nativesockets import getLocalAddr, Domain, AF_INET, AF_INET6 +import std/private/since + +when defined(nimPreviewSlimSystem): + import std/assertions export httpcore except parseHeader @@ -58,14 +72,25 @@ type reuseAddr: bool reusePort: bool maxBody: int ## The maximum content-length that will be read for the body. + maxFDs: int + +proc getPort*(self: AsyncHttpServer): Port {.since: (1, 5, 1).} = + ## Returns the port `self` was bound to. + ## + ## Useful for identifying what port `self` is bound to, if it + ## was chosen automatically, for example via `listen(Port(0))`. + runnableExamples: + from std/nativesockets import Port + let server = newAsyncHttpServer() + server.listen(Port(0)) + assert server.getPort.uint16 > 0 + server.close() + result = getLocalAddr(self.socket)[1] proc newAsyncHttpServer*(reuseAddr = true, reusePort = false, maxBody = 8388608): AsyncHttpServer = - ## Creates a new ``AsyncHttpServer`` instance. - new result - result.reuseAddr = reuseAddr - result.reusePort = reusePort - result.maxBody = maxBody + ## Creates a new `AsyncHttpServer` instance. + result = AsyncHttpServer(reuseAddr: reuseAddr, reusePort: reusePort, maxBody: maxBody) proc addHeaders(msg: var string, headers: HttpHeaders) = for k, v in headers: @@ -79,22 +104,22 @@ proc sendHeaders*(req: Request, headers: HttpHeaders): Future[void] = proc respond*(req: Request, code: HttpCode, content: string, headers: HttpHeaders = nil): Future[void] = - ## Responds to the request with the specified ``HttpCode``, headers and + ## Responds to the request with the specified `HttpCode`, headers and ## content. ## ## This procedure will **not** close the client socket. ## ## Example: - ## - ## .. code-block::nim - ## import json - ## proc handler(req: Request) {.async.} = - ## if req.url.path == "/hello-world": - ## let msg = %* {"message": "Hello World"} - ## let headers = newHttpHeaders([("Content-Type","application/json")]) - ## await req.respond(Http200, $msg, headers) - ## else: - ## await req.respond(Http404, "Not Found") + ## ```Nim + ## import std/json + ## proc handler(req: Request) {.async.} = + ## if req.url.path == "/hello-world": + ## let msg = %* {"message": "Hello World"} + ## let headers = newHttpHeaders([("Content-Type","application/json")]) + ## await req.respond(Http200, $msg, headers) + ## else: + ## await req.respond(Http404, "Not Found") + ## ``` var msg = "HTTP/1.1 " & $code & "\c\L" if headers != nil: @@ -112,7 +137,7 @@ proc respond*(req: Request, code: HttpCode, content: string, result = req.client.send(msg) proc respondError(req: Request, code: HttpCode): Future[void] = - ## Responds to the request with the specified ``HttpCode``. + ## Responds to the request with the specified `HttpCode`. let content = $code var msg = "HTTP/1.1 " & content & "\c\L" @@ -133,11 +158,22 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] = proc sendStatus(client: AsyncSocket, status: string): Future[void] = client.send("HTTP/1.1 " & status & "\c\L\c\L") +func hasChunkedEncoding(request: Request): bool = + ## Searches for a chunked transfer encoding + const transferEncoding = "Transfer-Encoding" + + if request.headers.hasKey(transferEncoding): + for encoding in seq[string](request.headers[transferEncoding]): + if "chunked" == encoding.strip: + # Returns true if it is both an HttpPost and has chunked encoding + return request.reqMethod == HttpPost + return false + proc processRequest( server: AsyncHttpServer, req: FutureVar[Request], client: AsyncSocket, - address: string, + address: sink string, lineFut: FutureVar[string], callback: proc (request: Request): Future[void] {.closure, gcsafe.}, ): Future[bool] {.async.} = @@ -151,7 +187,10 @@ proc processRequest( # \n request.headers.clear() request.body = "" - request.hostname.shallowCopy(address) + when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc): + request.hostname = address + else: + request.hostname.shallowCopy(address) assert client != nil request.client = client @@ -252,6 +291,43 @@ proc processRequest( if request.body.len != contentLength: await request.respond(Http400, "Bad Request. Content-Length does not match actual.") return true + elif hasChunkedEncoding(request): + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding + var sizeOrData = 0 + var bytesToRead = 0 + request.body = "" + + while true: + lineFut.mget.setLen(0) + lineFut.clean() + + # The encoding format alternates between specifying a number of bytes to read + # and the data to be read, of the previously specified size + if sizeOrData mod 2 == 0: + # Expect a number of chars to read + await client.recvLineInto(lineFut, maxLength = maxLine) + try: + bytesToRead = lineFut.mget.parseHexInt + except ValueError: + # Malformed request + await request.respond(Http411, ("Invalid chunked transfer encoding - " & + "chunk data size must be hex encoded")) + return true + else: + if bytesToRead == 0: + # Done reading chunked data + break + + # Read bytesToRead and add to body + let chunk = await client.recv(bytesToRead) + request.body.add(chunk) + # Skip \r\n (chunk terminating bytes per spec) + let separator = await client.recv(2) + if separator != "\r\n": + await request.respond(Http400, "Bad Request. Encoding separator must be \\r\\n") + return true + + inc sizeOrData elif request.reqMethod == HttpPost: await request.respond(Http411, "Content-Length required.") return true @@ -292,43 +368,73 @@ proc processClient(server: AsyncHttpServer, client: AsyncSocket, address: string let retry = await processRequest( server, request, client, address, lineFut, callback ) - if not retry: break + if not retry: + client.close() + break + +const + nimMaxDescriptorsFallback* {.intdefine.} = 16_000 ## fallback value for \ + ## when `maxDescriptors` is not available. + ## This can be set on the command line during compilation + ## via `-d:nimMaxDescriptorsFallback=N` + +proc listen*(server: AsyncHttpServer; port: Port; address = ""; domain = AF_INET) = + ## Listen to the given port and address. + when declared(maxDescriptors): + server.maxFDs = try: maxDescriptors() except: nimMaxDescriptorsFallback + else: + server.maxFDs = nimMaxDescriptorsFallback + server.socket = newAsyncSocket(domain) + if server.reuseAddr: + server.socket.setSockOpt(OptReuseAddr, true) + when not defined(nuttx): + if server.reusePort: + server.socket.setSockOpt(OptReusePort, true) + server.socket.bindAddr(port, address) + server.socket.listen() + +proc shouldAcceptRequest*(server: AsyncHttpServer; + assumedDescriptorsPerRequest = 5): bool {.inline.} = + ## Returns true if the process's current number of opened file + ## descriptors is still within the maximum limit and so it's reasonable to + ## accept yet another request. + result = assumedDescriptorsPerRequest < 0 or + (activeDescriptors() + assumedDescriptorsPerRequest < server.maxFDs) + +proc acceptRequest*(server: AsyncHttpServer, + callback: proc (request: Request): Future[void] {.closure, gcsafe.}) {.async.} = + ## Accepts a single request. Write an explicit loop around this proc so that + ## errors can be handled properly. + var (address, client) = await server.socket.acceptAddr() + asyncCheck processClient(server, client, address, callback) proc serve*(server: AsyncHttpServer, port: Port, callback: proc (request: Request): Future[void] {.closure, gcsafe.}, - address = "") {.async.} = + address = ""; + assumedDescriptorsPerRequest = -1; + domain = AF_INET) {.async.} = ## Starts the process of listening for incoming HTTP connections on the ## specified address and port. ## ## When a request is made by a client the specified callback will be called. - server.socket = newAsyncSocket() - if server.reuseAddr: - server.socket.setSockOpt(OptReuseAddr, true) - if server.reusePort: - server.socket.setSockOpt(OptReusePort, true) - server.socket.bindAddr(port, address) - server.socket.listen() - + ## + ## If `assumedDescriptorsPerRequest` is 0 or greater the server cares about + ## the process's maximum file descriptor limit. It then ensures that the + ## process still has the resources for `assumedDescriptorsPerRequest` + ## file descriptors before accepting a connection. + ## + ## You should prefer to call `acceptRequest` instead with a custom server + ## loop so that you're in control over the error handling and logging. + listen server, port, address, domain while true: - var (address, client) = await server.socket.acceptAddr() - asyncCheck processClient(server, client, address, callback) + if shouldAcceptRequest(server, assumedDescriptorsPerRequest): + var (address, client) = await server.socket.acceptAddr() + asyncCheck processClient(server, client, address, callback) + else: + poll() #echo(f.isNil) #echo(f.repr) proc close*(server: AsyncHttpServer) = ## Terminates the async http server instance. server.socket.close() - -when not defined(testing) and isMainModule: - proc main = - var server = newAsyncHttpServer() - proc cb(req: Request) {.async.} = - #echo(req.reqMethod, " ", req.url) - #echo(req.headers) - let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT", - "Content-type": "text/plain; charset=utf-8"} - await req.respond(Http200, "Hello World", headers.newHttpHeaders()) - - asyncCheck server.serve(Port(5555), cb) - runForever() - main() |