asynchttpserver.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. ## This module implements a high performance asynchronous HTTP server.
  10. ##
  11. ## This HTTP server has not been designed to be used in production, but
  12. ## for testing applications locally. Because of this, when deploying your
  13. ## application you should use a reverse proxy (for example nginx) instead of
  14. ## allowing users to connect directly to this server.
  15. ##
  16. ## Basic usage
  17. ## ===========
  18. ##
  19. ## This example will create an HTTP server on port 8080. The server will
  20. ## respond to all requests with a ``200 OK`` response code and "Hello World"
  21. ## as the response body.
  22. ##
  23. ## .. code-block::nim
  24. ## import asynchttpserver, asyncdispatch
  25. ##
  26. ## var server = newAsyncHttpServer()
  27. ## proc cb(req: Request) {.async.} =
  28. ## await req.respond(Http200, "Hello World")
  29. ##
  30. ## waitFor server.serve(Port(8080), cb)
  31. import asyncnet, asyncdispatch, parseutils, uri, strutils
  32. import httpcore
  33. export httpcore except parseHeader
  34. const
  35. maxLine = 8*1024
  36. # TODO: If it turns out that the decisions that asynchttpserver makes
  37. # explicitly, about whether to close the client sockets or upgrade them are
  38. # wrong, then add a return value which determines what to do for the callback.
  39. # Also, maybe move `client` out of `Request` object and into the args for
  40. # the proc.
  41. type
  42. Request* = object
  43. client*: AsyncSocket # TODO: Separate this into a Response object?
  44. reqMethod*: HttpMethod
  45. headers*: HttpHeaders
  46. protocol*: tuple[orig: string, major, minor: int]
  47. url*: Uri
  48. hostname*: string ## The hostname of the client that made the request.
  49. body*: string
  50. AsyncHttpServer* = ref object
  51. socket: AsyncSocket
  52. reuseAddr: bool
  53. reusePort: bool
  54. maxBody: int ## The maximum content-length that will be read for the body.
  55. proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
  56. maxBody = 8388608): AsyncHttpServer =
  57. ## Creates a new ``AsyncHttpServer`` instance.
  58. new result
  59. result.reuseAddr = reuseAddr
  60. result.reusePort = reusePort
  61. result.maxBody = maxBody
  62. proc addHeaders(msg: var string, headers: HttpHeaders) =
  63. for k, v in headers:
  64. msg.add(k & ": " & v & "\c\L")
  65. proc sendHeaders*(req: Request, headers: HttpHeaders): Future[void] =
  66. ## Sends the specified headers to the requesting client.
  67. var msg = ""
  68. addHeaders(msg, headers)
  69. return req.client.send(msg)
  70. proc respond*(req: Request, code: HttpCode, content: string,
  71. headers: HttpHeaders = nil): Future[void] =
  72. ## Responds to the request with the specified ``HttpCode``, headers and
  73. ## content.
  74. ##
  75. ## This procedure will **not** close the client socket.
  76. ##
  77. ## Example:
  78. ##
  79. ## .. code-block::nim
  80. ## import json
  81. ## proc handler(req: Request) {.async.} =
  82. ## if req.url.path == "/hello-world":
  83. ## let msg = %* {"message": "Hello World"}
  84. ## let headers = newHttpHeaders([("Content-Type","application/json")])
  85. ## await req.respond(Http200, $msg, headers)
  86. ## else:
  87. ## await req.respond(Http404, "Not Found")
  88. var msg = "HTTP/1.1 " & $code & "\c\L"
  89. if headers != nil:
  90. msg.addHeaders(headers)
  91. # If the headers did not contain a Content-Length use our own
  92. if headers.isNil() or not headers.hasKey("Content-Length"):
  93. msg.add("Content-Length: ")
  94. # this particular way saves allocations:
  95. msg.addInt content.len
  96. msg.add "\c\L"
  97. msg.add "\c\L"
  98. msg.add(content)
  99. result = req.client.send(msg)
  100. proc respondError(req: Request, code: HttpCode): Future[void] =
  101. ## Responds to the request with the specified ``HttpCode``.
  102. let content = $code
  103. var msg = "HTTP/1.1 " & content & "\c\L"
  104. msg.add("Content-Length: " & $content.len & "\c\L\c\L")
  105. msg.add(content)
  106. result = req.client.send(msg)
  107. proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
  108. var i = protocol.skipIgnoreCase("HTTP/")
  109. if i != 5:
  110. raise newException(ValueError, "Invalid request protocol. Got: " &
  111. protocol)
  112. result.orig = protocol
  113. i.inc protocol.parseSaturatedNatural(result.major, i)
  114. i.inc # Skip .
  115. i.inc protocol.parseSaturatedNatural(result.minor, i)
  116. proc sendStatus(client: AsyncSocket, status: string): Future[void] =
  117. client.send("HTTP/1.1 " & status & "\c\L\c\L")
  118. proc processRequest(
  119. server: AsyncHttpServer,
  120. req: FutureVar[Request],
  121. client: AsyncSocket,
  122. address: string,
  123. lineFut: FutureVar[string],
  124. callback: proc (request: Request): Future[void] {.closure, gcsafe.},
  125. ): Future[bool] {.async.} =
  126. # Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
  127. template request(): Request =
  128. req.mget()
  129. # GET /path HTTP/1.1
  130. # Header: val
  131. # \n
  132. request.headers.clear()
  133. request.body = ""
  134. request.hostname.shallowCopy(address)
  135. assert client != nil
  136. request.client = client
  137. # We should skip at least one empty line before the request
  138. # https://tools.ietf.org/html/rfc7230#section-3.5
  139. for i in 0..1:
  140. lineFut.mget().setLen(0)
  141. lineFut.clean()
  142. await client.recvLineInto(lineFut, maxLength = maxLine) # TODO: Timeouts.
  143. if lineFut.mget == "":
  144. client.close()
  145. return false
  146. if lineFut.mget.len > maxLine:
  147. await request.respondError(Http413)
  148. client.close()
  149. return false
  150. if lineFut.mget != "\c\L":
  151. break
  152. # First line - GET /path HTTP/1.1
  153. var i = 0
  154. for linePart in lineFut.mget.split(' '):
  155. case i
  156. of 0:
  157. case linePart
  158. of "GET": request.reqMethod = HttpGet
  159. of "POST": request.reqMethod = HttpPost
  160. of "HEAD": request.reqMethod = HttpHead
  161. of "PUT": request.reqMethod = HttpPut
  162. of "DELETE": request.reqMethod = HttpDelete
  163. of "PATCH": request.reqMethod = HttpPatch
  164. of "OPTIONS": request.reqMethod = HttpOptions
  165. of "CONNECT": request.reqMethod = HttpConnect
  166. of "TRACE": request.reqMethod = HttpTrace
  167. else:
  168. asyncCheck request.respondError(Http400)
  169. return true # Retry processing of request
  170. of 1:
  171. try:
  172. parseUri(linePart, request.url)
  173. except ValueError:
  174. asyncCheck request.respondError(Http400)
  175. return true
  176. of 2:
  177. try:
  178. request.protocol = parseProtocol(linePart)
  179. except ValueError:
  180. asyncCheck request.respondError(Http400)
  181. return true
  182. else:
  183. await request.respondError(Http400)
  184. return true
  185. inc i
  186. # Headers
  187. while true:
  188. i = 0
  189. lineFut.mget.setLen(0)
  190. lineFut.clean()
  191. await client.recvLineInto(lineFut, maxLength = maxLine)
  192. if lineFut.mget == "":
  193. client.close(); return false
  194. if lineFut.mget.len > maxLine:
  195. await request.respondError(Http413)
  196. client.close(); return false
  197. if lineFut.mget == "\c\L": break
  198. let (key, value) = parseHeader(lineFut.mget)
  199. request.headers[key] = value
  200. # Ensure the client isn't trying to DoS us.
  201. if request.headers.len > headerLimit:
  202. await client.sendStatus("400 Bad Request")
  203. request.client.close()
  204. return false
  205. if request.reqMethod == HttpPost:
  206. # Check for Expect header
  207. if request.headers.hasKey("Expect"):
  208. if "100-continue" in request.headers["Expect"]:
  209. await client.sendStatus("100 Continue")
  210. else:
  211. await client.sendStatus("417 Expectation Failed")
  212. # Read the body
  213. # - Check for Content-length header
  214. if request.headers.hasKey("Content-Length"):
  215. var contentLength = 0
  216. if parseSaturatedNatural(request.headers["Content-Length"], contentLength) == 0:
  217. await request.respond(Http400, "Bad Request. Invalid Content-Length.")
  218. return true
  219. else:
  220. if contentLength > server.maxBody:
  221. await request.respondError(Http413)
  222. return false
  223. request.body = await client.recv(contentLength)
  224. if request.body.len != contentLength:
  225. await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
  226. return true
  227. elif request.reqMethod == HttpPost:
  228. await request.respond(Http411, "Content-Length required.")
  229. return true
  230. # Call the user's callback.
  231. await callback(request)
  232. if "upgrade" in request.headers.getOrDefault("connection"):
  233. return false
  234. # The request has been served, from this point on returning `true` means the
  235. # connection will not be closed and will be kept in the connection pool.
  236. # Persistent connections
  237. if (request.protocol == HttpVer11 and
  238. cmpIgnoreCase(request.headers.getOrDefault("connection"), "close") != 0) or
  239. (request.protocol == HttpVer10 and
  240. cmpIgnoreCase(request.headers.getOrDefault("connection"), "keep-alive") == 0):
  241. # In HTTP 1.1 we assume that connection is persistent. Unless connection
  242. # header states otherwise.
  243. # In HTTP 1.0 we assume that the connection should not be persistent.
  244. # Unless the connection header states otherwise.
  245. return true
  246. else:
  247. request.client.close()
  248. return false
  249. proc processClient(server: AsyncHttpServer, client: AsyncSocket, address: string,
  250. callback: proc (request: Request):
  251. Future[void] {.closure, gcsafe.}) {.async.} =
  252. var request = newFutureVar[Request]("asynchttpserver.processClient")
  253. request.mget().url = initUri()
  254. request.mget().headers = newHttpHeaders()
  255. var lineFut = newFutureVar[string]("asynchttpserver.processClient")
  256. lineFut.mget() = newStringOfCap(80)
  257. while not client.isClosed:
  258. let retry = await processRequest(
  259. server, request, client, address, lineFut, callback
  260. )
  261. if not retry: break
  262. proc serve*(server: AsyncHttpServer, port: Port,
  263. callback: proc (request: Request): Future[void] {.closure, gcsafe.},
  264. address = "") {.async.} =
  265. ## Starts the process of listening for incoming HTTP connections on the
  266. ## specified address and port.
  267. ##
  268. ## When a request is made by a client the specified callback will be called.
  269. server.socket = newAsyncSocket()
  270. if server.reuseAddr:
  271. server.socket.setSockOpt(OptReuseAddr, true)
  272. if server.reusePort:
  273. server.socket.setSockOpt(OptReusePort, true)
  274. server.socket.bindAddr(port, address)
  275. server.socket.listen()
  276. while true:
  277. var (address, client) = await server.socket.acceptAddr()
  278. asyncCheck processClient(server, client, address, callback)
  279. #echo(f.isNil)
  280. #echo(f.repr)
  281. proc close*(server: AsyncHttpServer) =
  282. ## Terminates the async http server instance.
  283. server.socket.close()
  284. when not defined(testing) and isMainModule:
  285. proc main =
  286. var server = newAsyncHttpServer()
  287. proc cb(req: Request) {.async.} =
  288. #echo(req.reqMethod, " ", req.url)
  289. #echo(req.headers)
  290. let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT",
  291. "Content-type": "text/plain; charset=utf-8"}
  292. await req.respond(Http200, "Hello World", headers.newHttpHeaders())
  293. asyncCheck server.serve(Port(5555), cb)
  294. runForever()
  295. main()