summary refs log tree commit diff stats
path: root/lib/pure/scgi.nim
blob: 3fdcba39ee332aa4959c408a69562a6d50805991 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#
#
#            Nimrod's Runtime Library
#        (c) Copyright 2011 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module implements helper procs for SCGI applictions. Example:
## 
## .. code-block:: Nimrod
##
##    import strtabs, sockets, scgi
##
##    var counter = 0
##    proc handleRequest(client: TSocket, input: string, 
##                       headers: PStringTable): bool {.procvar.} =
##      inc(counter)
##      client.writeStatusOkTextContent()
##      client.send("Hello for the $#th time." % $counter & "\c\L")
##      return false # do not stop processing
##
##    run(handleRequest)
##

import sockets, strutils, os, strtabs

type
  EScgi* = object of EIO ## the exception that is raised, if a SCGI error occurs

proc scgiError*(msg: string) {.noreturn.} = 
  ## raises an EScgi exception with message `msg`.
  var e: ref EScgi
  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): PStringTable = 
  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: scgiError("',' after netstring expected")
  
proc recvChar(s: TSocket): char = 
  var c: char
  if recv(s, addr(c), sizeof(c)) == sizeof(c): 
    result = c
  
type
  TScgiState* {.final.} = object ## SCGI state object
    server: TSocket
    bufLen: int
    client*: TSocket ## the client socket to send data to
    headers*: PStringTable ## the parsed headers
    input*: string  ## the input buffer
    
proc recvBuffer(s: var TScgiState, 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: 
    scgiError("could not read all data")
  setLen(s.input, L)
  
proc open*(s: var TScgiState, port = TPort(4000), address = "127.0.0.1") = 
  ## opens a connection.
  s.bufLen = 4000
  s.input = newString(s.buflen) # will be reused
  
  s.server = socket()
  if s.server == InvalidSocket: scgiError("could not open socket")
  #s.server.connect(connectionName, port)
  bindAddr(s.server, port, address)
  listen(s.server)
  
proc close*(s: var TScgiState) = 
  ## closes the connection.
  s.server.close()

proc next*(s: var TScgistate, timeout: int = -1): bool = 
  ## proceed to the first/next request. Waits ``timeout`` miliseconds 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 == 0:
    s.client = accept(s.server)
    var L = 0
    while true:
      var d = s.client.recvChar()
      if d notin strutils.digits: 
        if d != ':': scgiError("':' after length expected")
        break
      L = L * 10 + ord(d) - ord('0')  
    recvBuffer(s, L+1)
    s.headers = parseHeaders(s.input, L)
    if s.headers["SCGI"] != "1": scgiError("SCGI Version 1 expected")
    L = parseInt(s.headers["CONTENT_LENGTH"])
    recvBuffer(s, L)
    return True
  
proc writeStatusOkTextContent*(c: TSocket, 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: TSocket, input: string, 
                               headers: PStringTable): bool,
          port = TPort(4000)) = 
  ## encapsulates the SCGI object and main loop.
  var s: TScgiState
  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()

when false:
  var counter = 0
  proc handleRequest(client: TSocket, input: string, 
                     headers: PStringTable): bool {.procvar.} =
    inc(counter)
    client.writeStatusOkTextContent()
    client.send("Hello for the $#th time." % $counter & "\c\L")
    return false # do not stop processing

  run(handleRequest)