# # # Nim's Runtime Library # (c) Copyright 2013 Andreas Rumpf, Dominik Picheta # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## This module implements helper procs for SCGI applications. Example: ## ## .. code-block:: Nim ## ## import strtabs, sockets, scgi ## ## var counter = 0 ## proc handleRequest(client: Socket, input: string, ## headers: StringTableRef): bool {.procvar.} = ## inc(counter) ## client.writeStatusOkTextContent() ## client.send("Hello for the $#th time." % $counter & "\c\L") ## return false # do not stop processing ## ## run(handleRequest) ## ## **Warning:** The API of this module is unstable, and therefore is subject ## to change. ## ## **Warning:** This module only supports the old asynchronous interface. ## You may wish to use the `asynchttpserver `_ ## instead for web applications. include "system/inclrtl" import sockets, strutils, os, strtabs, asyncio type ScgiError* = object of IOError ## the exception that is raised, if a SCGI error occurs proc raiseScgiError*(msg: string) {.noreturn.} = ## raises an ScgiError exception with message `msg`. var e: ref ScgiError new(e) e.msg = msg raise e proc parseWord(inp: string, outp: var string, start: int): int = result = start while inp[result] != '\0': inc(result) outp = substr(inp, start, result-1) proc parseHeaders(s: string, L: int): StringTableRef = result = newStringTable() var i = 0 while i < L: var key, val: string i = parseWord(s, key, i)+1 i = parseWord(s, val, i)+1 result[key] = val if s[i] == ',': inc(i) else: raiseScgiError("',' after netstring expected") proc recvChar(s: Socket): char = var c: char if recv(s, addr(c), sizeof(c)) == sizeof(c): result = c type ScgiState* = object of RootObj ## SCGI state object server: Socket bufLen: int client*: Socket ## the client socket to send data to headers*: StringTableRef ## the parsed headers input*: string ## the input buffer # Async ClientMode = enum ClientReadChar, ClientReadHeaders, ClientReadContent AsyncClient = ref object c: AsyncSocket mode: ClientMode dataLen: int headers: StringTableRef ## the parsed headers input: string ## the input buffer AsyncScgiStateObj = object handleRequest: proc (client: AsyncSocket, input: string, headers: StringTableRef) {.closure, gcsafe.} asyncServer: AsyncSocket disp: Dispatcher AsyncScgiState* = ref AsyncScgiStateObj {.deprecated: [EScgi: ScgiError, TScgiState: ScgiState, PAsyncScgiState: AsyncScgiState, scgiError: raiseScgiError].} proc recvBuffer(s: var ScgiState, L: int) = if L > s.bufLen: s.bufLen = L s.input = newString(L) if L > 0 and recv(s.client, cstring(s.input), L) != L: raiseScgiError("could not read all data") setLen(s.input, L) proc open*(s: var ScgiState, port = Port(4000), address = "127.0.0.1", reuseAddr = false) = ## opens a connection. s.bufLen = 4000 s.input = newString(s.bufLen) # will be reused s.server = socket() if s.server == invalidSocket: raiseOSError(osLastError()) new(s.client) # Initialise s.client for `next` if s.server == invalidSocket: raiseScgiError("could not open socket") #s.server.connect(connectionName, port) if reuseAddr: s.server.setSockOpt(OptReuseAddr, true) bindAddr(s.server, port, address) listen(s.server) proc close*(s: var ScgiState) = ## closes the connection. s.server.close() proc next*(s: var ScgiState, timeout: int = -1): bool = ## proceed to the first/next request. Waits ``timeout`` milliseconds for a ## request, if ``timeout`` is `-1` then this function will never time out. ## Returns `true` if a new request has been processed. var rsocks = @[s.server] if select(rsocks, timeout) == 1 and rsocks.len == 1: new(s.client) accept(s.server, s.client) var L = 0 while true: var d = s.client.recvChar() if d == '\0': s.client.close() return false if d notin strutils.Digits: if d != ':': raiseScgiError("':' after length expected") break L = L * 10 + ord(d) - ord('0') recvBuffer(s, L+1) s.headers = parseHeaders(s.input, L) if s.headers.getOrDefault("SCGI") != "1": raiseScgiError("SCGI Version 1 expected") L = parseInt(s.headers.getOrDefault("CONTENT_LENGTH")) recvBuffer(s, L) return true proc writeStatusOkTextContent*(c: Socket, contentType = "text/html") = ## sends the following string to the socket `c`:: ## ## Status: 200 OK\r\LContent-Type: text/html\r\L\r\L ## ## You should send this before sending your HTML page, for example. c.send("Status: 200 OK\r\L" & "Content-Type: $1\r\L\r\L" % contentType) proc run*(handleRequest: proc (client: Socket, input: string, headers: StringTableRef): bool {.nimcall,gcsafe.}, port = Port(4000)) = ## encapsulates the SCGI object and main loop. var s: ScgiState s.open(port) var stop = false while not stop: if next(s): stop = handleRequest(s.client, s.input, s.headers) s.client.close() s.close() # -- AsyncIO start proc recvBufferAsync(client: AsyncClient, L: int): ReadLineResult = result = ReadPartialLine var data = "" if L < 1: raiseScgiError("Cannot read negative or zero length: " & $L) let ret = recvAsync(client.c, data, L) if ret == 0 and data == "": client.c.close() return ReadDisconnected if ret == -1: return ReadNone # No more data available client.input.add(data) if ret == L: return ReadFullLine proc checkCloseSocket(client: AsyncClient) = if not client.c.isClosed: if client.c.isSendDataBuffered: client.c.setHandleWrite do (s: AsyncSocket): if not s.isClosed and not s.isSendDataBuffered: s.close() s.delHandleWrite() else: client.c.close() proc handleClientRead(client: AsyncClient, s: AsyncScgiState) = case client.mode of ClientReadChar: while true: var d = "" let ret = client.c.recvAsync(d, 1) if d == "" and ret == 0: # Disconnected client.c.close() return if ret == -1: return # No more data available if d[0] notin strutils.Digits: if d[0] != ':': raiseScgiError("':' after length expected") break client.dataLen = client.dataLen * 10 + ord(d[0]) - ord('0') client.mode = ClientReadHeaders handleClientRead(client, s) # Allow progression of ClientReadHeaders: let ret = recvBufferAsync(client, (client.dataLen+1)-client.input.len) case ret of ReadFullLine: client.headers = parseHeaders(client.input, client.input.len-1) if client.headers.getOrDefault("SCGI") != "1": raiseScgiError("SCGI Version 1 expected") client.input = "" # For next part let contentLen = parseInt(client.headers.getOrDefault("CONTENT_LENGTH")) if contentLen > 0: client.mode = ClientReadContent else: s.handleRequest(client.c, client.input, client.headers) checkCloseSocket(client) of ReadPartialLine, ReadDisconnected, ReadNone: return of ClientReadContent: let L = parseInt(client.headers.getOrDefault("CONTENT_LENGTH")) - client.input.len if L > 0: let ret = recvBufferAsync(client, L) case ret of ReadFullLine: s.handleRequest(client.c, client.input, client.headers) checkCloseSocket(client) of ReadPartialLine, ReadDisconnected, ReadNone: return else: s.handleRequest(client.c, client.input, client.headers) checkCloseSocket(client) proc handleAccept(sock: AsyncSocket, s: AsyncScgiState) = var client: AsyncSocket new(client) accept(s.asyncServer, client) var asyncClient = AsyncClient(c: client, mode: ClientReadChar, dataLen: 0, headers: newStringTable(), input: "") client.handleRead = proc (sock: AsyncSocket) = handleClientRead(asyncClient, s) s.disp.register(client) proc open*(handleRequest: proc (client: AsyncSocket, input: string, headers: StringTableRef) {. closure, gcsafe.}, port = Port(4000), address = "127.0.0.1", reuseAddr = false): AsyncScgiState = ## Creates an ``AsyncScgiState`` object which serves as a SCGI server. ## ## After the execution of ``handleRequest`` the client socket will be closed ## automatically unless it has already been closed. var cres: AsyncScgiState new(cres) cres.asyncServer = asyncSocket() cres.asyncServer.handleAccept = proc (s: AsyncSocket) = handleAccept(s, cres) if reuseAddr: cres.asyncServer.setSockOpt(OptReuseAddr, true) bindAddr(cres.asyncServer, port, address) listen(cres.asyncServer) cres.handleRequest = handleRequest result = cres proc register*(d: Dispatcher, s: AsyncScgiState): Delegate {.discardable.} = ## Registers ``s`` with dispatcher ``d``. result = d.register(s.asyncServer) s.disp = d proc close*(s: AsyncScgiState) = ## Closes the ``AsyncScgiState``. s.asyncServer.close() when false: var counter = 0 proc handleRequest(client: Socket, input: string, headers: StringTableRef): bool {.procvar.} = inc(counter) client.writeStatusOkTextContent() client.send("Hello for the $#th time." % $counter & "\c\L") return false # do not stop processing run(handleRequest)