123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 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, "<p>Your browser sent a bad request, " &
- "such as a POST without a Content-Length.</p>" & wwwNL)
- proc cannotExec(client: Socket) =
- send(client, "HTTP/1.0 500 Internal Server Error" & wwwNL)
- sendTextContentType(client)
- send(client, "<P>Error prohibited CGI execution.</p>" & 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, "<html><title>Not Found</title>" & wwwNL)
- send(client, "<body><p>The server could not fulfill" & wwwNL)
- send(client, "your request because the resource <b>" & path & "</b>" & wwwNL)
- send(client, "is unavailable or nonexistent.</p>" & wwwNL)
- send(client, "</body></html>" & wwwNL)
- proc unimplemented(client: Socket) =
- 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: 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()
|