Skip to content

Commit

Permalink
Add strong ETag support
Browse files Browse the repository at this point in the history
Using a useStrongETags init parameter on the default (and WebDAV)
servlet.
The ETag generated is a SHA-1 checksum of the file content.
  • Loading branch information
rmaucher committed Nov 19, 2024
1 parent a6546c4 commit 7ce9e07
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 3 deletions.
13 changes: 11 additions & 2 deletions java/org/apache/catalina/WebResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
16 changes: 15 additions & 1 deletion java/org/apache/catalina/servlets/DefaultServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ public class DefaultServlet extends HttpServlet {
*/
private boolean allowPartialPut = true;

/**
* Use strong etags whenever possible.
*/
private boolean useStrongETags = false;


// --------------------------------------------------------- Public Methods

Expand Down Expand Up @@ -404,6 +409,11 @@ public void init() throws ServletException {
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) {
Expand Down Expand Up @@ -2288,7 +2298,11 @@ protected boolean checkIfUnmodifiedSince(HttpServletRequest request, 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();
}
}


Expand Down
52 changes: 52 additions & 0 deletions java/org/apache/catalina/webresources/AbstractResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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) {
Expand Down Expand Up @@ -75,6 +81,52 @@ public final String getETag() {
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;
Expand Down
5 changes: 5 additions & 0 deletions java/org/apache/catalina/webresources/CachedResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ public String getETag() {
return webResource.getETag();
}

@Override
public String getStrongETag() {
return webResource.getStrongETag();
}

@Override
public void setMimeType(String mimeType) {
webResource.setMimeType(mimeType);
Expand Down
16 changes: 16 additions & 0 deletions test/org/apache/catalina/servlets/TestWebdavServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ public void testBasicOperations() throws Exception {
webdavServlet.addInitParameter("listings", "true");
webdavServlet.addInitParameter("secret", "foo");
webdavServlet.addInitParameter("readonly", "false");
webdavServlet.addInitParameter("useStrongETags", "true");
ctxt.addServletMappingDecoded("/*", "webdav");
tomcat.start();

Expand Down Expand Up @@ -607,6 +608,19 @@ public void testBasicOperations() throws Exception {
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 +
Expand All @@ -618,6 +632,8 @@ public void testBasicOperations() throws Exception {
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);
Expand Down
6 changes: 6 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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">
Expand Down

0 comments on commit 7ce9e07

Please sign in to comment.