mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-31 18:32:11 +00:00
295 lines
9.3 KiB
Nim
295 lines
9.3 KiB
Nim
#
|
|
#
|
|
# 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.
|
|
##
|
|
## 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
|
|
## import asynchttpserver, asyncdispatch
|
|
##
|
|
## var server = newAsyncHttpServer()
|
|
## proc cb(req: Request) {.async.} =
|
|
## await req.respond(Http200, "Hello World")
|
|
##
|
|
## waitFor server.serve(Port(8080), cb)
|
|
|
|
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",
|
|
Http412 = "412 Precondition Failed",
|
|
Http413 = "413 Request Entity Too Large",
|
|
Http414 = "414 Request-URI Too Long",
|
|
Http415 = "415 Unsupported Media Type",
|
|
Http416 = "416 Requested Range Not Satisfiable",
|
|
Http417 = "417 Expectation Failed",
|
|
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: StringTableRef = nil): Future[void] =
|
|
## Responds to the request with the specified ``HttpCode``, headers and
|
|
## content.
|
|
##
|
|
## This procedure will **not** close the client socket.
|
|
var msg = "HTTP/1.1 " & $code & "\c\L"
|
|
|
|
if headers != nil:
|
|
msg.addHeaders(headers)
|
|
msg.add("Content-Length: " & $content.len & "\c\L\c\L")
|
|
msg.add(content)
|
|
result = req.client.send(msg)
|
|
|
|
proc parseHeader(line: string): tuple[key, value: string] =
|
|
var i = 0
|
|
i = line.parseUntil(result.key, ':')
|
|
inc(i) # skip :
|
|
if i < len(line):
|
|
i += line.skipWhiteSpace(i)
|
|
i += line.parseUntil(result.value, {'\c', '\L'}, i)
|
|
else:
|
|
result.value = ""
|
|
|
|
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.} =
|
|
var request: Request
|
|
request.url = initUri()
|
|
request.headers = newStringTable(modeCaseInsensitive)
|
|
var lineFut = newFutureVar[string]("asynchttpserver.processClient")
|
|
lineFut.mget() = newStringOfCap(80)
|
|
var key, value = ""
|
|
|
|
while not client.isClosed:
|
|
# GET /path HTTP/1.1
|
|
# Header: val
|
|
# \n
|
|
request.headers.clear(modeCaseInsensitive)
|
|
request.body = ""
|
|
request.hostname.shallowCopy(address)
|
|
assert client != nil
|
|
request.client = client
|
|
|
|
# First line - GET /path HTTP/1.1
|
|
lineFut.mget().setLen(0)
|
|
lineFut.clean()
|
|
await client.recvLineInto(lineFut) # TODO: Timeouts.
|
|
if lineFut.mget == "":
|
|
client.close()
|
|
return
|
|
|
|
var i = 0
|
|
for linePart in lineFut.mget.split(' '):
|
|
case i
|
|
of 0: request.reqMethod.shallowCopy(linePart.normalize)
|
|
of 1: parseUri(linePart, request.url)
|
|
of 2:
|
|
try:
|
|
request.protocol = parseProtocol(linePart)
|
|
except ValueError:
|
|
asyncCheck request.respond(Http400,
|
|
"Invalid request protocol. Got: " & linePart)
|
|
continue
|
|
else:
|
|
await request.respond(Http400, "Invalid request. Got: " & lineFut.mget)
|
|
continue
|
|
inc i
|
|
|
|
# Headers
|
|
while true:
|
|
i = 0
|
|
lineFut.mget.setLen(0)
|
|
lineFut.clean()
|
|
await client.recvLineInto(lineFut)
|
|
|
|
if lineFut.mget == "":
|
|
client.close(); return
|
|
if lineFut.mget == "\c\L": break
|
|
let (key, value) = parseHeader(lineFut.mget)
|
|
request.headers[key] = value
|
|
|
|
if request.reqMethod == "post":
|
|
# Check for Expect header
|
|
if request.headers.hasKey("Expect"):
|
|
if request.headers.getOrDefault("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.getOrDefault("Content-Length"),
|
|
contentLength) == 0:
|
|
await request.respond(Http400, "Bad Request. Invalid Content-Length.")
|
|
continue
|
|
else:
|
|
request.body = await client.recv(contentLength)
|
|
assert request.body.len == contentLength
|
|
else:
|
|
await request.respond(Http400, "Bad Request. No Content-Length.")
|
|
continue
|
|
|
|
case request.reqMethod
|
|
of "get", "post", "head", "put", "delete", "trace", "options",
|
|
"connect", "patch":
|
|
await callback(request)
|
|
else:
|
|
await request.respond(Http400, "Invalid request method. Got: " &
|
|
request.reqMethod)
|
|
|
|
# Persistent connections
|
|
if (request.protocol == HttpVer11 and
|
|
request.headers.getOrDefault("connection").normalize != "close") or
|
|
(request.protocol == HttpVer10 and
|
|
request.headers.getOrDefault("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 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.newStringTable())
|
|
|
|
asyncCheck server.serve(Port(5555), cb)
|
|
runForever()
|
|
main()
|