scgi.nim 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2013 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 helper procs for SCGI applications. Example:
  10. ##
  11. ## .. code-block:: Nim
  12. ##
  13. ## import strtabs, sockets, scgi
  14. ##
  15. ## var counter = 0
  16. ## proc handleRequest(client: Socket, input: string,
  17. ## headers: StringTableRef): bool {.procvar.} =
  18. ## inc(counter)
  19. ## client.writeStatusOkTextContent()
  20. ## client.send("Hello for the $#th time." % $counter & "\c\L")
  21. ## return false # do not stop processing
  22. ##
  23. ## run(handleRequest)
  24. ##
  25. ## **Warning:** The API of this module is unstable, and therefore is subject
  26. ## to change.
  27. ##
  28. ## **Warning:** This module only supports the old asynchronous interface.
  29. ## You may wish to use the `asynchttpserver <asynchttpserver.html>`_
  30. ## instead for web applications.
  31. include "system/inclrtl"
  32. import sockets, strutils, os, strtabs, asyncio
  33. type
  34. ScgiError* = object of IOError ## the exception that is raised, if a SCGI error occurs
  35. proc raiseScgiError*(msg: string) {.noreturn.} =
  36. ## raises an ScgiError exception with message `msg`.
  37. var e: ref ScgiError
  38. new(e)
  39. e.msg = msg
  40. raise e
  41. proc parseWord(inp: string, outp: var string, start: int): int =
  42. result = start
  43. while inp[result] != '\0': inc(result)
  44. outp = substr(inp, start, result-1)
  45. proc parseHeaders(s: string, L: int): StringTableRef =
  46. result = newStringTable()
  47. var i = 0
  48. while i < L:
  49. var key, val: string
  50. i = parseWord(s, key, i)+1
  51. i = parseWord(s, val, i)+1
  52. result[key] = val
  53. if s[i] == ',': inc(i)
  54. else: raiseScgiError("',' after netstring expected")
  55. proc recvChar(s: Socket): char =
  56. var c: char
  57. if recv(s, addr(c), sizeof(c)) == sizeof(c):
  58. result = c
  59. type
  60. ScgiState* = object of RootObj ## SCGI state object
  61. server: Socket
  62. bufLen: int
  63. client*: Socket ## the client socket to send data to
  64. headers*: StringTableRef ## the parsed headers
  65. input*: string ## the input buffer
  66. # Async
  67. ClientMode = enum
  68. ClientReadChar, ClientReadHeaders, ClientReadContent
  69. AsyncClient = ref object
  70. c: AsyncSocket
  71. mode: ClientMode
  72. dataLen: int
  73. headers: StringTableRef ## the parsed headers
  74. input: string ## the input buffer
  75. AsyncScgiStateObj = object
  76. handleRequest: proc (client: AsyncSocket,
  77. input: string,
  78. headers: StringTableRef) {.closure, gcsafe.}
  79. asyncServer: AsyncSocket
  80. disp: Dispatcher
  81. AsyncScgiState* = ref AsyncScgiStateObj
  82. proc recvBuffer(s: var ScgiState, L: int) =
  83. if L > s.bufLen:
  84. s.bufLen = L
  85. s.input = newString(L)
  86. if L > 0 and recv(s.client, cstring(s.input), L) != L:
  87. raiseScgiError("could not read all data")
  88. setLen(s.input, L)
  89. proc open*(s: var ScgiState, port = Port(4000), address = "127.0.0.1",
  90. reuseAddr = false) =
  91. ## opens a connection.
  92. s.bufLen = 4000
  93. s.input = newString(s.bufLen) # will be reused
  94. s.server = socket()
  95. if s.server == invalidSocket: raiseOSError(osLastError())
  96. new(s.client) # Initialise s.client for `next`
  97. if s.server == invalidSocket: raiseScgiError("could not open socket")
  98. #s.server.connect(connectionName, port)
  99. if reuseAddr:
  100. s.server.setSockOpt(OptReuseAddr, true)
  101. bindAddr(s.server, port, address)
  102. listen(s.server)
  103. proc close*(s: var ScgiState) =
  104. ## closes the connection.
  105. s.server.close()
  106. proc next*(s: var ScgiState, timeout: int = -1): bool =
  107. ## proceed to the first/next request. Waits ``timeout`` milliseconds for a
  108. ## request, if ``timeout`` is `-1` then this function will never time out.
  109. ## Returns `true` if a new request has been processed.
  110. var rsocks = @[s.server]
  111. if select(rsocks, timeout) == 1 and rsocks.len == 1:
  112. new(s.client)
  113. accept(s.server, s.client)
  114. var L = 0
  115. while true:
  116. var d = s.client.recvChar()
  117. if d == '\0':
  118. s.client.close()
  119. return false
  120. if d notin strutils.Digits:
  121. if d != ':': raiseScgiError("':' after length expected")
  122. break
  123. L = L * 10 + ord(d) - ord('0')
  124. recvBuffer(s, L+1)
  125. s.headers = parseHeaders(s.input, L)
  126. if s.headers.getOrDefault("SCGI") != "1": raiseScgiError("SCGI Version 1 expected")
  127. L = parseInt(s.headers.getOrDefault("CONTENT_LENGTH"))
  128. recvBuffer(s, L)
  129. return true
  130. proc writeStatusOkTextContent*(c: Socket, contentType = "text/html") =
  131. ## sends the following string to the socket `c`::
  132. ##
  133. ## Status: 200 OK\r\LContent-Type: text/html\r\L\r\L
  134. ##
  135. ## You should send this before sending your HTML page, for example.
  136. c.send("Status: 200 OK\r\L" &
  137. "Content-Type: $1\r\L\r\L" % contentType)
  138. proc run*(handleRequest: proc (client: Socket, input: string,
  139. headers: StringTableRef): bool {.nimcall,gcsafe.},
  140. port = Port(4000)) =
  141. ## encapsulates the SCGI object and main loop.
  142. var s: ScgiState
  143. s.open(port)
  144. var stop = false
  145. while not stop:
  146. if next(s):
  147. stop = handleRequest(s.client, s.input, s.headers)
  148. s.client.close()
  149. s.close()
  150. # -- AsyncIO start
  151. proc recvBufferAsync(client: AsyncClient, L: int): ReadLineResult =
  152. result = ReadPartialLine
  153. var data = ""
  154. if L < 1:
  155. raiseScgiError("Cannot read negative or zero length: " & $L)
  156. let ret = recvAsync(client.c, data, L)
  157. if ret == 0 and data == "":
  158. client.c.close()
  159. return ReadDisconnected
  160. if ret == -1:
  161. return ReadNone # No more data available
  162. client.input.add(data)
  163. if ret == L:
  164. return ReadFullLine
  165. proc checkCloseSocket(client: AsyncClient) =
  166. if not client.c.isClosed:
  167. if client.c.isSendDataBuffered:
  168. client.c.setHandleWrite do (s: AsyncSocket):
  169. if not s.isClosed and not s.isSendDataBuffered:
  170. s.close()
  171. s.delHandleWrite()
  172. else: client.c.close()
  173. proc handleClientRead(client: AsyncClient, s: AsyncScgiState) =
  174. case client.mode
  175. of ClientReadChar:
  176. while true:
  177. var d = ""
  178. let ret = client.c.recvAsync(d, 1)
  179. if d == "" and ret == 0:
  180. # Disconnected
  181. client.c.close()
  182. return
  183. if ret == -1:
  184. return # No more data available
  185. if d[0] notin strutils.Digits:
  186. if d[0] != ':': raiseScgiError("':' after length expected")
  187. break
  188. client.dataLen = client.dataLen * 10 + ord(d[0]) - ord('0')
  189. client.mode = ClientReadHeaders
  190. handleClientRead(client, s) # Allow progression
  191. of ClientReadHeaders:
  192. let ret = recvBufferAsync(client, (client.dataLen+1)-client.input.len)
  193. case ret
  194. of ReadFullLine:
  195. client.headers = parseHeaders(client.input, client.input.len-1)
  196. if client.headers.getOrDefault("SCGI") != "1": raiseScgiError("SCGI Version 1 expected")
  197. client.input = "" # For next part
  198. let contentLen = parseInt(client.headers.getOrDefault("CONTENT_LENGTH"))
  199. if contentLen > 0:
  200. client.mode = ClientReadContent
  201. else:
  202. s.handleRequest(client.c, client.input, client.headers)
  203. checkCloseSocket(client)
  204. of ReadPartialLine, ReadDisconnected, ReadNone: return
  205. of ClientReadContent:
  206. let L = parseInt(client.headers.getOrDefault("CONTENT_LENGTH")) -
  207. client.input.len
  208. if L > 0:
  209. let ret = recvBufferAsync(client, L)
  210. case ret
  211. of ReadFullLine:
  212. s.handleRequest(client.c, client.input, client.headers)
  213. checkCloseSocket(client)
  214. of ReadPartialLine, ReadDisconnected, ReadNone: return
  215. else:
  216. s.handleRequest(client.c, client.input, client.headers)
  217. checkCloseSocket(client)
  218. proc handleAccept(sock: AsyncSocket, s: AsyncScgiState) =
  219. var client: AsyncSocket
  220. new(client)
  221. accept(s.asyncServer, client)
  222. var asyncClient = AsyncClient(c: client, mode: ClientReadChar, dataLen: 0,
  223. headers: newStringTable(), input: "")
  224. client.handleRead =
  225. proc (sock: AsyncSocket) =
  226. handleClientRead(asyncClient, s)
  227. s.disp.register(client)
  228. proc open*(handleRequest: proc (client: AsyncSocket,
  229. input: string, headers: StringTableRef) {.
  230. closure, gcsafe.},
  231. port = Port(4000), address = "127.0.0.1",
  232. reuseAddr = false): AsyncScgiState =
  233. ## Creates an ``AsyncScgiState`` object which serves as a SCGI server.
  234. ##
  235. ## After the execution of ``handleRequest`` the client socket will be closed
  236. ## automatically unless it has already been closed.
  237. var cres: AsyncScgiState
  238. new(cres)
  239. cres.asyncServer = asyncSocket()
  240. cres.asyncServer.handleAccept = proc (s: AsyncSocket) = handleAccept(s, cres)
  241. if reuseAddr:
  242. cres.asyncServer.setSockOpt(OptReuseAddr, true)
  243. bindAddr(cres.asyncServer, port, address)
  244. listen(cres.asyncServer)
  245. cres.handleRequest = handleRequest
  246. result = cres
  247. proc register*(d: Dispatcher, s: AsyncScgiState): Delegate {.discardable.} =
  248. ## Registers ``s`` with dispatcher ``d``.
  249. result = d.register(s.asyncServer)
  250. s.disp = d
  251. proc close*(s: AsyncScgiState) =
  252. ## Closes the ``AsyncScgiState``.
  253. s.asyncServer.close()
  254. when false:
  255. var counter = 0
  256. proc handleRequest(client: Socket, input: string,
  257. headers: StringTableRef): bool {.procvar.} =
  258. inc(counter)
  259. client.writeStatusOkTextContent()
  260. client.send("Hello for the $#th time." % $counter & "\c\L")
  261. return false # do not stop processing
  262. run(handleRequest)