summary refs log tree commit diff stats
path: root/lib/pure/httpserver.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/httpserver.nim')
-rw-r--r--lib/pure/httpserver.nim259
1 files changed, 259 insertions, 0 deletions
diff --git a/lib/pure/httpserver.nim b/lib/pure/httpserver.nim
new file mode 100644
index 000000000..2c85d8137
--- /dev/null
+++ b/lib/pure/httpserver.nim
@@ -0,0 +1,259 @@
+#
+#
+#            Nimrod's Runtime Library
+#        (c) Copyright 2010 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("Hallo for the $#th time." % $counter & wwwNL)
+##    return false # do not stop processing
+##  
+##  run(handleRequest, TPort(80))
+##
+
+import 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." & 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." &
+               "</body></HTML>" & wwwNL)
+
+# ----------------- file serving ---------------------------------------------
+
+proc discardHeaders(client: TSocket) = skip(client)
+
+proc serveFile(client: TSocket, filename: string) =
+  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(copy(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: OSError()
+    var inp = process.inputStream
+    inp.writeData(inp, buf, contentLength)
+  
+  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 data = buf.split()
+  var meth = reqGet
+  if cmpIgnoreCase(data[0], "GET") == 0:
+    var q = find(data[1], '?')
+    if q >= 0:
+      cgi = true
+      query = data[1].copy(q+1)
+  elif cmpIgnoreCase(data[0], "POST") == 0:
+    cgi = true
+    meth = reqPost
+  else:
+    unimplemented(client)
+    
+  var path = data[1]
+  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
+  ## aquired 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 aquired.
+  result = s.port
+  
+proc next*(s: var TServer) = 
+  ## proceed to the first/next request.
+  s.client = accept(s.socket)
+  headers(s.client, "")
+  var buf = ""
+  discard recvLine(s.client, buf)
+  var data = buf.split()
+  if cmpIgnoreCase(data[0], "GET") == 0:
+    var q = find(data[1], '?')
+    if q >= 0:
+      s.query = data[1].copy(q+1)
+      s.path = data[1].copy(0, q-1)
+    else:
+      s.query = ""
+      s.path = data[1]
+  else:
+    unimplemented(s.client)
+
+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
+  proc handleRequest(client: TSocket, path, query: string): bool {.procvar.} =
+    inc(counter)
+    client.send("Hallo, Andreas for the $#th time." % $counter & wwwNL)
+    return false # do not stop processing
+  
+  run(handleRequest, TPort(80))
+