about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile24
-rw-r--r--doc/cha.13
-rw-r--r--doc/config.md32
-rw-r--r--doc/localcgi.md111
-rw-r--r--src/config/config.nim5
-rw-r--r--src/extern/stdio.nim17
-rw-r--r--src/loader/cgi.nim165
-rw-r--r--src/loader/connecterror.nim10
-rw-r--r--src/loader/headers.nim33
-rw-r--r--src/loader/loader.nim6
-rw-r--r--src/local/container.nim7
-rw-r--r--src/server/buffer.nim2
-rw-r--r--src/server/forkserver.nim1
13 files changed, 380 insertions, 36 deletions
diff --git a/Makefile b/Makefile
index 8c244367..916ad5f9 100644
--- a/Makefile
+++ b/Makefile
@@ -50,30 +50,34 @@ clean:
 	rm -rf $(OBJDIR)
 	rm -f lib/libquickjs.a
 
-$(OBJDIR)/cha-%.md: doc/%.md | $(OBJDIR)/
+$(OBJDIR)/man/cha-%.md: doc/%.md | $(OBJDIR)/man/
 	sed -e '/<!-- MANOFF -->/,/<!-- MANON -->/d' \
 		-e '/^<!-- MANON$$/d' \
 		-e '/^MANOFF -->$$/d' $< | \
 		./table_rewrite.sh > $@
 
-$(OBJDIR)/cha-%.5: $(OBJDIR)/cha-%.md
+$(OBJDIR)/man/cha-%.5: $(OBJDIR)/man/cha-%.md | $(OBJDIR)/man/
 	pandoc --standalone --to man $< -o $@
 
-$(OBJDIR)/cha.1: doc/cha.1
-	cp doc/cha.1 "$(OBJDIR)/cha.1"
+$(OBJDIR)/man/cha.1: $(OBJDIR)/man/ doc/cha.1
+	cp doc/cha.1 "$(OBJDIR)/man/cha.1"
 
 .PHONY: manpage
-manpage: $(OBJDIR)/cha-config.5 $(OBJDIR)/cha-mailcap.5 \
-		$(OBJDIR)/cha-mime.types.5 $(OBJDIR)/cha.1
+manpage:  $(OBJDIR)/man/cha-config.5 $(OBJDIR)/man/cha-mailcap.5 \
+	$(OBJDIR)/man/cha-mime.types.5 $(OBJDIR)/man/cha-localcgi.5 \
+	$(OBJDIR)/man/cha.1
 
 .PHONY: install
 install:
 	mkdir -p "$(DESTDIR)$(prefix)/bin"
 	install -m755 cha "$(DESTDIR)$(prefix)/bin"
-	test -f "$(OBJDIR)/cha-config.5" && install -m755 "$(OBJDIR)/cha-config.5" "$(DESTDIR)$(manprefix5)" || true
-	test -f "$(OBJDIR)/cha-mailcap.5" && install -m755 "$(OBJDIR)/cha-mailcap.5" "$(DESTDIR)$(manprefix5)" || true
-	test -f "$(OBJDIR)/cha-mime.types.5" && install -m755 "$(OBJDIR)/cha-mime.types.5" "$(DESTDIR)$(manprefix5)" || true
-	test -f "$(OBJDIR)/cha.1" && install -m755 "$(OBJDIR)/cha.1" "$(DESTDIR)$(manprefix1)" || true
+	if test -d "$(OBJDIR)/man"; then \
+	install -m755 "$(OBJDIR)/man/cha-config.5" "$(DESTDIR)$(manprefix5)"; \
+	install -m755 "$(OBJDIR)/man/cha-mailcap.5" "$(DESTDIR)$(manprefix5)"; \
+	install -m755 "$(OBJDIR)/man/cha-mime.types.5" "$(DESTDIR)$(manprefix5)"; \
+	install -m755 "$(OBJDIR)/man/cha-localcgi.5" "$(DESTDIR)$(manprefix5)"; \
+	install -m755 "$(OBJDIR)/cha.1" "$(DESTDIR)$(manprefix1)"; \
+	fi
 
 .PHONY: uninstall
 uninstall:
diff --git a/doc/cha.1 b/doc/cha.1
index e2b7d35d..aad18ed4 100644
--- a/doc/cha.1
+++ b/doc/cha.1
@@ -123,4 +123,5 @@ configuration option is not set.
 Configuration options are described in \fBcha-config\fR(5).
 
 .SH SEE ALSO
-\fBcha-mailcap\fR(5), \fBcha-mime.types\fR(5)
+\fBcha-mailcap\fR(5), \fBcha-mime.types\fR(5), \fBcha-config\fR(5),
+\fBcha-localcgi\fR(5)
diff --git a/doc/config.md b/doc/config.md
index ad1807c5..6c933cdc 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -171,13 +171,41 @@ the line number.</td>
 <tr>
 <td>mailcap</td>
 <td>array of paths</td>
