summary refs log tree commit diff stats
path: root/lib
diff options
context:
space:
mode:
authorDominik Picheta <dominikpicheta@googlemail.com>2014-04-05 20:30:32 +0100
committerDominik Picheta <dominikpicheta@googlemail.com>2014-04-05 20:30:32 +0100
commitcd70bba332cffe51dd359b339ea6a40eff543ce9 (patch)
tree2486160ce58c0701c146193f8daff4780d51fe9d /lib
parentd0478a5637dd4f75cfd615e3ed8aa4d0a082a8c9 (diff)
downloadNim-cd70bba332cffe51dd359b339ea6a40eff543ce9.tar.gz
Added asynchttpserver module.
Diffstat (limited to 'lib')
-rw-r--r--lib/pure/asynchttpserver.nim177
1 files changed, 177 insertions, 0 deletions
diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim
new file mode 100644
index 000000000..2f33cf4ab
--- /dev/null
+++ b/lib/pure/asynchttpserver.nim
@@ -0,0 +1,177 @@
+#
+#
+#            Nimrod's Runtime Library
+#        (c) Copyright 2014 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.
+
+import strtabs, asyncnet, asyncdispatch, parseutils, parseurl, strutils
+type
+  TRequest* = object
+    client: PAsyncSocket # TODO: Separate this into a Response object?
+    reqMethod*: string
+    headers*: PStringTable
+    protocol*: tuple[orig: string, major, minor: int]
+    url*: TURL
+    hostname*: string ## The hostname of the client that made the request.
+
+  PAsyncHttpServer* = ref object
+    socket: PAsyncSocket
+
+  THttpCode* = enum
+    Http200 = "200 OK",
+    Http303 = "303 Moved",
+    Http400 = "400 Bad Request",
+    Http404 = "404 Not Found",
+    Http500 = "500 Internal Server Error",
+    Http502 = "502 Bad Gateway"
+
+  THttpVersion* = enum
+    HttpVer11,
+    HttpVer10
+
+proc `==`*(protocol: tuple[orig: string, major, minor: int],
+           ver: THttpVersion): bool =
+  let major =
+    case ver
+    of HttpVer11, HttpVer10: 1
+  let minor =
+    case ver
+    of HttpVer11: 1
+    of HttpVer10: 0
+  result = protocol.major == major and protocol.minor == minor
+
+proc newAsyncHttpServer*(): PAsyncHttpServer =
+  new result
+
+proc sendHeaders*(req: TRequest, headers: PStringTable) {.async.} =
+  ## Sends the specified headers to the requesting client.
+  for k, v in headers:
+    await req.client.send(k & ": " & v & "\c\L")
+
+proc respond*(req: TRequest, code: THttpCode,
+        content: string, headers: PStringTable = newStringTable()) {.async.} =
+  ## Responds to the request with the specified ``HttpCode``, headers and
+  ## content.
+  ##
+  ## This procedure will **not** close the client socket.
+  var customHeaders = headers
+  customHeaders["Content-Length"] = $content.len
+  await req.client.send("HTTP/1.1 " & $code & "\c\L")
+  await sendHeaders(req, headers)
+  await req.client.send("\c\L" & content)
+
+proc newRequest(): TRequest =
+  result.headers = newStringTable(modeCaseInsensitive)
+
+proc parseHeader(line: string): tuple[key, value: string] =
+  var i = 0
+  i = line.parseUntil(result.key, ':')
+  inc(i) # skip :
+  i += line.skipWhiteSpace(i)
+  i += line.parseUntil(result.value, {'\c', '\L'}, i)
+
+proc parseProtocol(protocol: string):  tuple[orig: string, major, minor: int] =
+  var i = protocol.skipIgnoreCase("HTTP/")
+  if i != 5:
+    raise newException(EInvalidValue, "Invalid request protocol. Got: " &
+        protocol)
+  result.orig = protocol
+  i.inc protocol.parseInt(result.major, i)
+  i.inc # Skip .
+  i.inc protocol.parseInt(result.minor, i)
+
+proc processClient(client: PAsyncSocket, address: string,
+                 callback: proc (request: TRequest): PFuture[void]) {.async.} =
+  # GET /path HTTP/1.1
+  # Header: val
+  # \n
+
+  var request = newRequest()
+  # First line - GET /path HTTP/1.1
+  let line = await client.recvLine() # TODO: Timeouts.
+  if line == "":
+    client.close()
+    return
+  let lineParts = line.split(' ')
+  if lineParts.len != 3:
+    request.respond(Http400, "Invalid request. Got: " & line)
+
+  let reqMethod = lineParts[0]
+  let path = lineParts[1]
+  let protocol = lineParts[2]
+
+  # Headers
+  var i = 0
+  while true:
+    i = 0
+    let headerLine = await client.recvLine()
+    if headerLine == "":
+      client.close(); return
+    if headerLine == "\c\L": break
+    # TODO: Compiler crash
+    #let (key, value) = parseHeader(headerLine)
+    let kv = parseHeader(headerLine)
+    request.headers[kv.key] = kv.value
+
+  request.reqMethod = reqMethod
+  request.url = parseUrl(path)
+  try:
+    request.protocol = protocol.parseProtocol()
+  except EInvalidValue:
+    request.respond(Http400, "Invalid request protocol. Got: " & protocol)
+    return
+  request.hostname = address
+  request.client = client
+  
+  case reqMethod.normalize
+  of "get":
+    await callback(request)
+  else:
+    echo(reqMethod.repr)
+    echo(line.repr)
+    request.respond(Http400, "Invalid request method. Got: " & reqMethod)
+
+  # Persistent connections
+  if (request.protocol == HttpVer11 and
+      request.headers["connection"].normalize != "close") or
+     (request.protocol == HttpVer10 and
+      request.headers["connection"].normalize == "keep-alive"):
+    # 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.
+    await processClient(client, address, callback)
+  else:
+    request.client.close()
+
+proc serve*(server: PAsyncHttpServer, port: TPort,
+            callback: proc (request: TRequest): PFuture[void],
+            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()
+  server.socket.bindAddr(port, address)
+  server.socket.listen()
+  
+  while true:
+    # TODO: Causes compiler crash.
+    #var (address, client) = await server.socket.acceptAddr()
+    var fut = await server.socket.acceptAddr()
+    processClient(fut.client, fut.address, callback)
+
+when isMainModule:
+  var server = newAsyncHttpServer()
+  proc cb(req: TRequest) {.async.} =
+    #echo(req.reqMethod, " ", req.url)
+    #echo(req.headers)
+    await req.respond(Http200, "Hello World")
+
+  server.serve(TPort(5555), cb)
+  runForever()