From 8df00018a252baa9497615d6420fb6c10466fa74 Mon Sep 17 00:00:00 2001
From: Mark Thomas <markt@apache.org>
Date: Mon, 28 Apr 2025 12:58:21 +0100
Subject: [PATCH] Refactor CGI servlet to access resources via WebResources
---
From 8cb95ff03221067c511b3fa66d4f745bc4b0a605 Mon Sep 17 00:00:00 2001
From: Mark Thomas <markt@apache.org>
Date: Fri, 2 May 2025 16:42:30 +0100
Subject: [PATCH] Use WebResource API to differentiate files and directories

It is much easier/more efficient to do this directly than via the
ServletContext API.
---
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/CGIServlet.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/CGIServlet.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/CGIServlet.java
@@ -43,12 +43,16 @@ import javax.servlet.RequestDispatcher;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
+import org.apache.catalina.Globals;
+import org.apache.catalina.WebResource;
+import org.apache.catalina.WebResourceRoot;
 import org.apache.catalina.util.IOTools;
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
@@ -286,6 +290,8 @@ public final class CGIServlet extends Ht
     private Set<String> cgiMethods = new HashSet<>();
     private boolean cgiMethodsAll = false;
 
+    private transient WebResourceRoot resources = null;
+
 
     /**
      * The time (in milliseconds) to wait for the reading of stderr to complete
@@ -439,6 +445,13 @@ public final class CGIServlet extends Ht
         } else if (value != null) {
             cmdLineArgumentsDecodedPattern = Pattern.compile(value);
         }
+
+        // Load the web resources
+        resources = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR);
+
+        if (resources == null) {
+            throw new UnavailableException(sm.getString("cgiServlet.noResources"));
+        }
     }
 
 
@@ -692,9 +705,6 @@ public final class CGIServlet extends Ht
         /** pathInfo for the current request */
         private String pathInfo = null;
 
-        /** real file system directory of the enclosing servlet's web app */
-        private String webAppRootDir = null;
-
         /** tempdir for context - used to expand scripts in unexpanded wars */
         private File tmpDir = null;
 
@@ -752,7 +762,6 @@ public final class CGIServlet extends Ht
          */
         protected void setupFromContext(ServletContext context) {
             this.context = context;
-            this.webAppRootDir = context.getRealPath("/");
             this.tmpDir = (File) context.getAttribute(ServletContext.TEMPDIR);
         }
 
@@ -870,12 +879,11 @@ public final class CGIServlet extends Ht
          *
          *</p>
          *
-         * @param pathInfo       String from HttpServletRequest.getPathInfo()
-         * @param webAppRootDir  String from context.getRealPath("/")
          * @param contextPath    String as from
          *                       HttpServletRequest.getContextPath()
          * @param servletPath    String as from
          *                       HttpServletRequest.getServletPath()
+         * @param pathInfo      String from HttpServletRequest.getPathInfo()
          * @param cgiPathPrefix  subdirectory of webAppRootDir below which
          *                       the web app's CGIs may be stored; can be null.
          *                       The CGI search path will start at
@@ -903,61 +911,101 @@ public final class CGIServlet extends Ht
          *                        cgi script, or null if no cgi was found
          * </ul>
          */
