httpserver.nim 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements a simple HTTP-Server.
  10. ##
  11. ## **Warning**: This module will soon be deprecated in favour of
  12. ## the ``asyncdispatch`` module, you should use it instead.
  13. ##
  14. ## Example:
  15. ##
  16. ## .. code-block:: nim
  17. ## import strutils, sockets, httpserver
  18. ##
  19. ## var counter = 0
  20. ## proc handleRequest(client: Socket, path, query: string): bool {.procvar.} =
  21. ## inc(counter)
  22. ## client.send("Hello for the $#th time." % $counter & wwwNL)
  23. ## return false # do not stop processing
  24. ##
  25. ## run(handleRequest, Port(80))
  26. ##
  27. import parseutils, strutils, os, osproc, strtabs, streams, sockets, asyncio
  28. const
  29. wwwNL* = "\r\L"
  30. ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL
  31. # --------------- output messages --------------------------------------------
  32. proc sendTextContentType(client: Socket) =
  33. send(client, "Content-type: text/html" & wwwNL)
  34. send(client, wwwNL)
  35. proc sendStatus(client: Socket, status: string) =
  36. send(client, "HTTP/1.1 " & status & wwwNL)
  37. proc badRequest(client: Socket) =
  38. # Inform the client that a request it has made has a problem.
  39. send(client, "HTTP/1.1 400 Bad Request" & wwwNL)
  40. sendTextContentType(client)
  41. send(client, "<p>Your browser sent a bad request, " &
  42. "such as a POST without a Content-Length.</p>" & wwwNL)
  43. when false:
  44. proc cannotExec(client: Socket) =
  45. send(client, "HTTP/1.1 500 Internal Server Error" & wwwNL)
  46. sendTextContentType(client)
  47. send(client, "<P>Error prohibited CGI execution." & wwwNL)
  48. proc headers(client: Socket, filename: string) =
  49. # XXX could use filename to determine file type
  50. send(client, "HTTP/1.1 200 OK" & wwwNL)
  51. send(client, ServerSig)
  52. sendTextContentType(client)
  53. proc notFound(client: Socket) =
  54. send(client, "HTTP/1.1 404 NOT FOUND" & wwwNL)
  55. send(client, ServerSig)
  56. sendTextContentType(client)
  57. send(client, "<html><title>Not Found</title>" & wwwNL)
  58. send(client, "<body><p>The server could not fulfill" & wwwNL)
  59. send(client, "your request because the resource specified" & wwwNL)
  60. send(client, "is unavailable or nonexistent.</p>" & wwwNL)
  61. send(client, "</body></html>" & wwwNL)
  62. proc unimplemented(client: Socket) =
  63. send(client, "HTTP/1.1 501 Method Not Implemented" & wwwNL)
  64. send(client, ServerSig)
  65. sendTextContentType(client)
  66. send(client, "<html><head><title>Method Not Implemented" &
  67. "</title></head>" &
  68. "<body><p>HTTP request method not supported.</p>" &
  69. "</body></HTML>" & wwwNL)
  70. # ----------------- file serving ---------------------------------------------
  71. when false:
  72. proc discardHeaders(client: Socket) = skip(client)
  73. proc serveFile*(client: Socket, filename: string) =
  74. ## serves a file to the client.
  75. var f: File
  76. if open(f, filename):
  77. headers(client, filename)
  78. const bufSize = 8000 # != 8K might be good for memory manager
  79. var buf = alloc(bufsize)
  80. while true:
  81. var bytesread = readBuffer(f, buf, bufsize)
  82. if bytesread > 0:
  83. var byteswritten = send(client, buf, bytesread)
  84. if bytesread != bytesWritten:
  85. dealloc(buf)
  86. close(f)
  87. raiseOSError(osLastError())
  88. if bytesread != bufSize: break
  89. dealloc(buf)
  90. close(f)
  91. else:
  92. notFound(client)
  93. # ------------------ CGI execution -------------------------------------------
  94. when false:
  95. # TODO: Fix this, or get rid of it.
  96. type
  97. RequestMethod = enum reqGet, reqPost
  98. proc executeCgi(client: Socket, path, query: string, meth: RequestMethod) =
  99. var env = newStringTable(modeCaseInsensitive)
  100. var contentLength = -1
  101. case meth
  102. of reqGet:
  103. discardHeaders(client)
  104. env["REQUEST_METHOD"] = "GET"
  105. env["QUERY_STRING"] = query
  106. of reqPost:
  107. var buf = TaintedString""
  108. var dataAvail = false
  109. while dataAvail:
  110. dataAvail = recvLine(client, buf) # TODO: This is incorrect.
  111. var L = toLowerAscii(buf.string)
  112. if L.startsWith("content-length:"):
  113. var i = len("content-length:")
  114. while L[i] in Whitespace: inc(i)
  115. contentLength = parseInt(substr(L, i))
  116. if contentLength < 0:
  117. badRequest(client)
  118. return
  119. env["REQUEST_METHOD"] = "POST"
  120. env["CONTENT_LENGTH"] = $contentLength
  121. send(client, "HTTP/1.0 200 OK" & wwwNL)
  122. var process = startProcess(command=path, env=env)
  123. if meth == reqPost:
  124. # get from client and post to CGI program:
  125. var buf = alloc(contentLength)
  126. if recv(client, buf, contentLength) != contentLength:
  127. dealloc(buf)
  128. raiseOSError()
  129. var inp = process.inputStream
  130. inp.writeData(buf, contentLength)
  131. dealloc(buf)
  132. var outp = process.outputStream
  133. var line = newStringOfCap(120).TaintedString
  134. while true:
  135. if outp.readLine(line):
  136. send(client, line.string)
  137. send(client, wwwNL)
  138. elif not running(process): break
  139. # --------------- Server Setup -----------------------------------------------
  140. proc acceptRequest(client: Socket) =
  141. var cgi = false
  142. var query = ""
  143. var buf = TaintedString""
  144. discard recvLine(client, buf)
  145. var path = ""
  146. var data = buf.string.split()
  147. var meth = reqGet
  148. var q = find(data[1], '?')
  149. # extract path
  150. if q >= 0:
  151. # strip "?..." from path, this may be found in both POST and GET
  152. path = "." & data[1].substr(0, q-1)
  153. else:
  154. path = "." & data[1]
  155. # path starts with "/", by adding "." in front of it we serve files from cwd
  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)
  170. else:
  171. when defined(Windows):
  172. var ext = splitFile(path).ext.toLowerAscii
  173. if ext == ".exe" or ext == ".cgi":
  174. # XXX: extract interpreter information here?
  175. cgi = true
  176. else:
  177. if {fpUserExec, fpGroupExec, fpOthersExec} * path.getFilePermissions != {}:
  178. cgi = true
  179. if not cgi:
  180. serveFile(client, path)
  181. else:
  182. executeCgi(client, path, query, meth)
  183. type
  184. Server* = object of RootObj ## contains the current server state
  185. socket: Socket
  186. port: Port
  187. client*: Socket ## the socket to write the file data to
  188. reqMethod*: string ## Request method. GET or POST.
  189. path*, query*: string ## path and query the client requested
  190. headers*: StringTableRef ## headers with which the client made the request
  191. body*: string ## only set with POST requests
  192. ip*: string ## ip address of the requesting client
  193. PAsyncHTTPServer* = ref AsyncHTTPServer
  194. AsyncHTTPServer = object of Server
  195. asyncSocket: AsyncSocket
  196. proc open*(s: var Server, port = Port(80), reuseAddr = false) =
  197. ## creates a new server at port `port`. If ``port == 0`` a free port is
  198. ## acquired that can be accessed later by the ``port`` proc.
  199. s.socket = socket(AF_INET)
  200. if s.socket == invalidSocket: raiseOSError(osLastError())
  201. if reuseAddr:
  202. s.socket.setSockOpt(OptReuseAddr, true)
  203. bindAddr(s.socket, port)
  204. listen(s.socket)
  205. if port == Port(0):
  206. s.port = getSockName(s.socket)
  207. else:
  208. s.port = port
  209. s.client = invalidSocket
  210. s.reqMethod = ""
  211. s.body = ""
  212. s.path = ""
  213. s.query = ""
  214. s.headers = {:}.newStringTable()
  215. proc port*(s: var Server): Port =
  216. ## get the port number the server has acquired.
  217. result = s.port
  218. proc next*(s: var Server) =
  219. ## proceed to the first/next request.
  220. var client: Socket
  221. new(client)
  222. var ip: string
  223. acceptAddr(s.socket, client, ip)
  224. s.client = client
  225. s.ip = ip
  226. s.headers = newStringTable(modeCaseInsensitive)
  227. #headers(s.client, "")
  228. var data = ""
  229. s.client.readLine(data)
  230. if data == "":
  231. # Socket disconnected
  232. s.client.close()
  233. next(s)
  234. return
  235. var header = ""
  236. while true:
  237. s.client.readLine(header)
  238. if header == "\c\L": break
  239. if header != "":
  240. var i = 0
  241. var key = ""
  242. var value = ""
  243. i = header.parseUntil(key, ':')
  244. inc(i) # skip :
  245. i += header.skipWhiteSpace(i)
  246. i += header.parseUntil(value, {'\c', '\L'}, i)
  247. s.headers[key] = value
  248. else:
  249. s.client.close()
  250. next(s)
  251. return
  252. var i = skipWhitespace(data)
  253. if skipIgnoreCase(data, "GET") > 0:
  254. s.reqMethod = "GET"
  255. inc(i, 3)
  256. elif skipIgnoreCase(data, "POST") > 0:
  257. s.reqMethod = "POST"
  258. inc(i, 4)
  259. else:
  260. unimplemented(s.client)
  261. s.client.close()
  262. next(s)
  263. return
  264. if s.reqMethod == "POST":
  265. # Check for Expect header
  266. if s.headers.hasKey("Expect"):
  267. if s.headers["Expect"].toLowerAscii == "100-continue":
  268. s.client.sendStatus("100 Continue")
  269. else:
  270. s.client.sendStatus("417 Expectation Failed")
  271. # Read the body
  272. # - Check for Content-length header
  273. if s.headers.hasKey("Content-Length"):
  274. var contentLength = 0
  275. if parseInt(s.headers["Content-Length"], contentLength) == 0:
  276. badRequest(s.client)
  277. s.client.close()
  278. next(s)
  279. return
  280. else:
  281. var totalRead = 0
  282. var totalBody = ""
  283. while totalRead < contentLength:
  284. var chunkSize = 8000
  285. if (contentLength - totalRead) < 8000:
  286. chunkSize = (contentLength - totalRead)
  287. var bodyData = newString(chunkSize)
  288. var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
  289. if octetsRead <= 0:
  290. s.client.close()
  291. next(s)
  292. return
  293. totalRead += octetsRead
  294. totalBody.add(bodyData)
  295. if totalBody.len != contentLength:
  296. s.client.close()
  297. next(s)
  298. return
  299. s.body = totalBody
  300. else:
  301. badRequest(s.client)
  302. s.client.close()
  303. next(s)
  304. return
  305. var L = skipWhitespace(data, i)
  306. inc(i, L)
  307. # XXX we ignore "HTTP/1.1" etc. for now here
  308. var query = 0
  309. var last = i
  310. while last < data.len and data[last] notin Whitespace:
  311. if data[last] == '?' and query == 0: query = last
  312. inc(last)
  313. if query > 0:
  314. s.query = data.substr(query+1, last-1)
  315. s.path = data.substr(i, query-1)
  316. else:
  317. s.query = ""
  318. s.path = data.substr(i, last-1)
  319. proc close*(s: Server) =
  320. ## closes the server (and the socket the server uses).
  321. close(s.socket)
  322. proc run*(handleRequest: proc (client: Socket,
  323. path, query: string): bool {.closure.},
  324. port = Port(80)) =
  325. ## encapsulates the server object and main loop
  326. var s: Server
  327. open(s, port, reuseAddr = true)
  328. #echo("httpserver running on port ", s.port)
  329. while true:
  330. next(s)
  331. if handleRequest(s.client, s.path, s.query): break
  332. close(s.client)
  333. close(s)
  334. # -- AsyncIO begin
  335. proc nextAsync(s: PAsyncHTTPServer) =
  336. ## proceed to the first/next request.
  337. var client: Socket
  338. new(client)
  339. var ip: string
  340. acceptAddr(getSocket(s.asyncSocket), client, ip)
  341. s.client = client
  342. s.ip = ip
  343. s.headers = newStringTable(modeCaseInsensitive)
  344. #headers(s.client, "")
  345. var data = ""
  346. s.client.readLine(data)
  347. if data == "":
  348. # Socket disconnected
  349. s.client.close()
  350. return
  351. var header = ""
  352. while true:
  353. s.client.readLine(header) # TODO: Very inefficient here. Prone to DOS.
  354. if header == "\c\L": break
  355. if header != "":
  356. var i = 0
  357. var key = ""
  358. var value = ""
  359. i = header.parseUntil(key, ':')
  360. inc(i) # skip :
  361. if i < header.len:
  362. i += header.skipWhiteSpace(i)
  363. i += header.parseUntil(value, {'\c', '\L'}, i)
  364. s.headers[key] = value
  365. else:
  366. s.client.close()
  367. return
  368. var i = skipWhitespace(data)
  369. if skipIgnoreCase(data, "GET") > 0:
  370. s.reqMethod = "GET"
  371. inc(i, 3)
  372. elif skipIgnoreCase(data, "POST") > 0:
  373. s.reqMethod = "POST"
  374. inc(i, 4)
  375. else:
  376. unimplemented(s.client)
  377. s.client.close()
  378. return
  379. if s.reqMethod == "POST":
  380. # Check for Expect header
  381. if s.headers.hasKey("Expect"):
  382. if s.headers["Expect"].toLowerAscii == "100-continue":
  383. s.client.sendStatus("100 Continue")
  384. else:
  385. s.client.sendStatus("417 Expectation Failed")
  386. # Read the body
  387. # - Check for Content-length header
  388. if s.headers.hasKey("Content-Length"):
  389. var contentLength = 0
  390. if parseInt(s.headers["Content-Length"], contentLength) == 0:
  391. badRequest(s.client)
  392. s.client.close()
  393. return
  394. else:
  395. var totalRead = 0
  396. var totalBody = ""
  397. while totalRead < contentLength:
  398. var chunkSize = 8000
  399. if (contentLength - totalRead) < 8000:
  400. chunkSize = (contentLength - totalRead)
  401. var bodyData = newString(chunkSize)
  402. var octetsRead = s.client.recv(cstring(bodyData), chunkSize)
  403. if octetsRead <= 0:
  404. s.client.close()
  405. return
  406. totalRead += octetsRead
  407. totalBody.add(bodyData)
  408. if totalBody.len != contentLength:
  409. s.client.close()
  410. return
  411. s.body = totalBody
  412. else:
  413. badRequest(s.client)
  414. s.client.close()
  415. return
  416. var L = skipWhitespace(data, i)
  417. inc(i, L)
  418. # XXX we ignore "HTTP/1.1" etc. for now here
  419. var query = 0
  420. var last = i
  421. while last < data.len and data[last] notin Whitespace:
  422. if data[last] == '?' and query == 0: query = last
  423. inc(last)
  424. if query > 0:
  425. s.query = data.substr(query+1, last-1)
  426. s.path = data.substr(i, query-1)
  427. else:
  428. s.query = ""
  429. s.path = data.substr(i, last-1)
  430. proc asyncHTTPServer*(handleRequest: proc (server: PAsyncHTTPServer, client: Socket,
  431. path, query: string): bool {.closure, gcsafe.},
  432. port = Port(80), address = "",
  433. reuseAddr = false): PAsyncHTTPServer =
  434. ## Creates an Asynchronous HTTP server at ``port``.
  435. var capturedRet: PAsyncHTTPServer
  436. new(capturedRet)
  437. capturedRet.asyncSocket = asyncSocket()
  438. capturedRet.asyncSocket.handleAccept =
  439. proc (s: AsyncSocket) =
  440. nextAsync(capturedRet)
  441. let quit = handleRequest(capturedRet, capturedRet.client, capturedRet.path,
  442. capturedRet.query)
  443. if quit: capturedRet.asyncSocket.close()
  444. if reuseAddr:
  445. capturedRet.asyncSocket.setSockOpt(OptReuseAddr, true)
  446. capturedRet.asyncSocket.bindAddr(port, address)
  447. capturedRet.asyncSocket.listen()
  448. if port == Port(0):
  449. capturedRet.port = getSockName(capturedRet.asyncSocket)
  450. else:
  451. capturedRet.port = port
  452. capturedRet.client = invalidSocket
  453. capturedRet.reqMethod = ""
  454. capturedRet.body = ""
  455. capturedRet.path = ""
  456. capturedRet.query = ""
  457. capturedRet.headers = {:}.newStringTable()
  458. result = capturedRet
  459. proc register*(d: Dispatcher, s: PAsyncHTTPServer) =
  460. ## Registers a ``PAsyncHTTPServer`` with a ``Dispatcher``.
  461. d.register(s.asyncSocket)
  462. proc close*(h: PAsyncHTTPServer) =
  463. ## Closes the ``PAsyncHTTPServer``.
  464. h.asyncSocket.close()
  465. when not defined(testing) and isMainModule:
  466. var counter = 0
  467. var s: Server
  468. open(s, Port(0))
  469. echo("httpserver running on port ", s.port)
  470. while true:
  471. next(s)
  472. inc(counter)
  473. s.client.send("Hello, Andreas, for the $#th time. $# ? $#" % [
  474. $counter, s.path, s.query] & wwwNL)
  475. close(s.client)
  476. close(s)