-<td>Search path for [mailcap](mailcap.md) files.</td>
+<td>Search path for
+<!-- MANOFF -->
+[mailcap](mailcap.md) files.
+<!-- MANON -->
+<!-- MANON
+mailcap (**cha-mailcap**(5))
+MANOFF -->
+files.</td>
 </tr>
 
 <tr>
 <td>mime-types</td>
 <td>array of paths</td>
-<td>Search path for [mime.types](mime.types.md) files.</td>
+<td>Search path for
+<!-- MANOFF -->
+[mime.types](mime.types.md)
+<!-- MANON -->
+<!-- MANON
+mime.types (**cha-mime.types**(5))
+MANOFF -->
+files.</td>
+</tr>
+
+<tr>
+<td>cgi-dir</td>
+<td>array of paths</td>
+<td>Search path for
+<!-- MANOFF -->
+[mime.types](mime.types.md)
+[local CGI](localcgi.md)
+<!-- MANON -->
+<!-- MANON
+local CGI (**cha-localcgi**(5))
+MANOFF -->
+scripts.</td>
 </tr>
 
 </table>
diff --git a/doc/localcgi.md b/doc/localcgi.md
new file mode 100644
index 00000000..ba7d5232
--- /dev/null
+++ b/doc/localcgi.md
@@ -0,0 +1,111 @@
+<!-- MANON
+% cha-localcgi(5) | Local CGI support in Chawan
+MANOFF -->
+
+# Local CGI support in Chawan
+
+Chawan supports the invocation of CGI scripts locally. This feature can be
+used in the following way:
+
+* All local CGI scripts must be placed in a directory specified in
+  `external.cgi-dir`. Multiple directories can be specified in an array too,
+  and directories specified first have higher precedence.
+* Then, a CGI script in one of these directories can be executed by visiting
+  the URL `cgi-bin:script-name`. $PATH_INFO and $QUERY_STRING are set as
+  normal, i.e. `cgi-bin:script-name/abcd?defgh=ijkl` will set $PATH_INFO to
+  `/abcd`, and $QUERY_STRING to `defgh=ijkl`.
+
+Further notes on processing CGI paths:
+
+* The URL must be opaque, so you must not add a double slash after the scheme.
+  e.g. `cgi-bin://script-name` will NOT work, only `cgi-bin:script-name`.
+* Absolute paths are accepted as e.g. `cgi-bin:/path/to/cgi/dir/script-name`.
+  Note however, that this only works if `/path/to/cgi/dir` has already been
+  specified as a CGI directory in `external.cgi-dir`.
+
+Note that this is different from w3m's cgi-bin functionality, in that we
+use a custom scheme for local CGI instead of interpreting all requests to
+a designated path as a CGI request. Also, for now Chawan has no equivalent
+to the W3m-control headers (but this may change in the future).
+
+## Environment variables
+
+Chawan sets the following environment variables:
+
+* `SERVER_SOFTWARE="Chawan"`
+* `SERVER_PROTOCOL="HTTP/1.0"`
+* `SERVER_NAME="localhost"`
+* `SERVER_PORT="80"`
+* `REMOTE_HOST="localhost"`
+* `REMOTE_ADDR="127.0.0.1"`
+* `GATEWAY_INTERFACE="CGI/1.1"`
+* `SCRIPT_NAME="/cgi-bin/script-name"` if called with a relative path, and
+  `"/path/to/script/script-name"` if called with an absolute path.
+* `SCRIPT_FILENAME="/path/to/script/script-name"`
+* `QUERY_STRING=` the query string (i.e. `URL.search`). Note that this
+  variable is percent-encoded.
+* `PATH_INFO=` everything after the script's path name,
+  e.g. for `cgi-bin:script-name/abcd/efgh` `"/abcd/efgh"`. Note that this
+  variable is NOT percent-encoded.
+* `REQUEST_URI="$SCRIPT_NAME/$PATH_INFO?$QUERY_STRING`
+* `REQUEST_METHOD=` HTTP method used for making the request, e.g. GET or POST
+* `CONTENT_TYPE=` for POST requests, the Content-Type header. Not set for
+  other request types (e.g. GET).
+* `CONTENT_LENGTH=` the content length, if $CONTENT_TYPE has been set.
+* `HTTP_PROXY=` and (lower case) `http_proxy=`: the proxy URL if a proxy
+  has been set and its scheme is either `http` or `https`.
+* `ALL_PROXY=` if a proxy has been set, the proxy URL.
+* `HTTP_COOKIE=` if set, the Cookie header.
+* `HTTP_REFERER=` if set, the Referer header.
+
+## Request body
+
+If the request body is not empty, it is streamed into the program through
+the standard input.
+
+NOTE: multipart requests are not implemented yet. This will be fixed in
+the future.
+
+## Troubleshooting
+
+Note that standard error is redirected to the browser console (by default,
+M-cM-c). This makes it easy to debug a misbehaving CGI script, but may also
+slow down the browser in case of excessive logging. If this is not the
+desired behavior, we recommend wrapping your script into a shell script that
+redirects stderr to /dev/null.
+
+### My script is returning a "no local-CGI directory configured" error message.
+
+Configure a local-CGI directory using `external.cgi-dir`.
+
+e.g. you could add this to your config.toml:
+
+```toml
+[external]
+cgi-dir = "/usr/local/libexec/chawan/cgi-bin"
+```
+
+and then put your script in `/usr/local/libexec/chawan/cgi-bin`.
+
+### My script is returning an "invalid CGI path" error message.
+
+Make sure that you did not include leading slashes. Reminder:
+`cgi-bin://script-name` does not work, use `cgi-bin:script-name`.
+
+### My script is returning a "CGI file not found" error message.
+
+Double check that your CGI script is in the correct location. Also, make
+sure that you are not accidentally calling the script with an absolute path via
+`cgi-bin:/script-name` (instead of the correct `cgi-bin:script-name`).
+
+Also, make sure `external.cgi-dir` is set to the directory your script is in.
+
+### My script returns a page saying "Failed to execute script".
+
+This means the `execl` call to the script failed. Make sure that your CGI
+script's executable bit is set, i.e. run `chmod +x /path/to/cgi/script`.
+
+### My script is returning a "failed to set up CGI script" error message.
+
+This means that either `pipe` or `fork` failed. Something strange is going on
+with your system; we recommend exorcism. (Maybe you are running out of memory?)
diff --git a/src/config/config.nim b/src/config/config.nim
index 641f63dd..da897133 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -87,6 +87,7 @@ type
     editor* {.jsgetset.}: string
     mailcap* {.jsgetset.}: seq[string]
     mime_types* {.jsgetset.}: seq[string]
