This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/10.1.x by this push:
     new ecf1b71f9c Fix BZ 69439 - Improve handling of multiple Cache-Control 
headers
ecf1b71f9c is described below

commit ecf1b71f9c5a0dc95a192c0443c713137efbc7b5
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Wed Nov 20 19:17:10 2024 +0000

    Fix BZ 69439 - Improve handling of multiple Cache-Control headers
    
    Based on #777 by Chenjp
---
 .../org/apache/catalina/filters/ExpiresFilter.java |  20 ++--
 .../apache/catalina/filters/TestExpiresFilter.java | 127 +++++++++++++++++----
 webapps/docs/changelog.xml                         |   5 +
 3 files changed, 122 insertions(+), 30 deletions(-)

diff --git a/java/org/apache/catalina/filters/ExpiresFilter.java 
b/java/org/apache/catalina/filters/ExpiresFilter.java
index 1cb05f0d82..8b5291f973 100644
--- a/java/org/apache/catalina/filters/ExpiresFilter.java
+++ b/java/org/apache/catalina/filters/ExpiresFilter.java
@@ -165,10 +165,10 @@ import org.apache.tomcat.util.buf.StringUtils;
  * <h4>
  * {@code ExpiresExcludedResponseStatusCodes}</h4>
  * <p>
- * This directive defines the http response status codes for which the
+ * This directive defines the HTTP response status codes for which the
  * {@code ExpiresFilter} will not generate expiration headers. By default, the
  * {@code 304} status code (&quot;{@code Not modified}&quot;) is skipped. The
- * value is a comma separated list of http status codes.
+ * value is a comma separated list of HTTP status codes.
  * </p>
  * <p>
  * This directive is useful to ease usage of {@code ExpiresDefault} directive.
@@ -332,7 +332,7 @@ import org.apache.tomcat.util.buf.StringUtils;
  * </p>
  * <p>
  * To trap the &#x27;before write response body&#x27; event, the
- * {@code ExpiresFilter} wraps the http servlet response&#x27;s writer and
+ * {@code ExpiresFilter} wraps the HTTP servlet response&#x27;s writer and
  * outputStream to intercept calls to the methods {@code write()},
  * {@code print()}, {@code close()} and {@code flush()}. For empty response
  * body (e.g. empty files), the {@code write()}, {@code print()},
@@ -517,7 +517,7 @@ public class ExpiresFilter extends FilterBase {
 
     /**
      * <p>
-     * Wrapping extension of the {@link HttpServletResponse} to yrap the 
"Start Write Response Body" event.
+     * Wrapping extension of the {@link HttpServletResponse} to wrap the 
"Start Write Response Body" event.
      * </p>
      * <p>
      * For performance optimization : this extended response holds the {@link 
#lastModifiedHeader} and
@@ -528,12 +528,12 @@ public class ExpiresFilter extends FilterBase {
     public class XHttpServletResponse extends HttpServletResponseWrapper {
 
         /**
-         * Value of the {@code Cache-Control} http response header if it has 
been set.
+         * Value of the {@code Cache-Control} HTTP response header if it has 
been set.
          */
         private String cacheControlHeader;
 
         /**
-         * Value of the {@code Last-Modified} http response header if it has 
been set.
+         * Value of the {@code Last-Modified} HTTP response header if it has 
been set.
          */
         private long lastModifiedHeader;
 
@@ -568,8 +568,12 @@ public class ExpiresFilter extends FilterBase {
         @Override
         public void addHeader(String name, String value) {
             super.addHeader(name, value);
-            if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name) && 
cacheControlHeader == null) {
-                cacheControlHeader = value;
+            if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name)) {
+                if (cacheControlHeader == null) {
+                    cacheControlHeader = value;
+                } else {
+                    cacheControlHeader = StringUtils.join(cacheControlHeader, 
value);
+                }
             }
         }
 
diff --git a/test/org/apache/catalina/filters/TestExpiresFilter.java 
b/test/org/apache/catalina/filters/TestExpiresFilter.java
index 83a82d3a6e..edb15813dc 100644
--- a/test/org/apache/catalina/filters/TestExpiresFilter.java
+++ b/test/org/apache/catalina/filters/TestExpiresFilter.java
@@ -42,6 +42,7 @@ import 
org.apache.catalina.filters.ExpiresFilter.StartingPoint;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.buf.StringUtils;
 import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
 import org.apache.tomcat.util.descriptor.web.FilterDef;
 import org.apache.tomcat.util.descriptor.web.FilterMap;
