diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-30 02:51:13 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-30 03:07:15 +0200 |
commit | 8048a943706ee32f5970e461dda0a01aeb55c27f (patch) | |
tree | a9456532629274491e7ccfdc0b7773da247e58a1 | |
parent | ef8124638b6b056a4721918b47fc00a349ab0da1 (diff) | |
download | chawan-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
-rw-r--r-- | Makefile | 24 | ||||
-rw-r--r-- | doc/cha.1 | 3 | ||||
-rw-r--r-- | doc/config.md | 32 | ||||
-rw-r--r-- | doc/localcgi.md | 111 | ||||
-rw-r--r-- | src/config/config.nim | 5 | ||||
-rw-r--r-- | src/extern/stdio.nim | 17 | ||||
-rw-r--r-- | src/loader/cgi.nim | 165 | ||||
-rw-r--r-- | src/loader/connecterror.nim | 10 | ||||
-rw-r--r-- | src/loader/headers.nim | 33 | ||||
-rw-r--r-- | src/loader/loader.nim | 6 | ||||
-rw-r--r-- | src/local/container.nim | 7 | ||||
-rw-r--r-- | src/server/buffer.nim | 2 | ||||
-rw-r--r-- | src/server/forkserver.nim | 1 |
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] |