import strutils, os, osproc, strtabs, streams, sockets const wwwNL* = "\r\L" ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL type TRequestMethod = enum reqGet, reqPost TServer* = object ## contains the current server state s: Socket job: seq[TJob] TJob* = object client: Socket process: Process # --------------- output messages -------------------------------------------- proc sendTextContentType(client: Socket) = send(client, "Content-type: text/html" & wwwNL) send(client, wwwNL) proc badRequest(client: Socket) = # 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, "

Your browser sent a bad request, " & "such as a POST without a Content-Length.

" & wwwNL) proc cannotExec(client: Socket) = send(client, "HTTP/1.0 500 Internal Server Error" & wwwNL) sendTextContentType(client) send(client, "

Error prohibited CGI execution.

" & wwwNL) proc headers(client: Socket, 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: Socket, path: string) = send(client, "HTTP/1.0 404 NOT FOUND" & wwwNL) send(client, ServerSig) sendTextContentType(client) send(client, "Not Found" & wwwNL) send(client, "

The server could not fulfill" & wwwNL) send(client, "your request because the resource " & path & "" & wwwNL) send(client, "is unavailable or nonexistent.

" & wwwNL) send(client, "" & wwwNL) proc unimplemented(client: Socket) = send(client, "HTTP/1.0 501 Method Not Implemented" & wwwNL) send(client, ServerSig) sendTextContentType(client) send(client, "Method Not Implemented" & "" & "

HTTP request method not supported.

" & "" & wwwNL) # ----------------- file serving --------------------------------------------- proc discardHeaders(client: Socket) = skip(client) proc serveFile(client: Socket, filename: string) = discardHeaders(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: let err = osLastError() dealloc(buf) close(f) raiseOSError(err) if bytesread != bufSize: break dealloc(buf) close(f) client.close() else: notFound(client, filename) # ------------------ CGI execution ------------------------------------------- proc executeCgi(server: var TServer, client: Socket, 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 = true while dataAvail: dataAvail = recvLine(client, buf) if buf.len == 0: break 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) var job: TJob job.process = process job.client = client server.job.add(job) if meth == reqPost: # get from client and post to CGI program: var buf = alloc(contentLength) if recv(client, buf, contentLength) != contentLength: let err = osLastError() dealloc(buf) raiseOSError(err) var inp = process.inputStream inp.writeData(buf, contentLength) dealloc(buf) proc animate(server: var TServer) = # checks list of jobs, removes finished ones (pretty sloppy by seq copying) var active_jobs: seq[TJob] = @[] for i in 0..server.job.len-1: var job = server.job[i] if running(job.process): active_jobs.add(job) else: # read process output stream and send it to client var outp = job.process.outputStream while true: var line = outp.readstr(1024) if line.len == 0: break else: try: send(job.client, line) except: echo("send failed, client diconnected") close(job.client) server.job = active_jobs # --------------- Server Setup ----------------------------------------------- proc acceptRequest(server: var TServer, client: Socket) = 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 path = "." & path echo("accept: " & path) 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, path) client.close() 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(server, client, path, query, meth) when isMainModule: var port = 80 var server: TServer server.job = @[] server.s = socket(AF_INET) if server.s == invalidSocket: raiseOSError(osLastError()) server.s.bindAddr(port=Port(port)) listen(server.s) echo("server up on port " & $port) while true: # check for new new connection & handle it var list: seq[Socket] = @[server.s] if select(list, 10) > 0: var client: Socket new(client) accept(server.s, client) try: acceptRequest(server, client) except: echo("failed to accept client request") # pooling events animate(server) # some slack for CPU sleep(10) server.s.close()