#
#
# Nim's Runtime Library
# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This module implements a simple HTTP-Server.
##
## **Warning**: This module will soon be deprecated in favour of
## the ``asyncdispatch`` module, you should use it instead.
##
## Example:
##
## .. code-block:: nim
## import strutils, sockets, httpserver
##
## var counter = 0
## proc handleRequest(client: Socket, path, query: string): bool {.procvar.} =
## inc(counter)
## client.send("Hello for the $#th time." % $counter & wwwNL)
## return false # do not stop processing
##
## run(handleRequest, Port(80))
##
import parseutils, strutils, os, osproc, strtabs, streams, sockets, asyncio
const
wwwNL* = "\r\L"
ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL
# --------------- output messages --------------------------------------------
proc sendTextContentType(client: Socket) =
send(client, "Content-type: text/html" & wwwNL)
send(client, wwwNL)
proc sendStatus(client: Socket, status: string) =
send(client, "HTTP/1.1 " & status & wwwNL)
proc badRequest(client: Socket) =
# Inform the client that a request it has made has a problem.
send(client, "HTTP/1.1 400 Bad Request" & wwwNL)
sendTextContentType(client)
send(client, "<p>Your browser sent a bad request, " &
"such as a POST without a Content-Length.</p>" & wwwNL)
when false:
proc cannotExec(client: Socket) =
send(client, "HTTP/1.1 500 Internal Server Error" & wwwNL)
sendTextContentType(client)
send(client, "<P>Error prohibited CGI execution." & wwwNL)
proc headers(client: Socket, filename: string) =
# XXX could use filename to determine file type
send(client, "HTTP/1.1 200 OK" & wwwNL)
send(client, ServerSig)
sendTextContentType(client)
proc notFound(client: Socket) =
send(client, "HTTP/1.1 404 NOT FOUND" & wwwNL)
send(client, ServerSig)
sendTextContentType(client)
send(client, "<html><title>Not Found</title>" & wwwNL)
send(client, "<body><p>The server could not fulfill" & wwwNL)
send(client, "your request because the resource specified" & wwwNL)
send(client, "is unavailable or nonexistent.</p>" & wwwNL)
send(client, "</body></html>" & wwwNL)
proc unimplemented(client: Socket) =
send(client, "HTTP/1.1 501 Method Not Implemented" & wwwNL)
send(client, ServerSig)
sendTextContentType(client)
send(client, "<html><head><title>Method Not Implemented" &
"</title></head>" &
"<body><p>HTTP request method not supported.</p>" &
"</body></HTML>" & wwwNL)
# ----------------- file serving ---------------------------------------------
when false:
proc discardHeaders(client: Socket) = skip(client)
proc serveFile*(client: Socket, filename: string) =
## serves a file to the client.
var f: File
if open(f, filename):
headers(client, filename)
const bufSize = 8000 # != 8K might be good for memory manager
var buf = alloc(bufsize)
while true:
var bytesread = readBuffer(f, buf, bufsize)
if bytesread > 0:
var byteswritten = send(client, buf, bytesread)
if bytesread != bytesWritten:
dealloc(buf)
close(f)
raiseOSError(osLastError())
if bytesread != bufSize: break
dealloc(buf)
close(f)
else:
notFound(client)
# ------------------ CGI execution -------------------------------------------
when false:
# TODO: Fix this, or get rid of it.
type
RequestMethod = enum reqGet, reqPost
{.deprecated: [TRequestMethod: RequestMethod].}
proc executeCgi(client: Socket, path, query: string, meth: RequestMethod) =
var env = newStringTable(modeCaseInsensitive)
var contentLength = -1
case meth
of reqGet:
discardHeaders(client)
env["REQUEST_METHOD"] = "GET"
env["QUERY_STRING"] = query
of reqPost:
var buf = TaintedString""
var dataAvail = false
while dataAvail:
dataAvail = recvLine(client, buf) # TODO: This is incorrect.
var L = toLower(buf.string)
if L.startsWith("content-length:"):
var i = len("content-length:")
while L[i] in Whitespace: inc(i)
contentLength = parseInt(substr(L, i))
if contentLength < 0:
badRequest(client)
return
env["REQUEST_METHOD"] = "POST"
env["CONTENT_LENGTH"] = $contentLength
send(client, "HTTP/1.0 200 OK" & wwwNL)
var process = startProcess(command=path, env=env)
if meth == reqPost:
# get from client and post to CGI program:
var buf = alloc(contentLength)
if recv(client, buf, contentLength) != contentLength:
dealloc(buf)
raiseOSError()
var inp = process.inputStream
inp.writeData(buf, contentLength)
dealloc(buf)
var outp = process.outputStream
var line = newStringOfCap(120).TaintedString
while true:
if outp.readLine(line):
send(client, line.string)
send(client, wwwNL)
elif not running(process): break
# --------------- Server Setup -----------------------------------------------
proc acceptRequest(client: Socket) =
var cgi = false
var query = ""
var buf = TaintedString""
discard recvLine(client, buf)
var path = ""
var data = buf.string.split()
var meth = reqGet
var q = find(data[1], '?')
# extract path
if q >= 0:
# strip "?..." from path, this may be found in both POST and GET
path = "." & data[1].substr(0, q-1)
else:
path = "." & data[1]
# path starts with "/", by adding "." in front of it we serve files from cwd
if cmpIgnoreCase(data[0], "GET") == 0:
if q >= 0:
cgi = true
query = data[1].substr(q+1)
elif cmpIgnoreCase(data[0], "POST") == 0:
cgi = true
meth = reqPost
else:
unimplemented(client)
if path[path.len-1] == '/' or existsDir(path):
path = path / "index.html"
if not existsFile(path):
discardHeaders(client)
notFound(client)
else:
when defined(Windows):
var ext = splitFile(path).ext.toLower
if ext == ".exe" or ext == ".cgi":
# XXX: extract interpreter information here?
cgi = true
else:
if {fpUserExec, fpGroupExec, fpOthersExec} * path.getFilePermissions != {}:
cgi = true
if not cgi:
serveFile(client, path)
else:
executeCgi(client, path, query, meth)
type
Server* = object of RootObj ## contains the current server state
socket: Socket
port: Port
client*: Socket ## the socket to write the file data to
reqMethod*: string ## Request method. GET or POST.
path*, query*: string ## path and query the client requested
headers*: StringTableRef ## headers with which the client made the request
body*: string ## only set with POST requests
ip*: string ## ip address of the requesting client
PAsyncHTTPServer* = ref AsyncHTTPServer
AsyncHTTPServer = object of Server
asyncSocket: AsyncSocket
{.deprecated: [TAsyncHTTPServer: AsyncHTTPServer, TServer: Server].}
proc open*(s: var Server, port = Port(80), reuseAddr = false) =
## creates a new server at port `port`. If ``port == 0`` a free port is
## acquired that can be accessed later by the ``port`` proc.
s.socket = socket(AF_INET)
if s.socket == invalidSocket: raiseOSError(osLastError())
if reuseAddr:
s.socket.setSockOpt(OptReuseAddr, true)
bindAddr(s.socket, port)
listen(s.socket)
if port == Port(0):
s.port = getSockName(s.socket)
else:
s.port = port
s.client = invalidSocket
s.reqMethod = ""
s.body = ""
s.path = ""
s.query = ""
s.headers = {:}.newStringTable()
proc port*(s: var Server): Port =
## get the port number the server has acquired.
result = s.port
proc next*(s: var Server) =
## proceed to the first/next request.
var client: Socket
new(client)
var ip: string
acceptAddr(s.socket, client, ip)
s.client = client
s.ip = ip
s.headers = newStringTable(modeCaseInsensitive)
#headers(s.client, "")
var data = ""
s.client.readLine(data)
if data == "":
# Socket disconnected
s.client.close()
next(s)
return
var header = ""
while true:
s.client.readLine(header)
if header == "\c\L": break
if header != "":
var i = 0
var key = ""
var value = ""
i = header.parseUntil(key, ':')
inc(i) # skip :
i += header.skipWhiteSpace(i)
i += header.parseUntil(value, {'\c', '\L'}, i)
s.headers[key] = value
else:
s.client.close()
next(s)
return
var i = skipWhitespace(data)
if skipIgnoreCase(data, "GET") > 0:
s.reqMethod = "GET"
inc(i, 3)
elif skipIgnoreCase(data, "POST") > 0:
s.reqMethod = "POST"
inc(i, 4)
else:
unimplemented(s.client)
s.client.close()
next(s)
return
if s.reqMethod == "POST":
# Check for Expect header
if s.headers.hasKey("Expect"):
if s.headers["Expect"].toLower == "100-continue":
s.client.sendStatus("100 Continue")
else:
s.client.sendStatus("417 Expectation Failed")
# Read the body
# - Check for Content-length header
if s.headers.hasKey("Content-Length"):
var contentLength = 0
if parseInt(s.headers["Content-Length"], contentLength) == 0:
badRequest(s.client)
s.client.close()
next(s)
return
else:
var totalRead = 0
var totalBody = ""
while totalRead < contentLength:
var chunkSize = 8000
if (contentLength - totalRead) < 8000:
chunkSize = (contentLength - totalRead)
var bodyData = newString(chunkSize)
var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
if octetsRead <= 0:
s.client.close()
next(s)
return
totalRead += octetsRead
totalBody.add(bodyData)
if totalBody.len != contentLength:
s.client.close()
next(s)
return
s.body = totalBody
else:
badRequest(s.client)
s.client.close()
next(s)
return
var L = skipWhitespace(data, i)
inc(i, L)
# XXX we ignore "HTTP/1.1" etc. for now here
var query = 0
var last = i
while last < data.len and data[last] notin Whitespace:
if data[last] == '?' and query == 0: query = last
inc(last)
if query > 0:
s.query = data.substr(query+1, last-1)
s.path = data.substr(i, query-1)
else:
s.query = ""
s.path = data.substr(i, last-1)
proc close*(s: Server) =
## closes the server (and the socket the server uses).
close(s.socket)
proc run*(handleRequest: proc (client: Socket,
path, query: string): bool {.closure.},
port = Port(80)) =
## encapsulates the server object and main loop
var s: Server
open(s, port, reuseAddr = true)
#echo("httpserver running on port ", s.port)
while true:
next(s)
if handleRequest(s.client, s.path, s.query): break
close(s.client)
close(s)
# -- AsyncIO begin
proc nextAsync(s: PAsyncHTTPServer) =
## proceed to the first/next request.
var client: Socket
new(client)
var ip: string
acceptAddr(getSocket(s.asyncSocket), client, ip)
s.client = client
s.ip = ip
s.headers = newStringTable(modeCaseInsensitive)
#headers(s.client, "")
var data = ""
s.client.readLine(data)
if data == "":
# Socket disconnected
s.client.close()
return
var header = ""
while true:
s.client.readLine(header) # TODO: Very inefficient here. Prone to DOS.
if header == "\c\L": break
if header != "":
var i = 0
var key = ""
var value = ""
i = header.parseUntil(key, ':')
inc(i) # skip :
if i < header.len:
i += header.skipWhiteSpace(i)
i += header.parseUntil(value, {'\c', '\L'}, i)
s.headers[key] = value
else:
s.client.close()
return
var i = skipWhitespace(data)
if skipIgnoreCase(data, "GET") > 0:
s.reqMethod = "GET"
inc(i, 3)
elif skipIgnoreCase(data, "POST") > 0:
s.reqMethod = "POST"
inc(i, 4)
else:
unimplemented(s.client)
s.client.close()
return
if s.reqMethod == "POST":
# Check for Expect header
if s.headers.hasKey("Expect"):
if s.headers["Expect"].toLower == "100-continue":
s.client.sendStatus("100 Continue")
else:
s.client.sendStatus("417 Expectation Failed")
# Read the body
# - Check for Content-length header
if s.headers.hasKey("Content-Length"):
var contentLength = 0
if parseInt(s.headers["Content-Length"], contentLength) == 0:
badRequest(s.client)
s.client.close()
return
else:
var totalRead = 0
var totalBody = ""
while totalRead < contentLength:
var chunkSize = 8000
if (contentLength - totalRead) < 8000:
chunkSize = (contentLength - totalRead)
var bodyData = newString(chunkSize)
var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
if octetsRead <= 0:
s.client.close()
return
totalRead += octetsRead
totalBody.add(bodyData)
if totalBody.len != contentLength:
s.client.close()
return
s.body = totalBody
else:
badRequest(s.client)
s.client.close()
return
var L = skipWhitespace(data, i)
inc(i, L)
# XXX we ignore "HTTP/1.1" etc. for now here
var query = 0
var last = i
while last < data.len and data[last] notin Whitespace:
if data[last] == '?' and query == 0: query = last
inc(last)
if query > 0:
s.query = data.substr(query+1, last-1)
s.path = data.substr(i, query-1)
else:
s.query = ""
s.path = data.substr(i, last-1)
proc asyncHTTPServer*(handleRequest: proc (server: PAsyncHTTPServer, client: Socket,
path, query: string): bool {.closure, gcsafe.},
port = Port(80), address = "",
reuseAddr = false): PAsyncHTTPServer =
## Creates an Asynchronous HTTP server at ``port``.
var capturedRet: PAsyncHTTPServer
new(capturedRet)
capturedRet.asyncSocket = asyncSocket()
capturedRet.asyncSocket.handleAccept =
proc (s: AsyncSocket) =
nextAsync(capturedRet)
let quit = handleRequest(capturedRet, capturedRet.client, capturedRet.path,
capturedRet.query)
if quit: capturedRet.asyncSocket.close()
if reuseAddr:
capturedRet.asyncSocket.setSockOpt(OptReuseAddr, true)
capturedRet.asyncSocket.bindAddr(port, address)
capturedRet.asyncSocket.listen()
if port == Port(0):
capturedRet.port = getSockName(capturedRet.asyncSocket)
else:
capturedRet.port = port
capturedRet.client = invalidSocket
capturedRet.reqMethod = ""
capturedRet.body = ""
capturedRet.path = ""
capturedRet.query = ""
capturedRet.headers = {:}.newStringTable()
result = capturedRet
proc register*(d: Dispatcher, s: PAsyncHTTPServer) =
## Registers a ``PAsyncHTTPServer`` with a ``Dispatcher``.
d.register(s.asyncSocket)
proc close*(h: PAsyncHTTPServer) =
## Closes the ``PAsyncHTTPServer``.
h.asyncSocket.close()
when not defined(testing) and isMainModule:
var counter = 0
var s: Server
open(s, Port(0))
echo("httpserver running on port ", s.port)
while true:
next(s)
inc(counter)
s.client.send("Hello, Andreas, for the $#th time. $# ? $#" % [
$counter, s.path, s.query] & wwwNL)
close(s.client)
close(s)