@@ -92,8 +93,8 @@ public class TestExpiresFilter extends TomcatBaseTest {
             Assert.assertEquals(1, 
expiresConfigurationDefault.getDurations().get(0).getAmount());
 
             // VERIFY TEXT/HTML
-            ExpiresConfiguration expiresConfigurationTextHtml = 
expiresFilter.getExpiresConfigurationByContentType()
-                    .get("text/html");
+            ExpiresConfiguration expiresConfigurationTextHtml =
+                    
expiresFilter.getExpiresConfigurationByContentType().get("text/html");
             Assert.assertEquals(StartingPoint.ACCESS_TIME, 
expiresConfigurationTextHtml.getStartingPoint());
 
             Assert.assertEquals(3, 
expiresConfigurationTextHtml.getDurations().size());
@@ -111,8 +112,8 @@ public class TestExpiresFilter extends TomcatBaseTest {
             Assert.assertEquals(2, twoHours.getAmount());
 
             // VERIFY IMAGE/GIF
-            ExpiresConfiguration expiresConfigurationImageGif = 
expiresFilter.getExpiresConfigurationByContentType()
-                    .get("image/gif");
+            ExpiresConfiguration expiresConfigurationImageGif =
+                    
expiresFilter.getExpiresConfigurationByContentType().get("image/gif");
             Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME, 
expiresConfigurationImageGif.getStartingPoint());
 
             Assert.assertEquals(2, 
expiresConfigurationImageGif.getDurations().size());
@@ -126,8 +127,8 @@ public class TestExpiresFilter extends TomcatBaseTest {
             Assert.assertEquals(3, threeMinutes.getAmount());
 
             // VERIFY IMAGE/JPG
-            ExpiresConfiguration expiresConfigurationImageJpg = 
expiresFilter.getExpiresConfigurationByContentType()
-                    .get("image/jpg");
+            ExpiresConfiguration expiresConfigurationImageJpg =
+                    
expiresFilter.getExpiresConfigurationByContentType().get("image/jpg");
             Assert.assertEquals(StartingPoint.ACCESS_TIME, 
expiresConfigurationImageJpg.getStartingPoint());
 
             Assert.assertEquals(1, 
expiresConfigurationImageJpg.getDurations().size());
@@ -137,8 +138,8 @@ public class TestExpiresFilter extends TomcatBaseTest {
             Assert.assertEquals(10000, tenThousandSeconds.getAmount());
 
             // VERIFY VIDEO/MPEG
-            ExpiresConfiguration expiresConfiguration = 
expiresFilter.getExpiresConfigurationByContentType()
-                    .get("video/mpeg");
+            ExpiresConfiguration expiresConfiguration =
+                    
expiresFilter.getExpiresConfigurationByContentType().get("video/mpeg");
             Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME, 
expiresConfiguration.getStartingPoint());
 
             Assert.assertEquals(1, expiresConfiguration.getDurations().size());
@@ -173,8 +174,8 @@ public class TestExpiresFilter extends TomcatBaseTest {
     @Test
     public void testParseExpiresConfigurationCombinedDuration() {
         ExpiresFilter expiresFilter = new ExpiresFilter();
-        ExpiresConfiguration actualConfiguration = expiresFilter
-                .parseExpiresConfiguration("access plus 1 month 15 days 2 
hours");
+        ExpiresConfiguration actualConfiguration =
+                expiresFilter.parseExpiresConfiguration("access plus 1 month 
15 days 2 hours");
 
         Assert.assertEquals(StartingPoint.ACCESS_TIME, 
actualConfiguration.getStartingPoint());
 
@@ -226,7 +227,7 @@ public class TestExpiresFilter extends TomcatBaseTest {
             }
         };
 
-        validate(servlet, null, HttpServletResponse.SC_NOT_MODIFIED);
+        validate(servlet, null, null, HttpServletResponse.SC_NOT_MODIFIED);
     }
 
     @Test
@@ -362,11 +363,11 @@ public class TestExpiresFilter extends TomcatBaseTest {
     }
 
     protected void validate(HttpServlet servlet, Integer 
expectedMaxAgeInSeconds) throws Exception {
-        validate(servlet, expectedMaxAgeInSeconds, HttpServletResponse.SC_OK);
+        validate(servlet, expectedMaxAgeInSeconds, null, 
HttpServletResponse.SC_OK);
     }
 
-    protected void validate(HttpServlet servlet, Integer 
expectedMaxAgeInSeconds, int expectedResponseStatusCode)
-            throws Exception {
+    protected void validate(HttpServlet servlet, Integer 
expectedMaxAgeInSeconds, Boolean expectedExpires,
+            int expectedResponseStatusCode) throws Exception {
 
         // SETUP
 
@@ -401,14 +402,14 @@ public class TestExpiresFilter extends TomcatBaseTest {
 
             // TEST
             ByteChunk bc = new ByteChunk();
-            Map<String, List<String>> responseHeaders = new HashMap<>();
+            Map<String,List<String>> responseHeaders = new HashMap<>();
             int rc = getUrl("http://localhost:"; + getPort() + "/test", bc, 
responseHeaders);
 
             // VALIDATE
             Assert.assertEquals(expectedResponseStatusCode, rc);
 
             StringBuilder msg = new StringBuilder();
-            for (Entry<String, List<String>> field : 
responseHeaders.entrySet()) {
+            for (Entry<String,List<String>> field : 
responseHeaders.entrySet()) {
                 for (String value : field.getValue()) {
                     msg.append((field.getKey() == null ? "" : field.getKey() + 
": ") + value + "\n");
                 }
@@ -417,7 +418,7 @@ public class TestExpiresFilter extends TomcatBaseTest {
 
             Integer actualMaxAgeInSeconds;
 
-            String cacheControlHeader = getSingleHeader("Cache-Control", 
responseHeaders);
+            String cacheControlHeader = 
StringUtils.join(responseHeaders.get("Cache-Control"));
 
             if (cacheControlHeader == null) {
                 actualMaxAgeInSeconds = null;
@@ -438,13 +439,30 @@ public class TestExpiresFilter extends TomcatBaseTest {
             }
 
             if (expectedMaxAgeInSeconds == null) {
-                Assert.assertNull("actualMaxAgeInSeconds '" + 
actualMaxAgeInSeconds + "' should be null",
-                        actualMaxAgeInSeconds);
+                Assert.assertTrue(
+                        "actualMaxAgeInSeconds [" + actualMaxAgeInSeconds +
+                                "] should be null or 'no-store' should be set",
+                        actualMaxAgeInSeconds == null ||
+                                (cacheControlHeader != null && 
cacheControlHeader.contains("no-store")));
                 return;
             }
 
             Assert.assertNotNull(actualMaxAgeInSeconds);
 
+            if (expectedExpires != null) {
+                if (expectedExpires.booleanValue()) {
+                    // Expires is expected and should be greater than date
+                    Assert.assertNotNull(responseHeaders.get("Expires"));
+                    String expiresString = 
responseHeaders.get("Expires").get(0);
+                    long expiresDate = 
FastHttpDateFormat.parseDate(expiresString);
+                    Assert.assertTrue(
+                            "Expires header value [" + expiresString + "] does 
not specify a date in the future",
+                            expiresDate > System.currentTimeMillis());
+                } else {
+                    Assert.assertNull(responseHeaders.get("Expires"));
+                }
+            }
+
             String contentType = getSingleHeader("Content-Type", 
responseHeaders);
 
             int deltaInSeconds = Math.abs(actualMaxAgeInSeconds.intValue() - 
expectedMaxAgeInSeconds.intValue());
@@ -495,11 +513,11 @@ public class TestExpiresFilter extends TomcatBaseTest {
         tomcat.start();
 
         ByteChunk bc = new ByteChunk();
-        Map<String, List<String>> requestHeaders = new 
CaseInsensitiveKeyMap<>();
+        Map<String,List<String>> requestHeaders = new 
CaseInsensitiveKeyMap<>();
         List<String> ifModifiedSinceValues = new ArrayList<>();
         ifModifiedSinceValues.add(FastHttpDateFormat.getCurrentDate());
         requestHeaders.put("If-Modified-Since", ifModifiedSinceValues);
-        Map<String, List<String>> responseHeaders = new 
CaseInsensitiveKeyMap<>();
+        Map<String,List<String>> responseHeaders = new 
CaseInsensitiveKeyMap<>();
 
         int rc = getUrl("http://localhost:"; + getPort() + 
"/test/bug6nnnn/bug69303.txt", bc, requestHeaders,
                 responseHeaders);
@@ -507,7 +525,7 @@ public class TestExpiresFilter extends TomcatBaseTest {
         Assert.assertEquals(HttpServletResponse.SC_NOT_MODIFIED, rc);
 
         StringBuilder msg = new StringBuilder();
-        for (Entry<String, List<String>> field : responseHeaders.entrySet()) {
+        for (Entry<String,List<String>> field : responseHeaders.entrySet()) {
             for (String value : field.getValue()) {
                 msg.append((field.getKey() == null ? "" : field.getKey() + ": 
") + value + "\n");
             }
@@ -539,4 +557,69 @@ public class TestExpiresFilter extends TomcatBaseTest {
         Assert.assertNotNull(actualMaxAgeInSeconds);
         Assert.assertTrue(Math.abs(actualMaxAgeInSeconds.intValue() - 420) < 
3);
     }
+
+
+    @Test
+    public void testBug69439CacheControlDirectiveConflict() throws Exception {
+        HttpServlet servlet = new HttpServlet() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            protected void service(HttpServletRequest request, 
HttpServletResponse response)
+                    throws ServletException, IOException {
+                response.setContentType("image/jpeg");
+                response.addHeader("Cache-Control", "public");
+                response.addHeader("Cache-Control", "no-store");
+                response.addHeader("Cache-Control", "max-age=300");
+                response.getWriter().print("Hello world");
+            }
+        };
+        // rfc9111 - 4.2.1 - If directives conflict (e.g., both max-age and 
no-cache are present), the most restrictive
+        // directive should be honored.
+        // final effective Cache-Control: no-store
+        // skip ExpiresFilter
+        validate(servlet, null, Boolean.FALSE, HttpServletResponse.SC_OK);
+    }
+
+
+    @Test
+    public void testBug69439CacheControlMaxAgePredefined() throws Exception {
+        HttpServlet servlet = new HttpServlet() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            protected void service(HttpServletRequest request, 
HttpServletResponse response)
+                    throws ServletException, IOException {
+                response.setContentType("image/jpeg");
+                response.addHeader("Cache-Control", "public");
+                response.addHeader("Cache-Control", "max-age=600");
+                response.getWriter().print("Hello world");
+            }
+        };
+        // rfc9111 - 4.2.1 - If directives conflict (e.g., both max-age and 
no-cache are present), the most restrictive
+        // directive should be honored.
+        // final effective Cache-Control: public, max-age=600
+        // skip ExpiresFilter
+        validate(servlet, Integer.valueOf(600), Boolean.FALSE, 
HttpServletResponse.SC_OK);
+    }
+
+
+    @Test
+    public void testBug69439CacheControlUndefined() throws Exception {
+        HttpServlet servlet = new HttpServlet() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            protected void service(HttpServletRequest request, 
HttpServletResponse response)
+                    throws ServletException, IOException {
+                response.setContentType("image/jpeg");
+                response.getWriter().print("Hello world");
+            }
+        };
+        // rfc9111 - 4.2.1 - If directives conflict (e.g., both max-age and 
no-cache are present), the most restrictive
+        // directive should be honored.
+        // final effective Cache-Control: public, max-age=600
+        // apply ExpiresFilter
+        validate(servlet, Integer.valueOf(60), Boolean.TRUE, 
HttpServletResponse.SC_OK);
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 30124ef5b3..e340fc7087 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -154,6 +154,11 @@
       <fix>
         Use client locale for directory listings. (remm)
       </fix>
+      <fix>
+        <bug>69439</bug>: Improve the handling of multiple
+        <code>Cache-Control</code> headers in the <code>ExpiresFilter</code>.
+        Based on pull request <pr>777</pr> by Chenjp. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to