summary refs log tree commit diff stats
path: root/examples/httpserver2.nim
blob: 050684d3e4752253f1631a8c833ed27d2a9e9498 (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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import strutils, os, osproc, strtabs, streams, sockets

const
  wwwNL* = "\r\L"
  ServerSig = "Server: httpserver.nim/1.0.0" & wwwNL

type
  TRequestMethod = enum reqGet, reqPost
  TServer* = object       ## contains the current server state
    s: Socket
    job: seq[TJob]
  TJob* = object
    client: Socket
    process: Process

# --------------- output messages --------------------------------------------

proc sendTextContentType(client: Socket) =
  send(client, "Content-type: text/html" & wwwNL)
  send(client, wwwNL)

proc badRequest(client: Socket) =
  # Inform the client that a request it has made has a problem.
  send(client, "HTTP/1.0 400 BAD REQUEST" & wwwNL)
  sendTextContentType(client)
  send(client, "<p>Your browser sent a bad request, " &
               "such as a POST without a Content-Length.</p>" & wwwNL)


proc cannotExec(client: Socket) =
  send(client, "HTTP/1.0 500 Internal Server Error" & wwwNL)
  sendTextContentType(client)
  send(client, "<P>Error prohibited CGI execution.</p>" & wwwNL)


proc headers(client: Socket, filename: string) =
  # XXX could use filename to determine file type
  send(client, "HTTP/1.0 200 OK" & wwwNL)
  send(client, ServerSig)
  sendTextContentType(client)

proc notFound(client: Socket, path: string) =
  send(client, "HTTP/1.0 404 NOT FOUND" & wwwNL)
  send(client, ServerSig)
  sendTextContentType(client)
  send(client, "<html><title>Not Found</title>" & wwwNL)
  send(client, "<body><p>The server could not fulfill" & wwwNL)
  send(client, "your request because the resource <b>" & path & "</b>" & wwwNL)
  send(client, "is unavailable or nonexistent.</p>" & wwwNL)
  send(client, "</body></html>" & wwwNL)


proc unimplemented(client: Socket) =
  send(client, "HTTP/1.0 501 Method Not Implemented" & wwwNL)
  send(client, ServerSig)
  sendTextContentType(client)
  send(client, "<html><head><title>Method Not Implemented" &
               "</title></head>" &
               "<body><p>HTTP request method not supported.</p>" &
               "</body></HTML>" & wwwNL)


# ----------------- file serving ---------------------------------------------

proc discardHeaders(client: Socket) = skip(client)

proc serveFile(client: Socket, filename: string) =
  discardHeaders(client)

  var f: File
  if open(f, filename):
    headers(client, filename)
    const bufSize = 8000 # != 8K might be good for memory manager
    var buf = alloc(bufsize)
    while true:
      var bytesread = readBuffer(f, buf, bufsize)
      if bytesread > 0:
        var byteswritten = send(client, buf, bytesread)
        if bytesread != bytesWritten:
          let err = osLastError()
          dealloc(buf)
          close(f)
          raiseOSError(err)
      if bytesread != bufSize: break
    dealloc(buf)
    close(f)
    client.close()
  else:
    notFound(client, filename)

# ------------------ CGI execution -------------------------------------------

proc executeCgi(server: var TServer, client: Socket, path, query: string,
                meth: TRequestMethod) =
  var env = newStringTable(modeCaseInsensitive)
  var contentLength = -1
  case meth
  of reqGet:
    discardHeaders(client)

    env["REQUEST_METHOD"] = "GET"
    env["QUERY_STRING"] = query
  of reqPost:
    var buf = ""
    var dataAvail = true
    while dataAvail:
      dataAvail = recvLine(client, buf)
      if buf.len == 0:
        break
      var L = toLower(buf)
      if L.startsWith("content-length:"):
        var i = len("content-length:")
        while L[i] in Whitespace: inc(i)
        contentLength = parseInt(substr(L, i))

    if contentLength < 0:
      badRequest(client)
      return

    env["REQUEST_METHOD"] = "POST"
    env["CONTENT_LENGTH"] = $contentLength

  send(client, "HTTP/1.0 200 OK" & wwwNL)

  var process = startProcess(command=path, env=env)

  var job: TJob
  job.process = process
  job.client = client
  server.job.add(job)

  if meth == reqPost:
    # get from client and post to CGI program:
    var buf = alloc(contentLength)
    if recv(client, buf, contentLength) != contentLength:
      let err = osLastError()
      dealloc(buf)
      raiseOSError(err)
    var inp = process.inputStream
    inp.writeData(buf, contentLength)
    dealloc(buf)

proc animate(server: var TServer) =
  # checks list of jobs, removes finished ones (pretty sloppy by seq copying)
  var active_jobs: seq[TJob] = @[]
  for i in 0..server.job.len-1:
    var job = server.job[i]
    if running(job.process):
      active_jobs.add(job)
    else:
      # read process output stream and send it to client
      var outp = job.process.outputStream
      while true:
        var line = outp.readstr(1024)
        if line.len == 0:
          break
        else:
          try:
            send(job.client, line)
          except:
            echo("send failed, client diconnected")
      close(job.client)

  server.job = active_jobs

# --------------- Server Setup -----------------------------------------------

proc acceptRequest(server: var TServer, client: Socket) =
  var cgi = false
  var query = ""
  var buf = ""
  discard recvLine(client, buf)
  var path = ""
  var data = buf.split()
  var meth = reqGet
  var q = find(data[1], '?')

  # extract path
  if q >= 0:
    # strip "?..." from path, this may be found in both POST and GET
    path = data[1].substr(0, q-1)
  else:
    path = data[1]
  # path starts with "/", by adding "." in front of it we serve files from cwd
  path = "." & path

  echo("accept: " & path)

  if cmpIgnoreCase(data[0], "GET") == 0:
    if q >= 0:
      cgi = true
      query = data[1].substr(q+1)
  elif cmpIgnoreCase(data[0], "POST") == 0:
    cgi = true
    meth = reqPost
  else:
    unimplemented(client)

  if path[path.len-1] == '/' or existsDir(path):
    path = path / "index.html"

  if not existsFile(path):
    discardHeaders(client)
    notFound(client, path)
    client.close()
  else:
    when defined(Windows):
      var ext = splitFile(path).ext.toLower
      if ext == ".exe" or ext == ".cgi":
        # XXX: extract interpreter information here?
        cgi = true
    else:
      if {fpUserExec, fpGroupExec, fpOthersExec} * path.getFilePermissions != {}:
        cgi = true
    if not cgi:
      serveFile(client, path)
    else:
      executeCgi(server, client, path, query, meth)

when isMainModule:
  var port = 80

  var server: TServer
  server.job = @[]
  server.s = socket(AF_INET)
  if server.s == invalidSocket: raiseOSError(osLastError())
  server.s.bindAddr(port=Port(port))
  listen(server.s)
  echo("server up on port " & $port)

  while true:
    # check for new new connection & handle it
    var list: seq[Socket] = @[server.s]
    if select(list, 10) > 0:
      var client: Socket
      new(client)
      accept(server.s, client)
      try:
        acceptRequest(server, client)
      except:
        echo("failed to accept client request")

    # pooling events
    animate(server)
    # some slack for CPU
    sleep(10)
  server.s.close()