diff options
author | bptato <nincsnevem662@gmail.com> | 2024-09-15 18:41:20 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-09-15 19:13:08 +0200 |
commit | b7ac2954a90e44fd727c770c47e8f9706b40004f (patch) | |
tree | c058565a825d89d1364067fd1c54718e8830db0d | |
parent | 9f453ca3997528252eb28268e38480f58fbce4f6 (diff) | |
download | chawan-b7ac2954a90e44fd727c770c47e8f9706b40004f.tar.gz |
loader: refactor/move around some procs
Module boundaries didn't make much sense here either. Specifically: * loader/cgi was originally just one of the many "real" protocols supported by loader, so it was in a separate module (like the other ones). Now it's mostly an "internal" protocol, and it was getting cumbersome to pass all required loader state to loadCGI. * The loader interface has grown quite large, but there is no need for (or advantage in) putting it in the same module as the implementation. Now CGI is handled by loader, and the interface is in the new module "loaderiface".
-rw-r--r-- | src/html/dom.nim | 7 | ||||
-rw-r--r-- | src/html/env.nim | 2 | ||||
-rw-r--r-- | src/html/xmlhttprequest.nim | 2 | ||||
-rw-r--r-- | src/loader/cgi.nim | 303 | ||||
-rw-r--r-- | src/loader/loader.nim | 746 | ||||
-rw-r--r-- | src/loader/loaderiface.nim | 429 | ||||
-rw-r--r-- | src/local/client.nim | 3 | ||||
-rw-r--r-- | src/local/container.nim | 5 | ||||
-rw-r--r-- | src/local/pager.nim | 5 | ||||
-rw-r--r-- | src/server/buffer.nim | 3 | ||||
-rw-r--r-- | src/server/forkserver.nim | 1 |
11 files changed, 763 insertions, 743 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim index 9275538c..2a4dc921 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -19,8 +19,6 @@ import html/catom import html/enums import html/event import html/script -import types/bitmap -import types/path import io/bufwriter import io/dynstream import io/promise @@ -28,8 +26,9 @@ import js/console import js/domexception import js/timeout import loader/headers -import loader/loader +import loader/loaderiface import loader/request +import loader/response import monoucha/fromjs import monoucha/javascript import monoucha/jserror @@ -38,12 +37,14 @@ import monoucha/jspropenumlist import monoucha/jsutils import monoucha/quickjs import monoucha/tojs +import types/bitmap import types/blob import types/canvastypes import types/color import types/line import types/matrix import types/opt +import types/path import types/referrer import types/url import types/vector diff --git a/src/html/env.nim b/src/html/env.nim index ea55dcbc..ab9a9be5 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -17,7 +17,7 @@ import js/encoding import js/intl import js/timeout import loader/headers -import loader/loader +import loader/loaderiface import loader/request import loader/response import monoucha/javascript diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim index cffd2357..67123904 100644 --- a/src/html/xmlhttprequest.nim +++ b/src/html/xmlhttprequest.nim @@ -13,7 +13,7 @@ import io/dynstream import io/promise import js/domexception import loader/headers -import loader/loader +import loader/loaderiface import loader/request import loader/response import monoucha/fromjs diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim deleted file mode 100644 index 7e294eae..00000000 --- a/src/loader/cgi.nim +++ /dev/null @@ -1,303 +0,0 @@ -import std/options -import std/os -import std/posix -import std/strutils - -import io/dynstream -import io/stdio -import loader/connecterror -import loader/headers -import loader/loaderhandle -import loader/request -import types/formdata -import types/url -import utils/twtstr - -proc putMappedURL(url: URL) = - putEnv("MAPPED_URI_SCHEME", url.scheme) - putEnv("MAPPED_URI_USERNAME", url.username) - putEnv("MAPPED_URI_PASSWORD", url.password) - putEnv("MAPPED_URI_HOST", url.hostname) - putEnv("MAPPED_URI_PORT", url.port) - putEnv("MAPPED_URI_PATH", url.path.serialize()) - putEnv("MAPPED_URI_QUERY", url.query.get("")) - -proc setupEnv(cmd, scriptName, pathInfo, requestURI, myDir: string; - request: Request; contentLen: int; prevURL: URL; - insecureSSLNoVerify: bool) = - let url = request.url - putEnv("SCRIPT_NAME", scriptName) - putEnv("SCRIPT_FILENAME", cmd) - putEnv("REQUEST_URI", requestURI) - putEnv("REQUEST_METHOD", $request.httpMethod) - var headers = "" - for k, v in request.headers: - headers &= k & ": " & v & "\r\n" - putEnv("REQUEST_HEADERS", headers) - if prevURL != nil: - putMappedURL(prevURL) - if pathInfo != "": - putEnv("PATH_INFO", pathInfo) - if url.query.isSome: - putEnv("QUERY_STRING", url.query.get) - if request.httpMethod == hmPost: - if request.body.t == rbtMultipart: - putEnv("CONTENT_TYPE", request.body.multipart.getContentType()) - else: - 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.referrer != nil: - putEnv("HTTP_REFERER", $request.referrer) - if request.proxy != nil: - putEnv("ALL_PROXY", $request.proxy) - if insecureSSLNoVerify: - putEnv("CHA_INSECURE_SSL_NO_VERIFY", "1") - setCurrentDir(myDir) - -type ControlResult = enum - crDone, crContinue, crError - -proc handleFirstLine(handle: InputHandle; line: string; headers: Headers; - status: var uint16): ControlResult = - let k = line.until(':') - if k.len == line.len: - # invalid - handle.sendResult(ERROR_CGI_MALFORMED_HEADER) - return crError - let v = line.substr(k.len + 1).strip() - if k.equalsIgnoreCase("Status"): - handle.sendResult(0) # success - status = parseUInt16(v, allowSign = false).get(0) - return crContinue - if k.equalsIgnoreCase("Cha-Control"): - if v.startsWithIgnoreCase("Connected"): - handle.sendResult(0) # success - return crContinue - elif v.startsWithIgnoreCase("ConnectionError"): - let errs = v.split(' ') - if errs.len <= 1: - handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL) - else: - let fb = int32(ERROR_CGI_INVALID_CHA_CONTROL) - let code = int(parseInt32(errs[1]).get(fb)) - var message = "" - if errs.len > 2: - message &= errs[2] - for i in 3 ..< errs.len: - message &= ' ' - message &= errs[i] - handle.sendResult(code, message) - return crError - elif v.startsWithIgnoreCase("ControlDone"): - return crDone - handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL) - return crError - handle.sendResult(0) # success - headers.add(k, v) - return crDone - -proc handleControlLine(handle: InputHandle; line: string; headers: Headers; - status: var uint16): ControlResult = - let k = line.until(':') - if k.len == line.len: - # invalid - return crError - let v = line.substr(k.len + 1).strip() - if k.equalsIgnoreCase("Status"): - status = parseUInt16(v, allowSign = false).get(0) - return crContinue - if k.equalsIgnoreCase("Cha-Control"): - if v.startsWithIgnoreCase("ControlDone"): - return crDone - return crError - headers.add(k, v) - return crDone - -# returns false if transfer was interrupted -proc handleLine(handle: InputHandle; line: string; headers: Headers) = - let k = line.until(':') - if k.len == line.len: - # invalid - return - let v = line.substr(k.len + 1).strip() - headers.add(k, v) - -proc loadCGI*(handle: InputHandle; request: Request; cgiDir: seq[string]; - prevURL: URL; insecureSSLNoVerify: bool; handleMap: openArray[LoaderHandle]; - istream: PosixStream; ostream: var PosixStream) = - if cgiDir.len == 0: - handle.sendResult(ERROR_NO_CGI_DIR) - return - var path = percentDecode(request.url.pathname) - if path.startsWith("/cgi-bin/"): - path.delete(0 .. "/cgi-bin/".high) - elif path.startsWith("/$LIB/"): - path.delete(0 .. "/$LIB/".high) - if path == "" or request.url.hostname != "": - handle.sendResult(ERROR_INVALID_CGI_PATH) - return - var basename: string - var pathInfo: string - var cmd: string - var scriptName: string - var requestURI: string - var myDir: 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 - myDir = dir - scriptName = path.substr(0, dir.len + basename.len) - requestURI = cmd / pathInfo & request.url.search - break - if cmd == "": - 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): - myDir = dir - break - if not fileExists(cmd): - handle.sendResult(ERROR_CGI_FILE_NOT_FOUND) - return - if basename in ["", ".", ".."] or basename.startsWith("~"): - handle.sendResult(ERROR_INVALID_CGI_PATH) - return - var pipefd: array[0..1, cint] # child -> parent - if pipe(pipefd) == -1: - handle.sendResult(ERROR_FAIL_SETUP_CGI) - return - # Pipe the request body as stdin for POST. - var pipefd_read: array[0..1, cint] # parent -> child - if request.body.t notin {rbtNone, rbtCache}: - if pipe(pipefd_read) == -1: - handle.sendResult(ERROR_FAIL_SETUP_CGI) - return - let contentLen = request.body.contentLength() - stdout.flushFile() - stderr.flushFile() - let pid = fork() - if pid == -1: - handle.sendResult(ERROR_FAIL_SETUP_CGI) - elif pid == 0: - discard close(pipefd[0]) # close read - discard dup2(pipefd[1], 1) # dup stdout - discard close(pipefd[1]) - if istream != nil: # cached input (file) - discard dup2(istream.fd, 0) # dup stdin - istream.sclose() - elif request.body.t notin {rbtNone, rbtCache}: - 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, myDir, request, contentLen, - prevURL, insecureSSLNoVerify) - # reset SIGCHLD to the default handler. this is useful if the child process - # expects SIGCHLD to be untouched. (e.g. git dies a horrible death with - # SIGCHLD as SIG_IGN) - signal(SIGCHLD, SIG_DFL) - # close the parent handles - for i in 0 ..< handleMap.len: - if handleMap[i] != nil: - discard close(cint(i)) - discard execl(cstring(cmd), cstring(basename), nil) - let code = int(ERROR_FAILED_TO_EXECUTE_CGI_SCRIPT) - stdout.write("Cha-Control: ConnectionError " & $code & " " & - ($strerror(errno)).deleteChars({'\n', '\r'})) - quit(1) - else: - discard close(pipefd[1]) # close write - var ps: PosixStream = nil - if request.body.t notin {rbtNone, rbtCache}: - discard close(pipefd_read[0]) # close read - ps = newPosixStream(pipefd_read[1]) - case request.body.t - of rbtString: - ps.write(request.body.s) - ps.sclose() - of rbtMultipart: - let boundary = request.body.multipart.boundary - for entry in request.body.multipart.entries: - ps.writeEntry(entry, boundary) - ps.writeEnd(boundary) - ps.sclose() - of rbtOutput: - ostream = ps - of rbtCache: - istream.sclose() - of rbtNone: discard - handle.parser = HeaderParser(headers: newHeaders()) - handle.stream = newPosixStream(pipefd[0]) - -proc parseHeaders0(handle: InputHandle; buffer: LoaderBuffer): int = - let parser = handle.parser - var s = parser.lineBuffer - let L = if buffer == nil: 1 else: buffer.len - for i in 0 ..< L: - template die = - handle.parser = nil - return -1 - let c = if buffer != nil: - char(buffer.page[i]) - else: - '\n' - if parser.crSeen and c != '\n': - die - parser.crSeen = false - if c == '\r': - parser.crSeen = true - elif c == '\n': - if s == "": - if parser.state == hpsBeforeLines: - # body comes immediately, so we haven't had a chance to send result - # yet. - handle.sendResult(0) - handle.sendStatus(parser.status) - handle.sendHeaders(parser.headers) - handle.parser = nil - return i + 1 # +1 to skip \n - case parser.state - of hpsBeforeLines: - case handle.handleFirstLine(s, parser.headers, parser.status) - of crDone: parser.state = hpsControlDone - of crContinue: parser.state = hpsAfterFirstLine - of crError: die - of hpsAfterFirstLine: - case handle.handleControlLine(s, parser.headers, parser.status) - of crDone: parser.state = hpsControlDone - of crContinue: discard - of crError: die - of hpsControlDone: - handle.handleLine(s, parser.headers) - s = "" - else: - s &= c - if s != "": - parser.lineBuffer = s - return L - -proc parseHeaders*(handle: InputHandle; buffer: LoaderBuffer): int = - try: - return handle.parseHeaders0(buffer) - except ErrorBrokenPipe: - handle.parser = nil - return -1 - -proc finishParse*(handle: InputHandle) = - discard handle.parseHeaders(nil) diff --git a/src/loader/loader.nim b/src/loader/loader.nim index 2b846fb5..f99af337 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -33,19 +33,19 @@ import std/tables import io/bufreader import io/bufwriter import io/dynstream -import io/promise import io/serversocket +import io/stdio import io/tempfile import io/urlfilter -import loader/cgi import loader/connecterror import loader/headers import loader/loaderhandle +import loader/loaderiface import loader/request import loader/response import monoucha/javascript -import monoucha/jserror import types/cookie +import types/formdata import types/opt import types/referrer import types/urimethodmap @@ -56,55 +56,6 @@ export request export response type - FileLoader* = ref object - key*: ClientKey - process*: int - clientPid*: int - map: seq[LoaderData] - mapFds*: int # number of fds in map - unregistered*: seq[int] - registerFun*: proc(fd: int) - unregisterFun*: proc(fd: int) - # directory where we store UNIX domain sockets - sockDir*: string - # (FreeBSD only) fd for the socket directory so we can connectat() on it - sockDirFd*: int - - ConnectDataState = enum - cdsBeforeResult, cdsBeforeStatus, cdsBeforeHeaders - - LoaderData = ref object of RootObj - stream*: SocketStream - - ConnectData* = ref object of LoaderData - state: ConnectDataState - status: uint16 - res: int - outputId: int - redirectNum: int - promise: Promise[JSResult[Response]] - request: Request - - OngoingData* = ref object of LoaderData - response*: Response - - LoaderCommand = enum - lcAddCacheFile - lcAddClient - lcGetCacheFile - lcLoad - lcLoadConfig - lcPassFd - lcRedirectToFile - lcRemoveCachedItem - lcRemoveClient - lcResume - lcShareCachedItem - lcSuspend - lcTee - - ClientKey* = array[32, uint8] - CachedItem = ref object id: int path: string @@ -138,16 +89,6 @@ type tmpdir*: string sockdir*: string - LoaderClientConfig* = object - cookieJar*: CookieJar - defaultHeaders*: Headers - filter*: URLFilter - proxy*: URL - referrerPolicy*: ReferrerPolicy - insecureSSLNoVerify*: bool - - FetchPromise* = Promise[JSResult[Response]] - func isPrivileged(ctx: LoaderContext; client: ClientData): bool = return ctx.pagerClient == client @@ -192,6 +133,12 @@ func findCachedHandle(ctx: LoaderContext; cacheId: int): InputHandle = return it return nil +func find(cacheMap: seq[CachedItem]; id: int): int = + for i, it in cacheMap: + if it.id == id: + return i + -1 + type PushBufferResult = enum pbrDone, pbrUnregister @@ -316,6 +263,12 @@ proc addCacheFile(ctx: LoaderContext; client: ClientData; output: OutputHandle): return cacheId return -1 +proc openCachedItem(client: ClientData; id: int): (PosixStream, int) = + let n = client.cacheMap.find(id) + if n != -1: + return (newPosixStream(client.cacheMap[n].path, O_RDONLY, 0), n) + return (nil, -1) + proc put(ctx: LoaderContext; handle: LoaderHandle) = let fd = int(handle.stream.fd) if ctx.handleMap.len <= fd: @@ -336,6 +289,131 @@ proc addFd(ctx: LoaderContext; handle: InputHandle) = ctx.put(handle) ctx.put(output) +type ControlResult = enum + crDone, crContinue, crError + +proc handleFirstLine(handle: InputHandle; line: string; headers: Headers; + status: var uint16): ControlResult = + let k = line.until(':') + if k.len == line.len: + # invalid + handle.sendResult(ERROR_CGI_MALFORMED_HEADER) + return crError + let v = line.substr(k.len + 1).strip() + if k.equalsIgnoreCase("Status"): + handle.sendResult(0) # success + status = parseUInt16(v, allowSign = false).get(0) + return crContinue + if k.equalsIgnoreCase("Cha-Control"): + if v.startsWithIgnoreCase("Connected"): + handle.sendResult(0) # success + return crContinue + elif v.startsWithIgnoreCase("ConnectionError"): + let errs = v.split(' ') + if errs.len <= 1: + handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL) + else: + let fb = int32(ERROR_CGI_INVALID_CHA_CONTROL) + let code = int(parseInt32(errs[1]).get(fb)) + var message = "" + if errs.len > 2: + message &= errs[2] + for i in 3 ..< errs.len: + message &= ' ' + message &= errs[i] + handle.sendResult(code, message) + return crError + elif v.startsWithIgnoreCase("ControlDone"): + return crDone + handle.sendResult(ERROR_CGI_INVALID_CHA_CONTROL) + return crError + handle.sendResult(0) # success + headers.add(k, v) + return crDone + +proc handleControlLine(handle: InputHandle; line: string; headers: Headers; + status: var uint16): ControlResult = + let k = line.until(':') + if k.len == line.len: + # invalid + return crError + let v = line.substr(k.len + 1).strip() + if k.equalsIgnoreCase("Status"): + status = parseUInt16(v, allowSign = false).get(0) + return crContinue + if k.equalsIgnoreCase("Cha-Control"): + if v.startsWithIgnoreCase("ControlDone"): + return crDone + return crError + headers.add(k, v) + return crDone + +# returns false if transfer was interrupted +proc handleLine(handle: InputHandle; line: string; headers: Headers) = + let k = line.until(':') + if k.len == line.len: + # invalid + return + let v = line.substr(k.len + 1).strip() + headers.add(k, v) + +proc parseHeaders0(handle: InputHandle; buffer: LoaderBuffer): int = + let parser = handle.parser + var s = parser.lineBuffer + let L = if buffer == nil: 1 else: buffer.len + for i in 0 ..< L: + template die = + handle.parser = nil + return -1 + let c = if buffer != nil: + char(buffer.page[i]) + else: + '\n' + if parser.crSeen and c != '\n': + die + parser.crSeen = false + if c == '\r': + parser.crSeen = true + elif c == '\n': + if s == "": + if parser.state == hpsBeforeLines: + # body comes immediately, so we haven't had a chance to send result + # yet. + handle.sendResult(0) + handle.sendStatus(parser.status) + handle.sendHeaders(parser.headers) + handle.parser = nil + return i + 1 # +1 to skip \n + case parser.state + of hpsBeforeLines: + case handle.handleFirstLine(s, parser.headers, parser.status) + of crDone: parser.state = hpsControlDone + of crContinue: parser.state = hpsAfterFirstLine + of crError: die + of hpsAfterFirstLine: + case handle.handleControlLine(s, parser.headers, parser.status) + of crDone: parser.state = hpsControlDone + of crContinue: discard + of crError: die + of hpsControlDone: + handle.handleLine(s, parser.headers) + s = "" + else: + s &= c + if s != "": + parser.lineBuffer = s + return L + +proc parseHeaders(handle: InputHandle; buffer: LoaderBuffer): int = + try: + return handle.parseHeaders0(buffer) + except ErrorBrokenPipe: + handle.parser = nil + return -1 + +proc finishParse(handle: InputHandle) = + discard handle.parseHeaders(nil) + type HandleReadResult = enum hrrDone, hrrUnregister, hrrBrokenPipe @@ -410,6 +488,181 @@ proc loadStreamRegular(ctx: LoaderContext; handle, cachedHandle: InputHandle) = handle.outputs.setLen(0) handle.iclose() +proc putMappedURL(url: URL) = + putEnv("MAPPED_URI_SCHEME", url.scheme) + putEnv("MAPPED_URI_USERNAME", url.username) + putEnv("MAPPED_URI_PASSWORD", url.password) + putEnv("MAPPED_URI_HOST", url.hostname) + putEnv("MAPPED_URI_PORT", url.port) + putEnv("MAPPED_URI_PATH", url.path.serialize()) + putEnv("MAPPED_URI_QUERY", url.query.get("")) + +type CGIPath = object + basename: string + pathInfo: string + cmd: string + scriptName: string + requestURI: string + myDir: string + +proc setupEnv(cpath: CGIPath; request: Request; contentLen: int; prevURL: URL; + insecureSSLNoVerify: bool) = + let url = request.url + putEnv("SCRIPT_NAME", cpath.scriptName) + putEnv("SCRIPT_FILENAME", cpath.cmd) + putEnv("REQUEST_URI", cpath.requestURI) + putEnv("REQUEST_METHOD", $request.httpMethod) + var headers = "" + for k, v in request.headers: + headers &= k & ": " & v & "\r\n" + putEnv("REQUEST_HEADERS", headers) + if prevURL != nil: + putMappedURL(prevURL) + if cpath.pathInfo != "": + putEnv("PATH_INFO", cpath.pathInfo) + if url.query.isSome: + putEnv("QUERY_STRING", url.query.get) + if request.httpMethod == hmPost: + if request.body.t == rbtMultipart: + putEnv("CONTENT_TYPE", request.body.multipart.getContentType()) + else: + 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.referrer != nil: + putEnv("HTTP_REFERER", $request.referrer) + if request.proxy != nil: + putEnv("ALL_PROXY", $request.proxy) + if insecureSSLNoVerify: + putEnv("CHA_INSECURE_SSL_NO_VERIFY", "1") + setCurrentDir(cpath.myDir) + +proc parseCGIPath(ctx: LoaderContext; request: Request): CGIPath = + var path = percentDecode(request.url.pathname) + if path.startsWith("/cgi-bin/"): + path.delete(0 .. "/cgi-bin/".high) + elif path.startsWith("/$LIB/"): + path.delete(0 .. "/$LIB/".high) + var cpath = CGIPath() + if path == "" or request.url.hostname != "": + return cpath + if path[0] == '/': + for dir in ctx.config.cgiDir: + if path.startsWith(dir): + cpath.basename = path.substr(dir.len).until('/') + cpath.pathInfo = path.substr(dir.len + cpath.basename.len) + cpath.cmd = dir / cpath.basename + if not fileExists(cpath.cmd): + continue + cpath.myDir = dir + cpath.scriptName = path.substr(0, dir.len + cpath.basename.len) + cpath.requestURI = cpath.cmd / cpath.pathInfo & request.url.search + break + else: + cpath.basename = path.until('/') + cpath.pathInfo = path.substr(cpath.basename.len) + cpath.scriptName = "/cgi-bin/" & cpath.basename + cpath.requestURI = "/cgi-bin/" & path & request.url.search + for dir in ctx.config.cgiDir: + cpath.cmd = dir / cpath.basename + if fileExists(cpath.cmd): + cpath.myDir = dir + break + return cpath + +# Returns a stream on rbtOutput body type. +proc loadCGI(ctx: LoaderContext; client: ClientData; handle: InputHandle; + request: Request; prevURL: URL; insecureSSLNoVerify: bool): PosixStream = + if ctx.config.cgiDir.len == 0: + handle.sendResult(ERROR_NO_CGI_DIR) + return nil + let cpath = ctx.parseCGIPath(request) + if cpath.cmd == "" or cpath.basename in ["", ".", ".."] or + cpath.basename[0] == '~': + handle.sendResult(ERROR_INVALID_CGI_PATH) + return nil + if not fileExists(cpath.cmd): + handle.sendResult(ERROR_CGI_FILE_NOT_FOUND) + return nil + var pipefd: array[0..1, cint] # child -> parent + if pipe(pipefd) == -1: + handle.sendResult(ERROR_FAIL_SETUP_CGI) + return nil + # Pipe the request body as stdin for POST. + var istream: PosixStream = nil # child end (read) + var ostream: PosixStream = nil # parent end (write) + case request.body.t + of rbtString, rbtMultipart, rbtOutput: + var pipefdRead: array[2, cint] # parent -> child + if pipe(pipefdRead) == -1: + handle.sendResult(ERROR_FAIL_SETUP_CGI) + return + istream = newPosixStream(pipefdRead[0]) + ostream = newPosixStream(pipefdRead[1]) + of rbtCache: + var n: int + (istream, n) = client.openCachedItem(request.body.cacheId) + if istream == nil: + handle.sendResult(ERROR_FAIL_SETUP_CGI) + return + of rbtNone: discard + let contentLen = request.body.contentLength() + stdout.flushFile() + stderr.flushFile() + let pid = fork() + if pid == -1: + handle.sendResult(ERROR_FAIL_SETUP_CGI) + elif pid == 0: + discard close(pipefd[0]) # close read + discard dup2(pipefd[1], 1) # dup stdout + discard close(pipefd[1]) + if ostream != nil: + ostream.sclose() # close write + if istream != nil: + if istream.fd != 0: + discard dup2(istream.fd, 0) # dup stdin + istream.sclose() + else: + closeStdin() + # we leave stderr open, so it can be seen in the browser console + setupEnv(cpath, request, contentLen, prevURL, insecureSSLNoVerify) + # reset SIGCHLD to the default handler. this is useful if the child process + # expects SIGCHLD to be untouched. (e.g. git dies a horrible death with + # SIGCHLD as SIG_IGN) + signal(SIGCHLD, SIG_DFL) + # close the parent handles + for i in 0 ..< ctx.handleMap.len: + if ctx.handleMap[i] != nil: + discard close(cint(i)) + discard execl(cstring(cpath.cmd), cstring(cpath.basename), nil) + let code = int(ERROR_FAILED_TO_EXECUTE_CGI_SCRIPT) + stdout.write("Cha-Control: ConnectionError " & $code & " " & + ($strerror(errno)).deleteChars({'\n', '\r'})) + quit(1) + else: + discard close(pipefd[1]) # close write + if request.body.t != rbtNone: + istream.sclose() # close read + handle.parser = HeaderParser(headers: newHeaders()) + handle.stream = newPosixStream(pipefd[0]) + case request.body.t + of rbtString: + ostream.write(request.body.s) + ostream.sclose() + return nil + of rbtMultipart: + let boundary = request.body.multipart.boundary + for entry in request.body.multipart.entries: + ostream.writeEntry(entry, boundary) + ostream.writeEnd(boundary) + ostream.sclose() + return nil + of rbtOutput: + return ostream + of rbtCache, rbtNone: + return nil + proc loadStream(ctx: LoaderContext; client: ClientData; handle: InputHandle; request: Request) = client.passedFdMap.withValue(request.url.pathname, fdp): @@ -430,18 +683,6 @@ proc loadStream(ctx: LoaderContext; client: ClientData; handle: InputHandle; do: handle.sendResult(ERROR_FILE_NOT_FOUND, "stream not found") -func find(cacheMap: seq[CachedItem]; id: int): int = - for i, it in cacheMap: - if it.id == id: - return i - -1 - -proc openCachedItem(client: ClientData; id: int): (PosixStream, int) = - let n = client.cacheMap.find(id) - if n != -1: - return (newPosixStream(client.cacheMap[n].path, O_RDONLY, 0), n) - return (nil, -1) - proc loadFromCache(ctx: LoaderContext; client: ClientData; handle: InputHandle; request: Request) = let id = parseInt32(request.url.pathname).get(-1) @@ -534,13 +775,8 @@ proc loadResource(ctx: LoaderContext; client: ClientData; redo = true continue if request.url.scheme == "cgi-bin": - var istream: PosixStream = nil # for rbtCache - var ostream: PosixStream = nil # for rbtOutput - if request.body.t == rbtCache: - var n: int - (istream, n) = client.openCachedItem(request.body.cacheId) - handle.loadCGI(request, ctx.config.cgiDir, prevurl, - config.insecureSSLNoVerify, ctx.handleMap, istream, ostream) + let ostream = ctx.loadCGI(client, handle, request, prevurl, + config.insecureSSLNoVerify) if handle.stream != nil: if ostream != nil: let outputIn = ctx.findOutput(request.body.outputId, client) @@ -1015,351 +1251,3 @@ proc runFileLoader*(fd: cint; config: LoaderConfig) = unregWrite.add(OutputHandle(handle)) ctx.finishCycle(unregRead, unregWrite) ctx.exitLoader() - -proc getRedirect*(response: Response; request: Request): Request = - if "Location" in response.headers.table: - if response.status in 301u16..303u16 or response.status in 307u16..308u16: - let location = response.headers.table["Location"][0] - let url = parseURL(location, option(request.url)) - if url.isSome: - let status = response.status - if status == 303 and request.httpMethod notin {hmGet, hmHead} or - status == 301 or - status == 302 and request.httpMethod == hmPost: - return newRequest(url.get, hmGet) - else: - return newRequest(url.get, request.httpMethod, body = request.body) - return nil - -template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader; - w, body: untyped) = - stream.withPacketWriter w: - w.swrite(loader.clientPid) - w.swrite(loader.key) - body - -proc connect(loader: FileLoader): SocketStream = - return connectSocketStream(loader.sockDir, loader.sockDirFd, loader.process, - blocking = true) - -# Start a request. This should not block (not for a significant amount of time -# anyway). -proc startRequest(loader: FileLoader; request: Request): SocketStream = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcLoad) - w.swrite(request) - return stream - -proc startRequest*(loader: FileLoader; request: Request; - config: LoaderClientConfig): SocketStream = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcLoadConfig) - w.swrite(request) - w.swrite(config) - return stream - -iterator data*(loader: FileLoader): LoaderData {.inline.} = - for it in loader.map: - if it != nil: - yield it - -iterator ongoing*(loader: FileLoader): OngoingData {.inline.} = - for it in loader.data: - if it of OngoingData: - yield OngoingData(it) - -func fd*(data: LoaderData): int = - return int(data.stream.fd) - -proc put*(loader: FileLoader; data: LoaderData) = - let fd = int(data.stream.fd) - if loader.map.len <= fd: - loader.map.setLen(fd + 1) - assert loader.map[fd] == nil - loader.map[fd] = data - inc loader.mapFds - -proc get*(loader: FileLoader; fd: int): LoaderData = - if fd < loader.map.len: - return loader.map[fd] - return nil - -proc unset*(loader: FileLoader; data: LoaderData) = - let fd = int(data.stream.fd) - if loader.get(fd) != nil: - dec loader.mapFds - loader.map[fd] = nil - -proc fetch0(loader: FileLoader; input: Request; promise: FetchPromise; - redirectNum: int) = - let stream = loader.startRequest(input) - loader.registerFun(int(stream.fd)) - loader.put(ConnectData( - promise: promise, - request: input, - stream: stream, - redirectNum: redirectNum - )) - -proc fetch*(loader: FileLoader; input: Request): FetchPromise = - let promise = FetchPromise() - loader.fetch0(input, promise, 0) - return promise - -proc reconnect*(loader: FileLoader; data: ConnectData) = - data.stream.sclose() - let stream = loader.startRequest(data.request) - let data = ConnectData( - promise: data.promise, - request: data.request, - stream: stream - ) - loader.put(data) - loader.registerFun(data.fd) - -proc suspend*(loader: FileLoader; fds: seq[int]) = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcSuspend) - w.swrite(fds) - stream.sclose() - -proc resume*(loader: FileLoader; fds: openArray[int]) = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcResume) - w.swrite(fds) - stream.sclose() - -proc resume*(loader: FileLoader; fds: int) = - loader.resume([fds]) - -proc tee*(loader: FileLoader; sourceId, targetPid: int): (SocketStream, int) = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcTee) - w.swrite(sourceId) - w.swrite(targetPid) - var outputId: int - var r = stream.initPacketReader() - r.sread(outputId) - return (stream, outputId) - -proc addCacheFile*(loader: FileLoader; outputId, targetPid: int): int = - let stream = loader.connect() - if stream == nil: - return -1 - stream.withLoaderPacketWriter loader, w: - w.swrite(lcAddCacheFile) - w.swrite(outputId) - w.swrite(targetPid) - var r = stream.initPacketReader() - var outputId: int - r.sread(outputId) - stream.sclose() - return outputId - -proc getCacheFile*(loader: FileLoader; cacheId: int): string = - let stream = loader.connect() - if stream == nil: - return "" - stream.withLoaderPacketWriter loader, w: - w.swrite(lcGetCacheFile) - w.swrite(cacheId) - var r = stream.initPacketReader() - var s: string - r.sread(s) - stream.sclose() - return s - -proc redirectToFile*(loader: FileLoader; outputId: int; targetPath: string): - bool = - let stream = loader.connect() - if stream == nil: - return false - stream.withLoaderPacketWriter loader, w: - w.swrite(lcRedirectToFile) - w.swrite(outputId) - w.swrite(targetPath) - var r = stream.initPacketReader() - var res: bool - r.sread(res) - stream.sclose() - return res - -proc onConnected(loader: FileLoader; connectData: ConnectData) = - let stream = connectData.stream - let promise = connectData.promise - let request = connectData.request - var r = stream.initPacketReader() - case connectData.state - of cdsBeforeResult: - var res: int - r.sread(res) # packet 1 - if res == 0: - r.sread(connectData.outputId) # packet 1 - inc connectData.state - else: - var msg: string - # msg is discarded. - #TODO maybe print if called from trusted code (i.e. global == client)? - r.sread(msg) # packet 1 - let fd = connectData.fd - loader.unregisterFun(fd) - loader.unregistered.add(fd) - stream.sclose() - # delete before resolving the promise - loader.unset(connectData) - let err = newTypeError("NetworkError when attempting to fetch resource") - promise.resolve(JSResult[Response].err(err)) - of cdsBeforeStatus: - r.sread(connectData.status) # packet 2 - inc connectData.state - of cdsBeforeHeaders: - let response = newResponse(connectData.res, request, stream, - connectData.outputId, connectData.status) - r.sread(response.headers) # packet 3 - # Only a stream of the response body may arrive after this point. - response.body = stream - # delete before resolving the promise - loader.unset(connectData) - let data = OngoingData(response: response, stream: stream) - loader.put(data) - assert loader.unregisterFun != nil - response.unregisterFun = proc() = - loader.unset(data) - let fd = data.fd - loader.unregistered.add(fd) - loader.unregisterFun(fd) - response.resumeFun = proc(outputId: int) = - loader.resume(outputId) - stream.setBlocking(false) - let redirect = response.getRedirect(request) - if redirect != nil: - response.unregisterFun() - stream.sclose() - let redirectNum = connectData.redirectNum + 1 - if redirectNum < 5: #TODO use config.network.max_redirect? - loader.fetch0(redirect, promise, redirectNum) - else: - let err = newTypeError("NetworkError when attempting to fetch resource") - promise.resolve(JSResult[Response].err(err)) - else: - promise.resolve(JSResult[Response].ok(response)) - -proc onRead*(loader: FileLoader; data: OngoingData) = - let response = data.response - response.onRead(response) - if response.body.isend: - if response.onFinish != nil: - response.onFinish(response, true) - response.onFinish = nil - response.close() - -proc onRead*(loader: FileLoader; fd: int) = - let data = loader.map[fd] - if data of ConnectData: - loader.onConnected(ConnectData(data)) - else: - loader.onRead(OngoingData(data)) - -proc onError*(loader: FileLoader; data: OngoingData) = - let response = data.response - if response.onFinish != nil: - response.onFinish(response, false) - response.onFinish = nil - response.close() - -proc onError*(loader: FileLoader; fd: int): bool = - let data = loader.map[fd] - if data of ConnectData: - # probably shouldn't happen. TODO - return false - else: - loader.onError(OngoingData(data)) - return true - -# Note: this blocks until headers are received. -proc doRequest*(loader: FileLoader; request: Request): Response = - let stream = loader.startRequest(request) - let response = Response(url: request.url) - var r = stream.initPacketReader() - r.sread(response.res) # packet 1 - if response.res == 0: - r.sread(response.outputId) # packet 1 - r = stream.initPacketReader() - r.sread(response.status) # packet 2 - r = stream.initPacketReader() - r.sread(response.headers) # packet 3 - # Only a stream of the response body may arrive after this point. - response.body = stream - response.resumeFun = proc(outputId: int) = - loader.resume(outputId) - else: - var msg: string - r.sread(msg) # packet 1 - stream.sclose() - return response - -proc shareCachedItem*(loader: FileLoader; id, targetPid: int; sourcePid = -1) = - let stream = loader.connect() - if stream != nil: - let sourcePid = if sourcePid != -1: sourcePid else: loader.clientPid - stream.withLoaderPacketWriter loader, w: - w.swrite(lcShareCachedItem) - w.swrite(sourcePid) - w.swrite(targetPid) - w.swrite(id) - stream.sclose() - -proc passFd*(loader: FileLoader; id: string; fd: FileHandle) = - let stream = loader.connect() - if stream != nil: - stream.withLoaderPacketWriter loader, w: - w.swrite(lcPassFd) - w.swrite(id) - stream.sendFileHandle(fd) - stream.sclose() - -proc removeCachedItem*(loader: FileLoader; cacheId: int) = - let stream = loader.connect() - if stream != nil: - stream.withLoaderPacketWriter loader, w: - w.swrite(lcRemoveCachedItem) - w.swrite(cacheId) - stream.sclose() - -proc addClient*(loader: FileLoader; key: ClientKey; pid: int; - config: LoaderClientConfig; clonedFrom: int): bool = - let stream = loader.connect() - stream.withLoaderPacketWriter loader, w: - w.swrite(lcAddClient) - w.swrite(key) - w.swrite(pid) - w.swrite(config) - w.swrite(clonedFrom) - var r = stream.initPacketReader() - var res: bool - r.sread(res) - stream.sclose() - return res - -proc removeClient*(loader: FileLoader; pid: int) = - let stream = loader.connect() - if stream != nil: - stream.withLoaderPacketWriter loader, w: - w.swrite(lcRemoveClient) - w.swrite(pid) - stream.sclose() - -when defined(freebsd): - let O_DIRECTORY* {.importc, header: "<fcntl.h>", noinit.}: cint - -proc setSocketDir*(loader: FileLoader; path: string) = - loader.sockDir = path - when defined(freebsd): - loader.sockDirFd = open(cstring(path), O_DIRECTORY) - else: - loader.sockDirFd = -1 diff --git a/src/loader/loaderiface.nim b/src/loader/loaderiface.nim new file mode 100644 index 00000000..c8635c6f --- /dev/null +++ b/src/loader/loaderiface.nim @@ -0,0 +1,429 @@ +# Interface to loader/loader. The idea is that modules don't have to +# depend on the entire loader implementation to interact with it. +# +# See loader/loader for a more detailed description of the protocol. + +import std/tables + +import io/bufreader +import io/bufwriter +import io/dynstream +import io/promise +import io/urlfilter +import loader/headers +import loader/request +import loader/response +import monoucha/javascript +import monoucha/jserror +import types/cookie +import types/opt +import types/referrer +import types/url + +type + FileLoader* = ref object + key*: ClientKey + process*: int + clientPid*: int + map: seq[LoaderData] + mapFds*: int # number of fds in map + unregistered*: seq[int] + registerFun*: proc(fd: int) + unregisterFun*: proc(fd: int) + # directory where we store UNIX domain sockets + sockDir*: string + # (FreeBSD only) fd for the socket directory so we can connectat() on it + sockDirFd*: int + + ConnectDataState = enum + cdsBeforeResult, cdsBeforeStatus, cdsBeforeHeaders + + LoaderData = ref object of RootObj + stream*: SocketStream + + ConnectData* = ref object of LoaderData + state: ConnectDataState + status: uint16 + res: int + outputId: int + redirectNum: int + promise: FetchPromise + request: Request + + OngoingData* = ref object of LoaderData + response*: Response + + LoaderCommand* = enum + lcAddCacheFile + lcAddClient + lcGetCacheFile + lcLoad + lcLoadConfig + lcPassFd + lcRedirectToFile + lcRemoveCachedItem + lcRemoveClient + lcResume + lcShareCachedItem + lcSuspend + lcTee + + ClientKey* = array[32, uint8] + + LoaderClientConfig* = object + cookieJar*: CookieJar + defaultHeaders*: Headers + filter*: URLFilter + proxy*: URL + referrerPolicy*: ReferrerPolicy + insecureSSLNoVerify*: bool + + FetchPromise* = Promise[JSResult[Response]] + +proc getRedirect*(response: Response; request: Request): Request = + if "Location" in response.headers.table: + if response.status in 301u16..303u16 or response.status in 307u16..308u16: + let location = response.headers.table["Location"][0] + let url = parseURL(location, option(request.url)) + if url.isSome: + let status = response.status + if status == 303 and request.httpMethod notin {hmGet, hmHead} or + status == 301 or + status == 302 and request.httpMethod == hmPost: + return newRequest(url.get, hmGet) + else: + return newRequest(url.get, request.httpMethod, body = request.body) + return nil + +template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader; + w, body: untyped) = + stream.withPacketWriter w: + w.swrite(loader.clientPid) + w.swrite(loader.key) + body + +proc connect(loader: FileLoader): SocketStream = + return connectSocketStream(loader.sockDir, loader.sockDirFd, loader.process, + blocking = true) + +# Start a request. This should not block (not for a significant amount of time +# anyway). +proc startRequest(loader: FileLoader; request: Request): SocketStream = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcLoad) + w.swrite(request) + return stream + +proc startRequest*(loader: FileLoader; request: Request; + config: LoaderClientConfig): SocketStream = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcLoadConfig) + w.swrite(request) + w.swrite(config) + return stream + +iterator data*(loader: FileLoader): LoaderData {.inline.} = + for it in loader.map: + if it != nil: + yield it + +iterator ongoing*(loader: FileLoader): OngoingData {.inline.} = + for it in loader.data: + if it of OngoingData: + yield OngoingData(it) + +func fd*(data: LoaderData): int = + return int(data.stream.fd) + +proc put*(loader: FileLoader; data: LoaderData) = + let fd = int(data.stream.fd) + if loader.map.len <= fd: + loader.map.setLen(fd + 1) + assert loader.map[fd] == nil + loader.map[fd] = data + inc loader.mapFds + +proc get*(loader: FileLoader; fd: int): LoaderData = + if fd < loader.map.len: + return loader.map[fd] + return nil + +proc unset*(loader: FileLoader; data: LoaderData) = + let fd = int(data.stream.fd) + if loader.get(fd) != nil: + dec loader.mapFds + loader.map[fd] = nil + +proc fetch0(loader: FileLoader; input: Request; promise: FetchPromise; + redirectNum: int) = + let stream = loader.startRequest(input) + loader.registerFun(int(stream.fd)) + loader.put(ConnectData( + promise: promise, + request: input, + stream: stream, + redirectNum: redirectNum + )) + +proc fetch*(loader: FileLoader; input: Request): FetchPromise = + let promise = FetchPromise() + loader.fetch0(input, promise, 0) + return promise + +proc reconnect*(loader: FileLoader; data: ConnectData) = + data.stream.sclose() + let stream = loader.startRequest(data.request) + let data = ConnectData( + promise: data.promise, + request: data.request, + stream: stream + ) + loader.put(data) + loader.registerFun(data.fd) + +proc suspend*(loader: FileLoader; fds: seq[int]) = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcSuspend) + w.swrite(fds) + stream.sclose() + +proc resume*(loader: FileLoader; fds: openArray[int]) = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcResume) + w.swrite(fds) + stream.sclose() + +proc resume*(loader: FileLoader; fds: int) = + loader.resume([fds]) + +proc tee*(loader: FileLoader; sourceId, targetPid: int): (SocketStream, int) = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcTee) + w.swrite(sourceId) + w.swrite(targetPid) + var outputId: int + var r = stream.initPacketReader() + r.sread(outputId) + return (stream, outputId) + +proc addCacheFile*(loader: FileLoader; outputId, targetPid: int): int = + let stream = loader.connect() + if stream == nil: + return -1 + stream.withLoaderPacketWriter loader, w: + w.swrite(lcAddCacheFile) + w.swrite(outputId) + w.swrite(targetPid) + var r = stream.initPacketReader() + var outputId: int + r.sread(outputId) + stream.sclose() + return outputId + +proc getCacheFile*(loader: FileLoader; cacheId: int): string = + let stream = loader.connect() + if stream == nil: + return "" + stream.withLoaderPacketWriter loader, w: + w.swrite(lcGetCacheFile) + w.swrite(cacheId) + var r = stream.initPacketReader() + var s: string + r.sread(s) + stream.sclose() + return s + +proc redirectToFile*(loader: FileLoader; outputId: int; targetPath: string): + bool = + let stream = loader.connect() + if stream == nil: + return false + stream.withLoaderPacketWriter loader, w: + w.swrite(lcRedirectToFile) + w.swrite(outputId) + w.swrite(targetPath) + var r = stream.initPacketReader() + var res: bool + r.sread(res) + stream.sclose() + return res + +proc onConnected(loader: FileLoader; connectData: ConnectData) = + let stream = connectData.stream + let promise = connectData.promise + let request = connectData.request + var r = stream.initPacketReader() + case connectData.state + of cdsBeforeResult: + var res: int + r.sread(res) # packet 1 + if res == 0: + r.sread(connectData.outputId) # packet 1 + inc connectData.state + else: + var msg: string + # msg is discarded. + #TODO maybe print if called from trusted code (i.e. global == client)? + r.sread(msg) # packet 1 + let fd = connectData.fd + loader.unregisterFun(fd) + loader.unregistered.add(fd) + stream.sclose() + # delete before resolving the promise + loader.unset(connectData) + let err = newTypeError("NetworkError when attempting to fetch resource") + promise.resolve(JSResult[Response].err(err)) + of cdsBeforeStatus: + r.sread(connectData.status) # packet 2 + inc connectData.state + of cdsBeforeHeaders: + let response = newResponse(connectData.res, request, stream, + connectData.outputId, connectData.status) + r.sread(response.headers) # packet 3 + # Only a stream of the response body may arrive after this point. + response.body = stream + # delete before resolving the promise + loader.unset(connectData) + let data = OngoingData(response: response, stream: stream) + loader.put(data) + assert loader.unregisterFun != nil + response.unregisterFun = proc() = + loader.unset(data) + let fd = data.fd + loader.unregistered.add(fd) + loader.unregisterFun(fd) + response.resumeFun = proc(outputId: int) = + loader.resume(outputId) + stream.setBlocking(false) + let redirect = response.getRedirect(request) + if redirect != nil: + response.unregisterFun() + stream.sclose() + let redirectNum = connectData.redirectNum + 1 + if redirectNum < 5: #TODO use config.network.max_redirect? + loader.fetch0(redirect, promise, redirectNum) + else: + let err = newTypeError("NetworkError when attempting to fetch resource") + promise.resolve(JSResult[Response].err(err)) + else: + promise.resolve(JSResult[Response].ok(response)) + +proc onRead*(loader: FileLoader; data: OngoingData) = + let response = data.response + response.onRead(response) + if response.body.isend: + if response.onFinish != nil: + response.onFinish(response, true) + response.onFinish = nil + response.close() + +proc onRead*(loader: FileLoader; fd: int) = + let data = loader.map[fd] + if data of ConnectData: + loader.onConnected(ConnectData(data)) + else: + loader.onRead(OngoingData(data)) + +proc onError*(loader: FileLoader; data: OngoingData) = + let response = data.response + if response.onFinish != nil: + response.onFinish(response, false) + response.onFinish = nil + response.close() + +proc onError*(loader: FileLoader; fd: int): bool = + let data = loader.map[fd] + if data of ConnectData: + # probably shouldn't happen. TODO + return false + else: + loader.onError(OngoingData(data)) + return true + +# Note: this blocks until headers are received. +proc doRequest*(loader: FileLoader; request: Request): Response = + let stream = loader.startRequest(request) + let response = Response(url: request.url) + var r = stream.initPacketReader() + r.sread(response.res) # packet 1 + if response.res == 0: + r.sread(response.outputId) # packet 1 + r = stream.initPacketReader() + r.sread(response.status) # packet 2 + r = stream.initPacketReader() + r.sread(response.headers) # packet 3 + # Only a stream of the response body may arrive after this point. + response.body = stream + response.resumeFun = proc(outputId: int) = + loader.resume(outputId) + else: + var msg: string + r.sread(msg) # packet 1 + stream.sclose() + return response + +proc shareCachedItem*(loader: FileLoader; id, targetPid: int; sourcePid = -1) = + let stream = loader.connect() + if stream != nil: + let sourcePid = if sourcePid != -1: sourcePid else: loader.clientPid + stream.withLoaderPacketWriter loader, w: + w.swrite(lcShareCachedItem) + w.swrite(sourcePid) + w.swrite(targetPid) + w.swrite(id) + stream.sclose() + +proc passFd*(loader: FileLoader; id: string; fd: FileHandle) = + let stream = loader.connect() + if stream != nil: + stream.withLoaderPacketWriter loader, w: + w.swrite(lcPassFd) + w.swrite(id) + stream.sendFileHandle(fd) + stream.sclose() + +proc removeCachedItem*(loader: FileLoader; cacheId: int) = + let stream = loader.connect() + if stream != nil: + stream.withLoaderPacketWriter loader, w: + w.swrite(lcRemoveCachedItem) + w.swrite(cacheId) + stream.sclose() + +proc addClient*(loader: FileLoader; key: ClientKey; pid: int; + config: LoaderClientConfig; clonedFrom: int): bool = + let stream = loader.connect() + stream.withLoaderPacketWriter loader, w: + w.swrite(lcAddClient) + w.swrite(key) + w.swrite(pid) + w.swrite(config) + w.swrite(clonedFrom) + var r = stream.initPacketReader() + var res: bool + r.sread(res) + stream.sclose() + return res + +proc removeClient*(loader: FileLoader; pid: int) = + let stream = loader.connect() + if stream != nil: + stream.withLoaderPacketWriter loader, w: + w.swrite(lcRemoveClient) + w.swrite(pid) + stream.sclose() + +when defined(freebsd): + let O_DIRECTORY* {.importc, header: "<fcntl.h>", noinit.}: cint + +proc setSocketDir*(loader: FileLoader; path: string) = + loader.sockDir = path + when defined(freebsd): + loader.sockDirFd = open(cstring(path), O_DIRECTORY) + else: + loader.sockDirFd = -1 diff --git a/src/local/client.nim b/src/local/client.nim index b8e131f8..bf276aa3 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -28,8 +28,9 @@ import js/intl import js/jsmodule import js/timeout import loader/headers -import loader/loader +import loader/loaderiface import loader/request +import loader/response import local/container import local/lineedit import local/pager diff --git a/src/local/container.nim b/src/local/container.nim index c6907982..6310c8d6 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -7,18 +7,19 @@ import std/tables import chagashi/charset import config/config import config/mimetypes -import types/bitmap import io/dynstream import io/promise import io/serversocket import layout/renderdocument import loader/headers -import loader/loader +import loader/loaderiface import loader/request +import loader/response import monoucha/javascript import monoucha/jsregex import monoucha/jstypes import server/buffer +import types/bitmap import types/blob import types/cell import types/color diff --git a/src/local/pager.nim b/src/local/pager.nim index 8b449f24..48241e4c 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -12,7 +12,6 @@ import chagashi/charset import config/chapath import config/config import config/mailcap -import types/bitmap import io/bufreader import io/dynstream import io/promise @@ -23,8 +22,9 @@ import js/timeout import layout/renderdocument import loader/connecterror import loader/headers -import loader/loader +import loader/loaderiface import loader/request +import loader/response import local/container import local/lineedit import local/term @@ -39,6 +39,7 @@ import monoucha/quickjs import monoucha/tojs import server/buffer import server/forkserver +import types/bitmap import types/blob import types/cell import types/color diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 66a63b6a..1cb78492 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -35,7 +35,8 @@ import js/console import js/timeout import layout/renderdocument import loader/headers -import loader/loader +import loader/loaderiface +import loader/request import monoucha/fromjs import monoucha/javascript import monoucha/jsregex diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim index b34c97b6..aa66a42b 100644 --- a/src/server/forkserver.nim +++ b/src/server/forkserver.nim @@ -13,6 +13,7 @@ import io/dynstream import io/serversocket import io/stdio import loader/loader +import loader/loaderiface import server/buffer import types/urimethodmap import types/url |