about summary refs log tree commit diff stats
path: root/src/loader
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-15 19:10:58 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-15 19:13:08 +0200
commit4a394302f3197dfa48edb09bd0773aa94dc8188d (patch)
treee67aff6c2dfcf628a9ae9558ee6a6280ab31c0a7 /src/loader
parent7656357382d603243c6b3e5f80557a23e4666313 (diff)
downloadchawan-4a394302f3197dfa48edb09bd0773aa94dc8188d.tar.gz
loader: fix rbtCache race
I thought this would work fine, but then images weren't loading. Upon
closer inspection I realized we don't necessarily let the image decoder
finish reading the entire file on pass 1 (header parsing), which means
pass 2 (decoding) can end up reading a partially downloaded cache file.

To fix this, we now fall back to streaming mode when an open handle to
the cached item still exists.

(An alternative would be to wait for the cached item to finish loading,
but that's harder to implement and might be slower anyway - the current
implementation allows for the decoder to run while an image is still
being loaded, but we couldn't do that with the second approach.)
Diffstat (limited to 'src/loader')
-rw-r--r--src/loader/connecterror.nim1
-rw-r--r--src/loader/loader.nim78
-rw-r--r--src/loader/loaderhandle.nim6
3 files changed, 49 insertions, 36 deletions
diff --git a/src/loader/connecterror.nim b/src/loader/connecterror.nim
index c3099b0f..d8b06881 100644
--- a/src/loader/connecterror.nim
+++ b/src/loader/connecterror.nim
@@ -1,4 +1,5 @@
 type ConnectErrorCode* = enum
+  ERROR_CGI_CACHED_BODY_NOT_FOUND = (-18, "cached request body not found")
   ERROR_FAILED_TO_REDIRECT = (-17, "failed to redirect request body")
   ERROR_URL_NOT_IN_CACHE = (-16, "URL was not found in the cache")
   ERROR_FILE_NOT_IN_CACHE = (-15, "file was not found in the cache")
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index f99af337..e7a45833 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -571,42 +571,51 @@ proc parseCGIPath(ctx: LoaderContext; request: Request): CGIPath =
         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 =
+    request: Request; prevURL: URL; insecureSSLNoVerify: bool) =
   if ctx.config.cgiDir.len == 0:
     handle.sendResult(ERROR_NO_CGI_DIR)
-    return nil
+    return
   let cpath = ctx.parseCGIPath(request)
   if cpath.cmd == "" or cpath.basename in ["", ".", ".."] or
       cpath.basename[0] == '~':
     handle.sendResult(ERROR_INVALID_CGI_PATH)
-    return nil
+    return
   if not fileExists(cpath.cmd):
     handle.sendResult(ERROR_CGI_FILE_NOT_FOUND)
-    return nil
+    return
   var pipefd: array[0..1, cint] # child -> parent
   if pipe(pipefd) == -1:
     handle.sendResult(ERROR_FAIL_SETUP_CGI)
-    return nil
+    return
   # 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 istream2: PosixStream = nil # child end (read) for rbtCache
+  var cachedHandle: InputHandle = nil # for rbtCache
+  var outputIn: OutputHandle = nil # for rbtOutput
+  if request.body.t == rbtCache:
+    var n: int
+    (istream, n) = client.openCachedItem(request.body.cacheId)
+    if istream == nil:
+      handle.sendResult(ERROR_CGI_CACHED_BODY_NOT_FOUND)
+      return
+    cachedHandle = ctx.findCachedHandle(request.body.cacheId)
+    if cachedHandle != nil: # cached item still open, switch to streaming mode
+      istream2 = istream
+  elif request.body.t == rbtOutput:
+    outputIn = ctx.findOutput(request.body.outputId, client)
+    if outputIn == nil:
+      handle.sendResult(ERROR_FAIL_SETUP_CGI)
+      return
+  if request.body.t in {rbtString, rbtMultipart, rbtOutput} or
+      request.body.t == rbtCache and istream2 != nil:
     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()
@@ -619,6 +628,8 @@ proc loadCGI(ctx: LoaderContext; client: ClientData; handle: InputHandle;
     discard close(pipefd[1])
     if ostream != nil:
       ostream.sclose() # close write
+    if istream2 != nil:
+      istream2.sclose() # close cache file; we aren't reading it directly
     if istream != nil:
       if istream.fd != 0:
         discard dup2(istream.fd, 0) # dup stdin
@@ -650,18 +661,30 @@ proc loadCGI(ctx: LoaderContext; client: ClientData; handle: InputHandle;
     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
+      ostream.setBlocking(false)
+      let output = outputIn.tee(ostream, ctx.getOutputId(), client.pid)
+      ctx.put(output)
+      output.suspended = false
+      if not output.isEmpty:
+        ctx.register(output)
+    of rbtCache:
+      if ostream != nil:
+        let handle = newInputHandle(ostream, ctx.getOutputId(), client.pid,
+          suspended = false)
+        handle.stream = istream2
+        ostream.setBlocking(false)
+        ctx.loadStreamRegular(handle, cachedHandle)
+        assert handle.stream == nil
+        handle.close()
+    of rbtNone:
+      discard
 
 proc loadStream(ctx: LoaderContext; client: ClientData; handle: InputHandle;
     request: Request) =
@@ -775,23 +798,10 @@ proc loadResource(ctx: LoaderContext; client: ClientData;
           redo = true
           continue
     if request.url.scheme == "cgi-bin":
-      let ostream = ctx.loadCGI(client, handle, request, prevurl,
-        config.insecureSSLNoVerify)
+      ctx.loadCGI(client, handle, request, prevurl, config.insecureSSLNoVerify)
       if handle.stream != nil:
-        if ostream != nil:
-          let outputIn = ctx.findOutput(request.body.outputId, client)
-          if outputIn != nil:
-            ostream.setBlocking(false)
-            let output = outputIn.tee(ostream, ctx.getOutputId(), client.pid)
-            ctx.put(output)
-            output.suspended = false
-            if not output.isEmpty:
-              ctx.register(output)
-          else:
-            ostream.sclose()
         ctx.addFd(handle)
       else:
-        assert ostream == nil
         handle.close()
     elif request.url.scheme == "stream":
       ctx.loadStream(client, handle, request)
diff --git a/src/loader/loaderhandle.nim b/src/loader/loaderhandle.nim
index acca6e34..b43149fb 100644
--- a/src/loader/loaderhandle.nim
+++ b/src/loader/loaderhandle.nim
@@ -1,5 +1,6 @@
 import std/deques
 import std/net
+import std/posix
 import std/tables
 
 import io/bufwriter
@@ -68,14 +69,15 @@ when defined(debug):
     return s
 
 # Create a new loader handle, with the output stream ostream.
-proc newInputHandle*(ostream: PosixStream; outputId, pid: int): InputHandle =
+proc newInputHandle*(ostream: PosixStream; outputId, pid: int;
+    suspended = true): InputHandle =
   let handle = InputHandle(cacheId: -1)
   handle.outputs.add(OutputHandle(
     stream: ostream,
     parent: handle,
     outputId: outputId,
     ownerPid: pid,
-    suspended: true
+    suspended: suspended
   ))
   return handle