This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new 4810b66087 Add strong ETag support 4810b66087 is described below commit 4810b660870d6b42bab6b076f57d444cc30b8d8c Author: remm <r...@apache.org> AuthorDate: Tue Nov 19 23:24:27 2024 +0100 Add strong ETag support Using a useStrongETags init parameter on the default (and WebDAV) servlet. The ETag generated is a SHA-1 checksum of the file content. --- java/org/apache/catalina/WebResource.java | 13 +++++- .../apache/catalina/servlets/DefaultServlet.java | 16 ++++++- .../catalina/webresources/AbstractResource.java | 52 ++++++++++++++++++++++ .../catalina/webresources/CachedResource.java | 5 +++ .../catalina/servlets/TestWebdavServlet.java | 16 +++++++ webapps/docs/changelog.xml | 6 +++ 6 files changed, 105 insertions(+), 3 deletions(-) diff --git a/java/org/apache/catalina/WebResource.java b/java/org/apache/catalina/WebResource.java index d71af62f26..ba5adec5e6 100644 --- a/java/org/apache/catalina/WebResource.java +++ b/java/org/apache/catalina/WebResource.java @@ -92,13 +92,22 @@ public interface WebResource { String getWebappPath(); /** - * Return the strong ETag if available (currently not supported) else return the weak ETag calculated from the - * content length and last modified. + * Return the weak ETag calculated from the content length and last modified. * * @return The ETag for this resource */ String getETag(); + /** + * Return the strong ETag if available else return the weak ETag calculated from the + * content length and last modified. + * + * @return The ETag for this resource + */ + default String getStrongETag() { + return getETag(); + } + /** * Set the MIME type for this Resource. * diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java b/java/org/apache/catalina/servlets/DefaultServlet.java index d288725b0d..c5e4c5a2c8 100644 --- a/java/org/apache/catalina/servlets/DefaultServlet.java +++ b/java/org/apache/catalina/servlets/DefaultServlet.java @@ -260,6 +260,11 @@ public class DefaultServlet extends HttpServlet { */ private boolean allowPartialPut = true; + /** + * Use strong etags whenever possible. + */ + private boolean useStrongETags = false; + // --------------------------------------------------------- Public Methods @@ -394,6 +399,11 @@ public class DefaultServlet extends HttpServlet { if (getServletConfig().getInitParameter("allowPartialPut") != null) { allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut")); } + + if (getServletConfig().getInitParameter("useStrongETags") != null) { + useStrongETags = Boolean.parseBoolean(getServletConfig().getInitParameter("useStrongETags")); + } + } private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) { @@ -2229,7 +2239,11 @@ public class DefaultServlet extends HttpServlet { * @return The result of calling {@link WebResource#getETag()} on the given resource */ protected String generateETag(WebResource resource) { - return resource.getETag(); + if (useStrongETags) { + return resource.getStrongETag(); + } else { + return resource.getETag(); + } } diff --git a/java/org/apache/catalina/webresources/AbstractResource.java b/java/org/apache/catalina/webresources/AbstractResource.java index 384c327321..2bd0bdf356 100644 --- a/java/org/apache/catalina/webresources/AbstractResource.java +++ b/java/org/apache/catalina/webresources/AbstractResource.java @@ -16,13 +16,18 @@ */ package org.apache.catalina.webresources; +import java.io.IOException; import java.io.InputStream; +import java.security.MessageDigest; 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.tomcat.util.buf.HexUtils; import org.apache.tomcat.util.http.FastHttpDateFormat; import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; public abstract class AbstractResource implements WebResource { @@ -33,6 +38,7 @@ public abstract class AbstractResource implements WebResource { private String mimeType = null; private volatile String weakETag; + private volatile String strongETag; protected AbstractResource(WebResourceRoot root, String webAppPath) { @@ -75,6 +81,52 @@ public abstract class AbstractResource implements WebResource { return weakETag; } + @Override + public final String getStrongETag() { + if (strongETag == null) { + synchronized (this) { + if (strongETag == null) { + long contentLength = getContentLength(); + long lastModified = getLastModified(); + if (contentLength > 0 && lastModified > 0) { + try (InputStream is = getInputStream()) { + if (contentLength <= 16 * 1024) { + byte[] buf = new byte[(int) contentLength]; + int n = IOTools.readFully(is, buf); + if (n > 0) { + buf = ConcurrentMessageDigest.digest("SHA-1", buf); + strongETag = HexUtils.toHexString(buf); + } else { + strongETag = getETag(); + } + } else { + byte[] buf = new byte[4096]; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + while (true) { + int n = is.read(buf); + if (n <= 0) { + break; + } + digest.update(buf, 0, n); + } + strongETag = HexUtils.toHexString(digest.digest()); + } catch (Exception e) { + strongETag = getETag(); + } + } + } catch (IOException e) { + strongETag = getETag(); + } + } else { + strongETag = getETag(); + } + } + } + } + return strongETag; + } + @Override public final void setMimeType(String mimeType) { this.mimeType = mimeType; diff --git a/java/org/apache/catalina/webresources/CachedResource.java b/java/org/apache/catalina/webresources/CachedResource.java index ef9236fc1a..74fa278d20 100644 --- a/java/org/apache/catalina/webresources/CachedResource.java +++ b/java/org/apache/catalina/webresources/CachedResource.java @@ -291,6 +291,11 @@ public class CachedResource implements WebResource { return webResource.getETag(); } + @Override + public String getStrongETag() { + return webResource.getStrongETag(); + } + @Override public void setMimeType(String mimeType) { webResource.setMimeType(mimeType); diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java index af51e99ffa..1a9d28ac29 100644 --- a/test/org/apache/catalina/servlets/TestWebdavServlet.java +++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java @@ -361,6 +361,7 @@ public class TestWebdavServlet extends TomcatBaseTest { webdavServlet.addInitParameter("listings", "true"); webdavServlet.addInitParameter("secret", "foo"); webdavServlet.addInitParameter("readonly", "false"); + webdavServlet.addInitParameter("useStrongETags", "true"); ctxt.addServletMappingDecoded("/*", "webdav"); tomcat.start(); @@ -607,6 +608,19 @@ public class TestWebdavServlet extends TomcatBaseTest { client.processRequest(true); Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3000; i++) { + sb.append(CONTENT); + } + client.setRequest(new String[] { "PUT /file6.txt HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "Content-Length: " + String.valueOf(sb.length()) + SimpleHttpClient.CRLF + + "Connection: Close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + sb.toString() }); + client.connect(); + client.processRequest(true); + Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode()); + // Verify that everything created is there client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + @@ -618,6 +632,8 @@ public class TestWebdavServlet extends TomcatBaseTest { Assert.assertFalse(client.getResponseBody().contains("/myfolder/file4.txt")); Assert.assertTrue(client.getResponseBody().contains("/file7.txt")); Assert.assertTrue(client.getResponseBody().contains("Second-")); + Assert.assertTrue(client.getResponseBody().contains("d1dc021f456864e84f9a37b7a6f51c51301128a0")); + Assert.assertTrue(client.getResponseBody().contains("f3390fe2e5546dac3d1968970df1a222a3a39c00")); String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-")); timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<')); Assert.assertTrue(Integer.valueOf(timeoutValue).intValue() <= 20); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 87fee2d758..ebdf0f6a2d 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -153,6 +153,12 @@ Avoid quotes for numeric values in the JSON generated by the status servlet. (remm) </fix> + <add> + Add strong ETag support for the WebDAV and default servlet, which can + be enabled by using the <code>useStrongETags</code> init parameter with + a value set to <code>true</code>. The ETag generated will be a SHA-1 + checksum of the resource content. (remm) + </add> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org