httpserver2.nim 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import strutils, os, osproc, strtabs, streams, sockets
  2. const
  3. wwwNL* = "\r\L"
  4. ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL
  5. type
  6. TRequestMethod = enum reqGet, reqPost
  7. TServer* = object ## contains the current server state
  8. s: Socket
  9. job: seq[TJob]
  10. TJob* = object
  11. client: Socket
  12. process: Process
  13. # --------------- output messages --------------------------------------------
  14. proc sendTextContentType(client: Socket) =
  15. send(client, "Content-type: text/html" & wwwNL)
  16. send(client, wwwNL)
  17. proc badRequest(client: Socket) =
  18. # Inform the client that a request it has made has a problem.
  19. send(client, "HTTP/1.0 400 BAD REQUEST" & wwwNL)
  20. sendTextContentType(client)
  21. send(client, "<p>Your browser sent a bad request, " &
  22. "such as a POST without a Content-Length.</p>" & wwwNL)
  23. proc cannotExec(client: Socket) =
  24. send(client, "HTTP/1.0 500 Internal Server Error" & wwwNL)
  25. sendTextContentType(client)
  26. send(client, "<P>Error prohibited CGI execution.</p>" & wwwNL)
  27. proc headers(client: Socket, filename: string) =
  28. # XXX could use filename to determine file type
  29. send(client, "HTTP/1.0 200 OK" & wwwNL)
  30. send(client, ServerSig)
  31. sendTextContentType(client)
  32. proc notFound(client: Socket, path: string) =
  33. send(client, "HTTP/1.0 404 NOT FOUND" & wwwNL)
  34. send(client, ServerSig)
  35. sendTextContentType(client)
  36. send(client, "<html><title>Not Found</title>" & wwwNL)
  37. send(client, "<body><p>The server could not fulfill" & wwwNL)
  38. send(client, "your request because the resource <b>" & path & "</b>" & wwwNL)
  39. send(client, "is unavailable or nonexistent.</p>" & wwwNL)
  40. send(client, "</body></html>" & wwwNL)
  41. proc unimplemented(client: Socket) =
  42. send(client, "HTTP/1.0 501 Method Not Implemented" & wwwNL)
  43. send(client, ServerSig)
  44. sendTextContentType(client)
  45. send(client, "<html><head><title>Method Not Implemented" &
  46. "</title></head>" &
  47. "<body><p>HTTP request method not supported.</p>" &
  48. "</body></HTML>" & wwwNL)
  49. # ----------------- file serving ---------------------------------------------
  50. proc discardHeaders(client: Socket) = skip(client)
  51. proc serveFile(client: Socket, filename: string) =
  52. discardHeaders(client)
  53. var f: File
  54. if open(f, filename):
  55. headers(client, filename)
  56. const bufSize = 8000 # != 8K might be good for memory manager
  57. var buf = alloc(bufsize)
  58. while true:
  59. var bytesread = readBuffer(f, buf, bufsize)
  60. if bytesread > 0:
  61. var byteswritten = send(client, buf, bytesread)
  62. if bytesread != bytesWritten:
  63. let err = osLastError()
  64. dealloc(buf)
  65. close(f)
  66. raiseOSError(err)
  67. if bytesread != bufSize: break
  68. dealloc(buf)
  69. close(f)
  70. client.close()
  71. else:
  72. notFound(client, filename)
  73. # ------------------ CGI execution -------------------------------------------
  74. proc executeCgi(server: var TServer, client: Socket, path, query: string,
  75. meth: TRequestMethod) =
  76. var env = newStringTable(modeCaseInsensitive)
  77. var contentLength = -1
  78. case meth
  79. of reqGet:
  80. discardHeaders(client)
  81. env["REQUEST_METHOD"] = "GET"
  82. env["QUERY_STRING"] = query
  83. of reqPost:
  84. var buf = ""
  85. var dataAvail = true
  86. while dataAvail:
  87. dataAvail = recvLine(client, buf)
  88. if buf.len == 0:
  89. break
  90. var L = toLower(buf)
  91. if L.startsWith("content-length:"):
  92. var i = len("content-length:")
  93. while L[i] in Whitespace: inc(i)
  94. contentLength = parseInt(substr(L, i))
  95. if contentLength < 0:
  96. badRequest(client)
  97. return
  98. env["REQUEST_METHOD"] = "POST"
  99. env["CONTENT_LENGTH"] = $contentLength
  100. send(client, "HTTP/1.0 200 OK" & wwwNL)
  101. var process = startProcess(command=path, env=env)
  102. var job: TJob
  103. job.process = process
  104. job.client = client
  105. server.job.add(job)
  106. if meth == reqPost:
  107. # get from client and post to CGI program:
  108. var buf = alloc(contentLength)
  109. if recv(client, buf, contentLength) != contentLength:
  110. let err = osLastError()
  111. dealloc(buf)
  112. raiseOSError(err)
  113. var inp = process.inputStream
  114. inp.writeData(buf, contentLength)
  115. dealloc(buf)
  116. proc animate(server: var TServer) =
  117. # checks list of jobs, removes finished ones (pretty sloppy by seq copying)
  118. var active_jobs: seq[TJob] = @[]
  119. for i in 0..server.job.len-1:
  120. var job = server.job[i]
  121. if running(job.process):
  122. active_jobs.add(job)
  123. else:
  124. # read process output stream and send it to client
  125. var outp = job.process.outputStream
  126. while true:
  127. var line = outp.readstr(1024)
  128. if line.len == 0:
  129. break
  130. else:
  131. try:
  132. send(job.client, line)
  133. except:
  134. echo("send failed, client diconnected")
  135. close(job.client)
  136. server.job = active_jobs
  137. # --------------- Server Setup -----------------------------------------------
  138. proc acceptRequest(server: var TServer, client: Socket) =
  139. var cgi = false
  140. var query = ""
  141. var buf = ""
  142. discard recvLine(client, buf)
  143. var path = ""
  144. var data = buf.split()
  145. var meth = reqGet
  146. var q = find(data[1], '?')
  147. # extract path
  148. if q >= 0:
  149. # strip "?..." from path, this may be found in both POST and GET
  150. path = data[1].substr(0, q-1)
  151. else:
  152. path = data[1]
  153. # path starts with "/", by adding "." in front of it we serve files from cwd
  154. path = "." & path
  155. echo("accept: " & path)
  156. if cmpIgnoreCase(data[0], "GET") == 0:
  157. if q >= 0:
  158. cgi = true
  159. query = data[1].substr(q+1)
  160. elif cmpIgnoreCase(data[0], "POST") == 0:
  161. cgi = true
  162. meth = reqPost
  163. else:
  164. unimplemented(client)
  165. if path[path.len-1] == '/' or existsDir(path):
  166. path = path / "index.html"
  167. if not existsFile(path):
  168. discardHeaders(client)
  169. notFound(client, path)
  170. client.close()
  171. else:
  172. when defined(Windows):
  173. var ext = splitFile(path).ext.toLower
  174. if ext == ".exe" or ext == ".cgi":
  175. # XXX: extract interpreter information here?
  176. cgi = true
  177. else:
  178. if {fpUserExec, fpGroupExec, fpOthersExec} * path.getFilePermissions != {}:
  179. cgi = true
  180. if not cgi:
  181. serveFile(client, path)
  182. else:
  183. executeCgi(server, client, path, query, meth)
  184. when isMainModule:
  185. var port = 80
  186. var server: TServer
  187. server.job = @[]
  188. server.s = socket(AF_INET)
  189. if server.s == invalidSocket: raiseOSError(osLastError())
  190. server.s.bindAddr(port=Port(port))
  191. listen(server.s)
  192. echo("server up on port " & $port)
  193. while true:
  194. # check for new new connection & handle it
  195. var list: seq[Socket] = @[server.s]
  196. if select(list, 10) > 0:
  197. var client: Socket
  198. new(client)
  199. accept(server.s, client)
  200. try:
  201. acceptRequest(server, client)
  202. except:
  203. echo("failed to accept client request")
  204. # pooling events
  205. animate(server)
  206. # some slack for CPU
  207. sleep(10)
  208. server.s.close()