-        protected String[] findCGI(String pathInfo, String webAppRootDir,
-                                   String contextPath, String servletPath,
-                                   String cgiPathPrefix) {
-            String path = null;
-            String name = null;
-            String scriptname = null;
+        protected String[] findCGI(String contextPath, String servletPath, String pathInfo, String cgiPathPrefix) {
 
-            if (webAppRootDir != null &&
-                    webAppRootDir.lastIndexOf(File.separator) == (webAppRootDir.length() - 1)) {
-                //strip the trailing "/" from the webAppRootDir
-                webAppRootDir = webAppRootDir.substring(0, (webAppRootDir.length() - 1));
-            }
+            StringBuilder cgiPath = new StringBuilder();
+            StringBuilder urlPath = new StringBuilder();
 
-            if (cgiPathPrefix != null) {
-                webAppRootDir = webAppRootDir + File.separator + cgiPathPrefix;
-            }
+            WebResource cgiScript = null;
 
-            if (log.isDebugEnabled()) {
-                log.debug(sm.getString("cgiServlet.find.path", pathInfo, webAppRootDir));
+            if (cgiPathPrefix == null || cgiPathPrefix.isEmpty()) {
+                cgiPath.append(servletPath);
+            } else {
+                cgiPath.append('/');
+                cgiPath.append(cgiPathPrefix);
             }
+            urlPath.append(servletPath);
 
-            File currentLocation = new File(webAppRootDir);
-            StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/");
-            if (log.isDebugEnabled()) {
-                log.debug(sm.getString("cgiServlet.find.location",
-                        currentLocation.getAbsolutePath()));
-            }
-            StringBuilder cginameBuilder = new StringBuilder();
-            while (!currentLocation.isFile() && dirWalker.hasMoreElements()) {
-                String nextElement = (String) dirWalker.nextElement();
-                currentLocation = new File(currentLocation, nextElement);
-                cginameBuilder.append('/').append(nextElement);
+            StringTokenizer pathWalker = new StringTokenizer(pathInfo, "/");
+
+            while (pathWalker.hasMoreElements() && (cgiScript == null || !cgiScript.isFile())) {
+                String urlSegment = pathWalker.nextToken();
+                cgiPath.append('/');
+                cgiPath.append(urlSegment);
+                urlPath.append('/');
+                urlPath.append(urlSegment);
                 if (log.isDebugEnabled()) {
-                    log.debug(sm.getString("cgiServlet.find.location",
-                            currentLocation.getAbsolutePath()));
+                    log.trace(sm.getString("cgiServlet.find.location", cgiPath.toString()));
                 }
+                cgiScript = resources.getResource(cgiPath.toString());
             }
-            String cginame = cginameBuilder.toString();
-            if (!currentLocation.isFile()) {
+
+            // No script was found
+            if (cgiScript == null || !cgiScript.isFile()) {
                 return new String[] { null, null, null, null };
             }
 
-            path = currentLocation.getAbsolutePath();
-            name = currentLocation.getName();
+            // Set-up return values
+            String path = null;
+            String scriptName = null;
+            String cgiName = null;
+            String name = null;
 
-            if (servletPath.startsWith(cginame)) {
-                scriptname = contextPath + cginame;
-            } else {
-                scriptname = contextPath + servletPath + cginame;
+            path = cgiScript.getCanonicalPath();
+            if (path == null) {
+                /*
+                 * The script doesn't exist directly on the file system. It might be located in an archive or similar.
+                 * Such scripts are extracted to the web application's temporary file location.
+                 */
+                File tmpCgiFile = new File(tmpDir + cgiPath.toString());
+                if (!tmpCgiFile.exists()) {
+
+                    // Create directories
+                    File parent = tmpCgiFile.getParentFile();
+                    if (!parent.mkdirs() && !parent.isDirectory()) {
+                        log.warn(sm.getString("cgiServlet.expandCreateDirFail", parent.getAbsolutePath()));
+                        return new String[] { null, null, null, null };
+                    }
+
+                    try (InputStream is = cgiScript.getInputStream()) {
+                        synchronized (expandFileLock) {
+                            // Check if file was created by concurrent request
+                            if (!tmpCgiFile.exists()) {
+                                try {
+                                    Files.copy(is, tmpCgiFile.toPath());
+                                } catch (IOException ioe) {
+                                    log.warn(sm.getString("cgiServlet.expandFail", cgiScript.getURL(),
+                                            tmpCgiFile.getAbsolutePath()), ioe);
+                                    if (tmpCgiFile.exists()) {
+                                        if (!tmpCgiFile.delete()) {
+                                            log.warn(sm.getString("cgiServlet.expandDeleteFail",
+                                                    tmpCgiFile.getAbsolutePath()));
+                                        }
+                                    }
+                                    return new String[] { null, null, null, null };
+                                }
+                                if (log.isDebugEnabled()) {
+                                    log.debug(sm.getString("cgiServlet.expandOk", cgiScript.getURL(),
+                                            tmpCgiFile.getAbsolutePath()));
+                                }
+                            }
+                        }
+                    } catch (IOException ioe) {
+                        log.warn(sm.getString("cgiServlet.expandCloseFail", cgiScript.getURL()), ioe);
+                    }
+                }
+                path = tmpCgiFile.getAbsolutePath();
             }
 
+            scriptName = urlPath.toString();
+            cgiName = scriptName.substring(servletPath.length());
+            name = scriptName.substring(scriptName.lastIndexOf('/') + 1);
+
             if (log.isDebugEnabled()) {
-                log.debug(sm.getString("cgiServlet.find.found", name, path, scriptname, cginame));
+                log.trace(sm.getString("cgiServlet.find.found", name, path, scriptName, cgiName));
             }
-            return new String[] { path, scriptname, cginame, name };
+
+            return new String[] { path, scriptName, cgiName, name };
         }
 
         /**
@@ -996,17 +1044,7 @@ public final class CGIServlet extends Ht
             sPathInfoOrig = this.pathInfo;
             sPathInfoOrig = sPathInfoOrig == null ? "" : sPathInfoOrig;
 
-            if (webAppRootDir == null ) {
-                // The app has not been deployed in exploded form
-                webAppRootDir = tmpDir.toString();
-                expandCGIScript();
-            }
-
-            sCGINames = findCGI(sPathInfoOrig,
-                                webAppRootDir,
-                                contextPath,
-                                servletPath,
-                                cgiPathPrefix);
+            sCGINames = findCGI(contextPath, servletPath, sPathInfoOrig, cgiPathPrefix);
 
             sCGIFullPath = sCGINames[0];
             sCGIScriptName = sCGINames[1];
@@ -1139,93 +1177,6 @@ public final class CGIServlet extends Ht
             return true;
         }
 
-        /**
-         * Extracts requested resource from web app archive to context work
-         * directory to enable CGI script to be executed.
-         */
-        protected void expandCGIScript() {
-            StringBuilder srcPath = new StringBuilder();
-            StringBuilder destPath = new StringBuilder();
-            InputStream is = null;
-
-            // paths depend on mapping
-            if (cgiPathPrefix == null ) {
-                srcPath.append(pathInfo);
-                is = context.getResourceAsStream(srcPath.toString());
-                destPath.append(tmpDir);
-                destPath.append(pathInfo);
-            } else {
-                // essentially same search algorithm as findCGI()
-                srcPath.append(cgiPathPrefix);
-                StringTokenizer pathWalker =
-                        new StringTokenizer (pathInfo, "/");
-                // start with first element
-                while (pathWalker.hasMoreElements() && (is == null)) {
-                    srcPath.append("/");
-                    srcPath.append(pathWalker.nextElement());
-                    is = context.getResourceAsStream(srcPath.toString());
-                }
-                destPath.append(tmpDir);
-                destPath.append("/");
-                destPath.append(srcPath);
-            }
-
-            if (is == null) {
-                // didn't find anything, give up now
-                log.warn(sm.getString("cgiServlet.expandNotFound", srcPath));
-                return;
-            }
-
-            try {
-                File f = new File(destPath.toString());
-                if (f.exists()) {
-                    // Don't need to expand if it already exists
-                    return;
-                }
-
-                // create directories
-                File dir = f.getParentFile();
-                if (!dir.mkdirs() && !dir.isDirectory()) {
-                    log.warn(sm.getString("cgiServlet.expandCreateDirFail", dir.getAbsolutePath()));
-                    return;
-                }
-
-                try {
-                    synchronized (expandFileLock) {
-                        // make sure file doesn't exist
-                        if (f.exists()) {
-                            return;
-                        }
-
-                        // create file
-                        if (!f.createNewFile()) {
-                            return;
-                        }
-
-                        Files.copy(is, f.toPath());
-
-                        if (log.isDebugEnabled()) {
-                            log.debug(sm.getString("cgiServlet.expandOk", srcPath, destPath));
-                        }
-                    }
-                } catch (IOException ioe) {
-                    log.warn(sm.getString("cgiServlet.expandFail", srcPath, destPath), ioe);
-                    // delete in case file is corrupted
-                    if (f.exists()) {
-                        if (!f.delete()) {
-                            log.warn(sm.getString("cgiServlet.expandDeleteFail", f.getAbsolutePath()));
-                        }
-                    }
-                }
-            } finally {
-                try {
-                    is.close();
-                } catch (IOException e) {
-                    log.warn(sm.getString("cgiServlet.expandCloseFail", srcPath), e);
-                }
-            }
-        }
-
 
         /**
          * Returns important CGI environment information in a multi-line text
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/LocalStrings.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings.properties
@@ -18,13 +18,13 @@ cgiServlet.expandCloseFail=Failed to clo
 cgiServlet.expandCreateDirFail=Failed to create destination directory [{0}] for script expansion
 cgiServlet.expandDeleteFail=Failed to delete file at [{0}] after IOException during expansion
 cgiServlet.expandFail=Failed to expand script at path [{0}] to [{1}]
-cgiServlet.expandNotFound=Unable to expand [{0}] as it could not be found
 cgiServlet.expandOk=Expanded script at path [{0}] to [{1}]
 cgiServlet.find.found=Found CGI: name [{0}], path [{1}], script name [{2}] and CGI name [{3}]
 cgiServlet.find.location=Looking for a file at [{0}]
 cgiServlet.find.path=CGI script requested at path [{0}] relative to CGI location [{1}]
 cgiServlet.invalidArgumentDecoded=The decoded command line argument [{0}] did not match the configured cmdLineArgumentsDecoded pattern [{1}]
 cgiServlet.invalidArgumentEncoded=The encoded command line argument [{0}] did not match the configured cmdLineArgumentsEncoded pattern [{1}]
+cgiServlet.noResources=No static resources were found
 cgiServlet.runBadHeader=Bad header line [{0}]
 cgiServlet.runFail=I/O problems processing CGI
 cgiServlet.runHeaderReaderFail=I/O problems closing header reader
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_fr.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/LocalStrings_fr.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_fr.properties
@@ -18,7 +18,6 @@ cgiServlet.expandCloseFail=Impossible de
 cgiServlet.expandCreateDirFail=Echec de la création du répertoire de destination [{0}] pour la décompression du script
 cgiServlet.expandDeleteFail=Impossible d''effacer le fichier [{0}] suite à une IOException pendant la décompression
 cgiServlet.expandFail=Impossible de faire l''expansion du script au chemin [{0}] vers [{1}]
-cgiServlet.expandNotFound=Impossible de décompresser [{0}] car il n''a pas été trouvé
 cgiServlet.expandOk=Extrait le script du chemin [{0}] vers [{1}]
 cgiServlet.find.found=Trouvé le CGI : nom [{0}], chemin [{1}], nom de script [{2}] et nom du CGI [{3}]
 cgiServlet.find.location=Recherche d''un fichier en [{0}]
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_ja.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/LocalStrings_ja.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_ja.properties
@@ -18,7 +18,6 @@ cgiServlet.expandCloseFail=パス[{0}]
 cgiServlet.expandCreateDirFail=スクリプトの展開先ディレクトリ[{0}]の作成に失敗しました。
 cgiServlet.expandDeleteFail=拡張中にIOExceptionの後に[{0}]でファイルを削除できませんでした
 cgiServlet.expandFail=パス[{0}]のスクリプトを[{1}]に展開できませんでした
-cgiServlet.expandNotFound=見つけることが出来なかったので[{0}]を展開できませんでした。
 cgiServlet.expandOk=パス[{0}]の[{1}]に展開されたスクリプト
 cgiServlet.find.found=見つかったCGI：名前[{0}]、パス[{1}]、スクリプト名[{2}]、CGI名[{3}]
 cgiServlet.find.location=ファイル [{0}] を探しています。
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_ko.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/LocalStrings_ko.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_ko.properties
@@ -18,7 +18,6 @@ cgiServlet.expandCloseFail=경로 [{0}]
 cgiServlet.expandCreateDirFail=스크립트를 압축해제 하기 위한 대상 디렉토리 [{0}]을(를) 생성하지 못했습니다.
 cgiServlet.expandDeleteFail=압축해제 중 IOException이 발생한 후, [{0}]에 위치한 해당 파일을 삭제하지 못했습니다.
 cgiServlet.expandFail=경로 [{0}]의 스크립트를 [{1}](으)로 압축해제 하지 못했습니다.
-cgiServlet.expandNotFound=[{0}]을(를) 찾을 수 없어서 압축해제 할 수 없습니다.
 cgiServlet.expandOk=[{0}] 경로에 있는 스트립트가 [{1}](으)로 압축 해제되었습니다.
 cgiServlet.find.found=CGI 발견: 이름 [{0}], 경로 [{1}], 스크립트 이름 [{2}], CGI 이름 [{3}]
 cgiServlet.find.location=[{0}]에 위치한 파일을 찾는 중
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties
@@ -18,7 +18,6 @@ cgiServlet.expandCloseFail=无法关闭
 cgiServlet.expandCreateDirFail=无法为脚本扩展创建目标目录[{0}]
 cgiServlet.expandDeleteFail=扩展期间，发生IOException异常后删除文件[{0}]失败
 cgiServlet.expandFail=在路径[{0}] 到[{1}] 展开脚本失败.
-cgiServlet.expandNotFound=无法展开[{0}]，因为找不到它。
 cgiServlet.expandOk=从路径[{0}]到[{1}]扩展脚本
 cgiServlet.find.found=找到CGI:name[{0}]、path[{1}]、script name[{2}]和CGI name[{3}]
 cgiServlet.find.location=在 [{0}] 查找文件
Index: apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
===================================================================
--- apache-tomcat-9.0.36-src.orig/webapps/docs/changelog.xml
+++ apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
@@ -139,6 +139,10 @@
         RewriteValve and document how <code>%nn</code> URL encoding may be used
         with rewrite rules. (markt)
       </fix>
+      <scode>
+        Refactor GCI servlet to access resources via the
+        <code>WebResource</code> API. (markt)
+      </scode>
     </changelog>
   </subsection>
   <subsection name="Coyote">