+    cgi_dir* {.jsgetset.}: seq[string]
 
   InputConfig = object
     vi_numeric_prefix* {.jsgetset.}: bool
@@ -141,6 +142,7 @@ type
     images*: bool
     proxy*: URL
     mimeTypes*: MimeTypes
+    cgiDir*: seq[string]
 
   ForkServerConfig* = object
     tmpdir*: string
@@ -233,7 +235,8 @@ proc getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar,
     charsets: charsets,
     images: images,
     proxy: proxy,
-    mimeTypes: mimeTypes
+    mimeTypes: mimeTypes,
+    cgiDir: config.external.cgi_dir
   )
 
 proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] =
diff --git a/src/extern/stdio.nim b/src/extern/stdio.nim
new file mode 100644
index 00000000..63c0e88d
--- /dev/null
+++ b/src/extern/stdio.nim
@@ -0,0 +1,17 @@
+import posix
+
+proc closeHandle(fd, flags: cint) =
+  let devnull = open("/dev/null", flags)
+  doAssert devnull != -1
+  if devnull != fd:
+    discard dup2(devnull, fd)
+    discard close(devnull)
+
+proc closeStdin*() =
+  closeHandle(0, O_RDONLY)
+
+proc closeStdout*() =
+  closeHandle(1, O_WRONLY)
+
+proc closeStderr*() =
+  closeHandle(2, O_WRONLY)
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()
diff --git a/src/loader/connecterror.nim b/src/loader/connecterror.nim
index 5fce5287..f10285d2 100644
--- a/src/loader/connecterror.nim
+++ b/src/loader/connecterror.nim
@@ -1,13 +1,17 @@
 import bindings/curl
 
 type ConnectErrorCode* = enum
+  ERROR_CGI_FILE_NOT_FOUND = (-12, "CGI file not found")
+  ERROR_INVALID_CGI_PATH = (-11, "invalid CGI path")
+  ERROR_FAIL_SETUP_CGI = (-10, "failed to set up CGI script")
+  ERROR_NO_CGI_DIR = (-9, "no local-CGI directory configured")
   ERROR_INVALID_METHOD = (-8, "invalid method")
   ERROR_INVALID_DATA_URL = (-7, "invalid data URL")
   ERROR_ABOUT_PAGE_NOT_FOUND = (-6, "about page not found")
   ERROR_FILE_NOT_FOUND = (-5, "file not found")
-  ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found"),
-  ERROR_LOADER_KILLED = (-3, "loader killed during transfer"),
-  ERROR_DISALLOWED_URL = (-2, "url not allowed by filter"),
+  ERROR_SOURCE_NOT_FOUND = (-4, "clone source could not be found")
+  ERROR_LOADER_KILLED = (-3, "loader killed during transfer")
+  ERROR_DISALLOWED_URL = (-2, "url not allowed by filter")
   ERROR_UNKNOWN_SCHEME = (-1, "unknown scheme")
 
 converter toInt*(code: ConnectErrorCode): int =
