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 /src/loader/cgi.nim | |
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
Diffstat (limited to 'src/loader/cgi.nim')
-rw-r--r-- | src/loader/cgi.nim | 165 |
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() |