about summary refs log tree commit diff stats
path: root/src/loader/cgi.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-09-30 02:51:13 +0200
committerbptato <nincsnevem662@gmail.com>2023-09-30 03:07:15 +0200
commit8048a943706ee32f5970e461dda0a01aeb55c27f (patch)
treea9456532629274491e7ccfdc0b7773da247e58a1 /src/loader/cgi.nim
parentef8124638b6b056a4721918b47fc00a349ab0da1 (diff)
downloadchawan-8048a943706ee32f5970e461dda0a01aeb55c27f.tar.gz
loader: add local-cgi
Add w3m-style local CGI support.

It is not quite as powerful as w3m's local CGI, because it lacks an
equivalent to W3m-control. Not sure if it's worth adding; we certainly
shouldn't allow passing JS in headers, but a custom language for
headers does not sound like a great idea either...

eh, idk. also, TODO add multipart
Diffstat (limited to 'src/loader/cgi.nim')
-rw-r--r--src/loader/cgi.nim165
1 files changed, 165 insertions, 0 deletions
diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim
new file mode 100644
index 00000000..d94a2243
--- /dev/null
+++ b/src/loader/cgi.nim
@@ -0,0 +1,165 @@
+import options
+import os
+import posix
+import streams
+import strutils
+
+import extern/stdio
+import io/posixstream
+import loader/connecterror
+import loader/headers
+import loader/loaderhandle
+import loader/request
+import types/opt
+import types/url
+import utils/twtstr
+
+proc setupEnv(cmd, scriptName, pathInfo, requestURI: string, request: Request,
+    contentLen: int) =
+  let url = request.url
+  putEnv("SERVER_SOFTWARE", "Chawan")
+  putEnv("SERVER_PROTOCOL", "HTTP/1.0")
+  putEnv("SERVER_NAME", "localhost")
+  putEnv("SERVER_PORT", "80")
+  putEnv("REMOTE_HOST", "localhost")
+  putEnv("REMOTE_ADDR", "127.0.0.1")
+  putEnv("GATEWAY_INTERFACE", "CGI/1.1")
+  putEnv("SCRIPT_NAME", scriptName)
+  putEnv("SCRIPT_FILENAME", cmd)
+  putEnv("REQUEST_URI", requestURI)
+  putEnv("REQUEST_METHOD", $request.httpmethod)
+  if pathInfo != "":
+    putEnv("PATH_INFO", pathInfo)
+  if url.query.isSome:
+    putEnv("QUERY_STRING", url.query.get)
+  if request.httpmethod == HTTP_POST:
+    putEnv("CONTENT_TYPE", request.headers.getOrDefault("Content-Type", ""))
+    putEnv("CONTENT_LENGTH", $contentLen)
+  if "Cookie" in request.headers:
+    putEnv("HTTP_COOKIE", request.headers["Cookie"])
+  if request.referer != nil:
+    putEnv("HTTP_REFERER", $request.referer)
+  if request.proxy != nil:
+    let s = $request.proxy
+    if request.proxy.scheme == "https" or request.proxy.scheme == "http":
+      putEnv("http_proxy", s)
+      putEnv("HTTP_PROXY", s)
+      putEnv("HTTPS_proxy", s)
+    putEnv("ALL_PROXY", s)
+
+proc loadCGI*(handle: LoaderHandle, request: Request, cgiDir: seq[string]) =
+  template t(body: untyped) =
+    if not body:
+      return
+  if cgiDir.len == 0:
+    discard handle.sendResult(ERROR_NO_CGI_DIR)
+    return
+  let path = percentDecode(request.url.pathname)
+  if path == "" or request.url.hostname != "":
+    discard handle.sendResult(ERROR_INVALID_CGI_PATH)
+    return
+  var basename: string
+  var pathInfo: string
+  var cmd: string
+  var scriptName: string
+  var requestURI: string
+  if path[0] == '/':
+    for dir in cgiDir:
+      if path.startsWith(dir):
+        basename = path.substr(dir.len).until('/')
+        pathInfo = path.substr(dir.len + basename.len)
+        cmd = dir / basename
+        if not fileExists(cmd):
+          continue
+        scriptName = path.substr(0, dir.len + basename.len)
+        requestURI = cmd / pathInfo & request.url.search
+        break
+    if cmd == "":
+      discard handle.sendResult(ERROR_INVALID_CGI_PATH)
+      return
+  else:
+    basename = path.until('/')
+    pathInfo = path.substr(basename.len)
+    scriptName = "/cgi-bin/" & basename
+    requestURI = "/cgi-bin/" & path & request.url.search
+    for dir in cgiDir:
+      cmd = dir / basename
+      if fileExists(cmd):
+        break
+  if not fileExists(cmd):
+    discard handle.sendResult(ERROR_CGI_FILE_NOT_FOUND)
+  if basename in ["", ".", ".."] or basename.startsWith("~"):
+    discard handle.sendResult(ERROR_INVALID_CGI_PATH)
+    return
+  var pipefd: array[0..1, cint] # child -> parent
+  if pipe(pipefd) == -1:
+    discard handle.sendResult(ERROR_FAIL_SETUP_CGI)
+    return
+  # Pipe the request body as stdin for POST.
+  var pipefd_read: array[0..1, cint] # parent -> child
+  let needsPipe = request.body.isSome or request.multipart.isSome
+  if needsPipe:
+    if pipe(pipefd_read) == -1:
+      discard handle.sendResult(ERROR_FAIL_SETUP_CGI)
+      return
+  var contentLen = 0
+  if request.body.isSome:
+    contentLen = request.body.get.len
+  elif request.multipart.isSome:
+    #TODO multipart
+    # maybe use curl formdata? (the mime api has no serialization functions)
+    discard
+  let pid = fork()
+  if pid == -1:
+    t handle.sendResult(ERROR_FAIL_SETUP_CGI)
+  elif pid == 0:
+    discard close(pipefd[0]) # close read
+    discard dup2(pipefd[1], 1) # dup stdout
+    if needsPipe:
+      discard close(pipefd_read[1]) # close write
+      if pipefd_read[0] != 0:
+        discard dup2(pipefd_read[0], 0) # dup stdin
+        discard close(pipefd_read[0])
+    else:
+      closeStdin()
+    # we leave stderr open, so it can be seen in the browser console
+    setupEnv(cmd, scriptName, pathInfo, requestURI, request, contentLen)
+    discard execl(cstring(cmd), cstring(basename), nil)
+    stdout.write("Content-Type: text/plain\r\n\r\nFailed to execute script.")
+    quit(1)
+  else:
+    discard close(pipefd[1]) # close write
+    if needsPipe:
+      discard close(pipefd_read[0]) # close read
+      let ps = newPosixStream(pipefd_read[1])
+      if request.body.isSome:
+        ps.write(request.body.get)
+      elif request.multipart.isSome:
+        #TODO
+        discard
+      ps.close()
+    discard handle.sendResult(0) # success
+    let ps = newPosixStream(pipefd[0])
+    let headers = newHeaders()
+    var status = 200
+    while not ps.atEnd:
+      let line = ps.readLine()
+      if line == "": #\r\n
+        break
+      let k = line.until(':')
+      if k == line:
+        # invalid?
+        discard
+      else:
+        let v = line.substr(k.len + 1).strip()
+        if k.equalsIgnoreCase("Status"):
+          status = parseInt32(v).get(0)
+        else:
+          headers.add(k, v)
+    t handle.sendStatus(status)
+    t handle.sendHeaders(headers)
+    var buffer: array[4096, uint8]
+    while not ps.atEnd:
+      let n = ps.readData(addr buffer[0], buffer.len)
+      t handle.sendData(addr buffer[0], n)
+    ps.close()
e previous revision' href='/akspecs/ranger/blame/Makefile?h=v1.7.0&id=cb116bf0dc119977f69f3146c786b09315ac9c50'>^
a082b66a ^

b0a216f5 ^
e9e4b4ff ^





ad75190c ^
e9e4b4ff ^
c7720fff ^



8d21b83c ^


e9e4b4ff ^
25a4162d ^
e9e4b4ff ^
0c2c782d ^
636d9393 ^
b0a216f5 ^
b3bc8431 ^
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