asynchttpserver.nim 15 KB

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