123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- #
- #
- # Nim's Runtime Library
- # (c) Copyright 2015 Dominik Picheta
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## This module implements a high performance asynchronous HTTP server.
- ##
- ## 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)
- import tables, asyncnet, asyncdispatch, parseutils, uri, strutils
- import httpcore
- export httpcore except parseHeader
- const
- maxLine = 8*1024
- # TODO: If it turns out that the decisions that asynchttpserver makes
- # explicitly, about whether to close the client sockets or upgrade them are
- # wrong, then add a return value which determines what to do for the callback.
- # Also, maybe move `client` out of `Request` object and into the args for
- # the proc.
- type
- Request* = object
- client*: AsyncSocket # TODO: Separate this into a Response object?
- reqMethod*: HttpMethod
- headers*: HttpHeaders
- protocol*: tuple[orig: string, major, minor: int]
- url*: Uri
- hostname*: string ## The hostname of the client that made the request.
- body*: string
- AsyncHttpServer* = ref object
- socket: AsyncSocket
- reuseAddr: bool
- reusePort: bool
- maxBody: int ## The maximum content-length that will be read for the body.
- 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
- proc addHeaders(msg: var string, headers: HttpHeaders) =
- for k, v in headers:
- msg.add(k & ": " & v & "\c\L")
- proc sendHeaders*(req: Request, headers: HttpHeaders): Future[void] =
- ## Sends the specified headers to the requesting client.
- var msg = ""
- addHeaders(msg, headers)
- return req.client.send(msg)
- proc respond*(req: Request, code: HttpCode, content: string,
- headers: HttpHeaders = nil): Future[void] =
- ## 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")
- var msg = "HTTP/1.1 " & $code & "\c\L"
- if headers != nil:
- msg.addHeaders(headers)
- msg.add("Content-Length: ")
- # this particular way saves allocations:
- msg.add content.len
- msg.add "\c\L\c\L"
- msg.add(content)
- result = req.client.send(msg)
- proc respondError(req: Request, code: HttpCode): Future[void] =
- ## Responds to the request with the specified ``HttpCode``.
- let content = $code
- var msg = "HTTP/1.1 " & content & "\c\L"
- msg.add("Content-Length: " & $content.len & "\c\L\c\L")
- msg.add(content)
- result = req.client.send(msg)
- proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
- var i = protocol.skipIgnoreCase("HTTP/")
- if i != 5:
- raise newException(ValueError, "Invalid request protocol. Got: " &
- protocol)
- result.orig = protocol
- i.inc protocol.parseSaturatedNatural(result.major, i)
- i.inc # Skip .
- i.inc protocol.parseSaturatedNatural(result.minor, i)
- proc sendStatus(client: AsyncSocket, status: string): Future[void] =
- client.send("HTTP/1.1 " & status & "\c\L\c\L")
- proc parseUppercaseMethod(name: string): HttpMethod =
- result =
- case name
- of "GET": HttpGet
- of "POST": HttpPost
- of "HEAD": HttpHead
- of "PUT": HttpPut
- of "DELETE": HttpDelete
- of "PATCH": HttpPatch
- of "OPTIONS": HttpOptions
- of "CONNECT": HttpConnect
- of "TRACE": HttpTrace
- else: raise newException(ValueError, "Invalid HTTP method " & name)
- proc processRequest(
- server: AsyncHttpServer,
- req: FutureVar[Request],
- client: AsyncSocket,
- address: string,
- lineFut: FutureVar[string],
- callback: proc (request: Request): Future[void] {.closure, gcsafe.},
- ): Future[bool] {.async.} =
- # Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
- template request(): Request =
- req.mget()
- # GET /path HTTP/1.1
- # Header: val
- # \n
- request.headers.clear()
- request.body = ""
- request.hostname.shallowCopy(address)
- assert client != nil
- request.client = client
- # We should skip at least one empty line before the request
- # https://tools.ietf.org/html/rfc7230#section-3.5
- for i in 0..1:
- lineFut.mget().setLen(0)
- lineFut.clean()
- await client.recvLineInto(lineFut, maxLength = maxLine) # TODO: Timeouts.
- if lineFut.mget == "":
- client.close()
- return false
- if lineFut.mget.len > maxLine:
- await request.respondError(Http413)
- client.close()
- return false
- if lineFut.mget != "\c\L":
- break
- # First line - GET /path HTTP/1.1
- var i = 0
- for linePart in lineFut.mget.split(' '):
- case i
- of 0:
- try:
- request.reqMethod = parseUppercaseMethod(linePart)
- except ValueError:
- asyncCheck request.respondError(Http400)
- return true # Retry processing of request
- of 1:
- try:
- parseUri(linePart, request.url)
- except ValueError:
- asyncCheck request.respondError(Http400)
- return true
- of 2:
- try:
- request.protocol = parseProtocol(linePart)
- except ValueError:
- asyncCheck request.respondError(Http400)
- return true
- else:
- await request.respondError(Http400)
- return true
- inc i
- # Headers
- while true:
- i = 0
- lineFut.mget.setLen(0)
- lineFut.clean()
- await client.recvLineInto(lineFut, maxLength = maxLine)
- if lineFut.mget == "":
- client.close(); return false
- if lineFut.mget.len > maxLine:
- await request.respondError(Http413)
- client.close(); return false
- if lineFut.mget == "\c\L": break
- let (key, value) = parseHeader(lineFut.mget)
- request.headers[key] = value
- # Ensure the client isn't trying to DoS us.
- if request.headers.len > headerLimit:
- await client.sendStatus("400 Bad Request")
- request.client.close()
- return false
- if request.reqMethod == HttpPost:
- # Check for Expect header
- if request.headers.hasKey("Expect"):
- if "100-continue" in request.headers["Expect"]:
- await client.sendStatus("100 Continue")
- else:
- await client.sendStatus("417 Expectation Failed")
- # Read the body
- # - Check for Content-length header
- if request.headers.hasKey("Content-Length"):
- var contentLength = 0
- if parseSaturatedNatural(request.headers["Content-Length"],
- contentLength) == 0:
- await request.respond(Http400, "Bad Request. Invalid Content-Length.")
- return true
- else:
- if contentLength > server.maxBody:
- await request.respondError(Http413)
- return false
- request.body = await client.recv(contentLength)
- if request.body.len != contentLength:
- await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
- return true
- elif request.reqMethod == HttpPost:
- await request.respond(Http411, "Content-Length required.")
- return true
- # Call the user's callback.
- await callback(request)
- if "upgrade" in request.headers.getOrDefault("connection"):
- return false
- # The request has been served, from this point on returning `true` means the
- # connection will not be closed and will be kept in the connection pool.
- # Persistent connections
- if (request.protocol == HttpVer11 and
- cmpIgnoreCase(request.headers.getOrDefault("connection"), "close") != 0) or
- (request.protocol == HttpVer10 and
- cmpIgnoreCase(request.headers.getOrDefault("connection"), "keep-alive") == 0):
- # In HTTP 1.1 we assume that connection is persistent. Unless connection
- # header states otherwise.
- # In HTTP 1.0 we assume that the connection should not be persistent.
- # Unless the connection header states otherwise.
- return true
- else:
- request.client.close()
- return false
- proc processClient(server: AsyncHttpServer, client: AsyncSocket, address: string,
- callback: proc (request: Request):
- Future[void] {.closure, gcsafe.}) {.async.} =
- var request = newFutureVar[Request]("asynchttpserver.processClient")
- request.mget().url = initUri()
- request.mget().headers = newHttpHeaders()
- var lineFut = newFutureVar[string]("asynchttpserver.processClient")
- lineFut.mget() = newStringOfCap(80)
- while not client.isClosed:
- let retry = await processRequest(
- server, request, client, address, lineFut, callback
- )
- if not retry: break
- proc serve*(server: AsyncHttpServer, port: Port,
- callback: proc (request: Request): Future[void] {.closure, gcsafe.},
- address = "") {.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()
- while true:
- var (address, client) = await server.socket.acceptAddr()
- asyncCheck processClient(server, client, address, callback)
- #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()
|