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 /src/loader | |
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".
Diffstat (limited to 'src/loader')
-rw-r--r-- | src/loader/cgi.nim | 303 | ||||
-rw-r--r-- | src/loader/loader.nim | 746 | ||||
-rw-r--r-- | src/loader/loaderiface.nim | 429 |
3 files changed, 746 insertions, 732 deletions
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 |