all repos — litestore @ 89bc50b400a10f9af41add180bddca2adc482c0a

A minimalist nosql document store.

lib/x_asynchttpserver.nim

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
#
#            Nim's Runtime Library
#        (c) Copyright 2014 Dominik Picheta
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#
#    Modified by Fabio Cevasco to allow simple processing of PUT and PATCH methods
#

## This module implements a high performance asynchronous HTTP server.
##
## Examples
## --------
##
## 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
##    var server = newAsyncHttpServer()
##    proc cb(req: Request) {.async.} =
##      await req.respond(Http200, "Hello World")
##
##    asyncCheck server.serve(Port(8080), cb)
##    runForever()

import strtabs, asyncnet, asyncdispatch, parseutils, uri, strutils
type
  Request* = object
    client*: AsyncSocket # TODO: Separate this into a Response object?
    reqMethod*: string
    headers*: StringTableRef
    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

  HttpCode* = enum
    Http100 = "100 Continue",
    Http101 = "101 Switching Protocols",
    Http200 = "200 OK",
    Http201 = "201 Created",
    Http202 = "202 Accepted",
    Http204 = "204 No Content",
    Http205 = "205 Reset Content",
    Http206 = "206 Partial Content",
    Http300 = "300 Multiple Choices",
    Http301 = "301 Moved Permanently",
    Http302 = "302 Found",
    Http303 = "303 See Other",
    Http304 = "304 Not Modified",
    Http305 = "305 Use Proxy",
    Http307 = "307 Temporary Redirect",
    Http400 = "400 Bad Request",
    Http401 = "401 Unauthorized",
    Http403 = "403 Forbidden",
    Http404 = "404 Not Found",
    Http405 = "405 Method Not Allowed",
    Http406 = "406 Not Acceptable",
    Http407 = "407 Proxy Authentication Required",
    Http408 = "408 Request Timeout",
    Http409 = "409 Conflict",
    Http410 = "410 Gone",
    Http411 = "411 Length Required",
    Http418 = "418 I'm a teapot",
    Http500 = "500 Internal Server Error",
    Http501 = "501 Not Implemented",
    Http502 = "502 Bad Gateway",
    Http503 = "503 Service Unavailable",
    Http504 = "504 Gateway Timeout",
    Http505 = "505 HTTP Version Not Supported"

  HttpVersion* = enum
    HttpVer11,
    HttpVer10

{.deprecated: [TRequest: Request, PAsyncHttpServer: AsyncHttpServer,
  THttpCode: HttpCode, THttpVersion: HttpVersion].}

proc `==`*(protocol: tuple[orig: string, major, minor: int],
           ver: HttpVersion): 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*(reuseAddr = true): AsyncHttpServer =
  ## Creates a new ``AsyncHttpServer`` instance.
  new result
  result.reuseAddr = reuseAddr

proc addHeaders(msg: var string, headers: StringTableRef) =
  for k, v in headers:
    msg.add(k & ": " & v & "\c\L")

proc sendHeaders*(req: Request, headers: StringTableRef): 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 = 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
  var msg = "HTTP/1.1 " & $code & "\c\L"
  msg.addHeaders(customHeaders)
  await req.client.send(msg & "\c\L" & content)

proc newRequest(): Request =
  result.headers = newStringTable(modeCaseInsensitive)
  result.hostname = ""
  result.body = ""

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(ValueError, "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 sendStatus(client: AsyncSocket, status: string): Future[void] =
  client.send("HTTP/1.1 " & status & "\c\L")

proc processClient(client: AsyncSocket, address: string,
                   callback: proc (request: Request):
                      Future[void] {.closure, gcsafe.}) {.async.} =
  while not client.isClosed:
    # GET /path HTTP/1.1
    # Header: val
    # \n
    var request = newRequest()
    request.hostname = address
    assert client != nil
    request.client = client

    # 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:
      await request.respond(Http400, "Invalid request. Got: " & line)
      continue

    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 = parseUri(path)
    try:
      request.protocol = protocol.parseProtocol()
    except ValueError:
      asyncCheck request.respond(Http400, "Invalid request protocol. Got: " &
          protocol)
      continue

    var nMethod = reqMethod.normalize
    if nMethod == "post" or nMethod == "put" or nMethod == "patch":
      # Check for Expect header
      if request.headers.hasKey("Expect"):
        if request.headers["Expect"].toLower == "100-continue":
          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 parseInt(request.headers["Content-Length"], contentLength) == 0:
          await request.respond(Http400, "Bad Request. Invalid Content-Length.")
        else:
          request.body = await client.recv(contentLength)
          assert request.body.len == contentLength
      else:
        await request.respond(Http400, "Bad Request. No Content-Length.")
        continue

    case reqMethod.normalize
    of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch":
      await callback(request)
    else:
      await 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.
      discard
    else:
      request.client.close()
      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)
  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()
    asyncCheck processClient(fut.client, fut.address, callback)
    #echo(f.isNil)
    #echo(f.repr)

proc close*(server: AsyncHttpServer) =
  ## Terminates the async http server instance.
  server.socket.close()

when 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.newStringTable())

    asyncCheck server.serve(Port(5555), cb)
    runForever()
  main()