diff --git a/src/loader/headers.nim b/src/loader/headers.nim
index 06e3f2ba..5e61fded 100644
--- a/src/loader/headers.nim
+++ b/src/loader/headers.nim
@@ -89,22 +89,31 @@ func clone*(headers: Headers): Headers =
     table: headers.table
   )
 
-proc add*(headers: var Headers, k, v: string) =
+proc add*(headers: Headers, k, v: string) =
   let k = k.toHeaderCase()
-  if k notin headers.table:
+  headers.table.withValue(k, p):
+    p[].add(v)
+  do:
     headers.table[k] = @[v]
-  else:
-    headers.table[k].add(v)
 
-proc `[]=`*(headers: var Headers, k, v: string) =
-  headers.table[k.toHeaderCase()] = @[v]
+proc `[]=`*(headers: Headers, k: static string, v: string) =
+  const k = k.toHeaderCase()
+  headers.table[k] = @[v]
 
-func getOrDefault*(headers: Headers, k: string, default = ""): string =
-  let k = k.toHeaderCase()
-  if k in headers.table:
-    headers.table[k][0]
-  else:
-    default
+func `[]`*(headers: Headers, k: static string): string =
+  const k = k.toHeaderCase()
+  return headers.table[k][0]
+
+func contains*(headers: Headers, k: static string): bool =
+  const k = k.toHeaderCase()
+  return k in headers.table
+
+func getOrDefault*(headers: Headers, k: static string, default = ""): string =
+  const k = k.toHeaderCase()
+  headers.table.withValue(k, p):
+    return p[][0]
+  do:
+    return default
 
 proc addHeadersModule*(ctx: JSContext) =
   ctx.registerType(Headers)
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 6cd17f44..809219fe 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -29,6 +29,7 @@ import io/urlfilter
 import js/error
 import js/javascript
 import loader/about
+import loader/cgi
 import loader/connecterror
 import loader/curlhandle
 import loader/data
@@ -98,6 +99,7 @@ type
     # When set to false, requests with a proxy URL are overridden by the
     # loader proxy.
     acceptProxy*: bool
+    cgiDir*: seq[string]
 
   FetchPromise* = Promise[JSResult[Response]]
 
@@ -130,6 +132,9 @@ proc loadResource(ctx: LoaderContext, request: Request, handle: LoaderHandle) =
     let handleData = handle.loadGopher(ctx.curlm, request)
     if handleData != nil:
       ctx.handleList.add(handleData)
+  of "cgi-bin":
+    handle.loadCGI(request, ctx.config.cgiDir)
+    handle.close()
   else:
     discard handle.sendResult(ERROR_UNKNOWN_SCHEME)
     handle.close()
@@ -201,7 +206,6 @@ proc acceptConnection(ctx: LoaderContext) =
       for fd in fds:
         ctx.handleMap.withValue(fd, handlep):
           handlep[].resume()
-
   except IOError:
     # End-of-file, broken pipe, or something else. For now we just
     # ignore it and pray nothing breaks.
diff --git a/src/local/container.nim b/src/local/container.nim
index ad98419d..ce049c08 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -8,6 +8,7 @@ when defined(posix):
 
 import config/config
 import display/window
+import extern/stdio
 import io/promise
 import io/serialize
 import js/javascript
@@ -990,11 +991,7 @@ proc setStream*(container: Container, stream: Stream) =
         if container.source.fd == 0:
           # We are closing stdin.
           # Leaving the stdin fileno open to grab is a bad idea.
-          let devnull = open("/dev/null", O_RDONLY)
-          doAssert devnull != -1
-          if devnull != 0:
-            discard dup2(devnull, 0)
-            discard close(devnull)
+          closeStdin()
         else:
           discard close(container.source.fd)
       )
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index f7ead4e5..91eae0e8 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -1129,7 +1129,7 @@ func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
 
   case scheme
   of "http", "https",
-      "gopher", "gophers": # Note: gopher/s is non-standard.
+      "gopher", "gophers", "cgi-bin": # Note: gopher/s, cgi-bin is non-standard.
     if formmethod == FORM_METHOD_GET:
       mutateActionUrl
     else:
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index 7fa7a63f..8ece45e6 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -119,6 +119,7 @@ proc forkBuffer(ctx: var ForkServerContext): Pid =
       referrerpolicy: config.referrerpolicy,
       #TODO these should be in a separate config I think
       proxy: config.proxy,
+      cgiDir: config.cgiDir
     )
   )
   var pipefd: array[2, cint]