123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- #
- #
- # 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
- 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 = toLowerAscii(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.toLowerAscii
- 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
- 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"].toLowerAscii == "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"].toLowerAscii == "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)
|