#
#
# Nimrod's Runtime Library
# (c) Copyright 2011 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This module implements a simple HTTP-Server.
##
## Example:
##
## .. code-block:: nimrod
## import strutils, sockets, httpserver
##
## var counter = 0
## proc handleRequest(client: TSocket, path, query: string): bool {.procvar.} =
## inc(counter)
## client.send("Hello for the $#th time." % $counter & wwwNL)
## return false # do not stop processing
##
## run(handleRequest, TPort(80))
##
import parseutils, strutils, os, osproc, strtabs, streams, sockets
const
wwwNL* = "\r\L"
ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL
# --------------- output messages --------------------------------------------
proc sendTextContentType(client: TSocket) =
send(client, "Content-type: text/html" & wwwNL)
send(client, wwwNL)
proc badRequest(client: TSocket) =
# Inform the client that a request it has made has a problem.
send(client, "HTTP/1.0 400 BAD REQUEST" & wwwNL)
sendTextContentType(client)
send(client, "<p>Your browser sent a bad request, " &
"such as a POST without a Content-Length." & wwwNL)
proc cannotExec(client: TSocket) =
send(client, "HTTP/1.0 500 Internal Server Error" & wwwNL)
sendTextContentType(client)
send(client, "<P>Error prohibited CGI execution." & wwwNL)
proc headers(client: TSocket, filename: string) =
# XXX could use filename to determine file type
send(client, "HTTP/1.0 200 OK" & wwwNL)
send(client, ServerSig)
sendTextContentType(client)
proc notFound(client: TSocket) =
send(client, "HTTP/1.0 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: TSocket) =
send(client, "HTTP/1.0 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 ---------------------------------------------
proc discardHeaders(client: TSocket) = skip(client)
proc serveFile*(client: TSocket, filename: string) =
## serves a file to the client.
when false: discardHeaders(client)
var f: TFile
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)
OSError()
if bytesread != bufSize: break
dealloc(buf)
close(f)
else:
notFound(client)
# ------------------ CGI execution -------------------------------------------
type
TRequestMethod = enum reqGet, reqPost
proc executeCgi(client: TSocket, path, query: string, meth: TRequestMethod) =
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 = ""
var dataAvail = false
while dataAvail:
dataAvail = recvLine(client, buf)
var L = toLower(buf)
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)
OSError()
var inp = process.inputStream
inp.writeData(inp, buf, contentLength)
dealloc(buf)
var outp = process.outputStream
while running(process) or not outp.atEnd(outp):
var line = outp.readLine()
send(client, line)
send(client, wwwNL)
# --------------- Server Setup -----------------------------------------------
proc acceptRequest(client: TSocket) =
var cgi = false
var query = ""
var buf = ""
discard recvLine(client, buf)
var path = ""
var data = buf.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
TServer* = object ## contains the current server state
socket: TSocket
port: TPort
client*: TSocket ## the socket to write the file data to
path*, query*: string ## path and query the client requested
proc open*(s: var TServer, port = TPort(80)) =
## 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: OSError()
bindAddr(s.socket, port)
listen(s.socket)
if port == TPort(0):
s.port = getSockName(s.socket)
else:
s.port = port
s.client = InvalidSocket
s.path = ""
s.query = ""
proc port*(s: var TServer): TPort =
## get the port number the server has acquired.
result = s.port
proc next*(s: var TServer) =
## proceed to the first/next request.
s.client = accept(s.socket)
headers(s.client, "")
var data = recv(s.client)
#discard recvLine(s.client, data)
var i = skipWhitespace(data)
if skipIgnoreCase(data, "GET") > 0: inc(i, 3)
elif skipIgnoreCase(data, "POST") > 0: inc(i, 4)
else:
unimplemented(s.client)
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: TServer) =
## closes the server (and the socket the server uses).
close(s.socket)
proc run*(handleRequest: proc (client: TSocket, path, query: string): bool,
port = TPort(80)) =
## encapsulates the server object and main loop
var s: TServer
open(s, port)
#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)
when isMainModule:
var counter = 0
var s: TServer
open(s, TPort(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)