Add weak ETag support in ShallowEtagHeaderFilter
This commit adds weak ETag support in ShallowEtagHeaderFilter. This improves the behavior of the filter in tow ways: * weak ETags in request headers such as `W/"0badc0ffee"` will be compared with a "weak comparison" (matching both weak and strong ETags of the same value) * when enabled with the "writeWeakETag" init param, the filter will write weak Etags in its HTTP responses Issue: SPR-13778
This commit is contained in:
@@ -19,6 +19,7 @@ package org.springframework.web.filter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
@@ -60,11 +61,28 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING";
|
||||
|
||||
|
||||
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
|
||||
private static final boolean servlet3Present =
|
||||
ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class);
|
||||
|
||||
private boolean writeWeakETag = false;
|
||||
|
||||
/**
|
||||
* Set whether the ETag value written to the response should be weak, as per rfc7232.
|
||||
* <p>Should be configured using an {@code <init-param>} for parameter name
|
||||
* "writeWeakETag" in the filter definition in {@code web.xml}.
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">rfc7232 section-2.3</a>
|
||||
*/
|
||||
public boolean isWriteWeakETag() {
|
||||
return writeWeakETag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the ETag value written to the response should be weak, as per rfc7232.
|
||||
*/
|
||||
public void setWriteWeakETag(boolean writeWeakETag) {
|
||||
this.writeWeakETag = writeWeakETag;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default value is "false" so that the filter may delay the generation of
|
||||
@@ -102,10 +120,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
|
||||
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
|
||||
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag);
|
||||
rawResponse.setHeader(HEADER_ETAG, responseETag);
|
||||
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
|
||||
if (responseETag.equals(requestETag)) {
|
||||
if (requestETag != null
|
||||
&& (responseETag.equals(requestETag)
|
||||
|| responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", ""))
|
||||
|| "*".equals(requestETag))) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
|
||||
}
|
||||
@@ -163,11 +184,17 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
* Generate the ETag header value from the given response body byte array.
|
||||
* <p>The default implementation generates an MD5 hash.
|
||||
* @param inputStream the response body as an InputStream
|
||||
* @param isWeak whether the generated ETag should be weak
|
||||
* @return the ETag header value
|
||||
* @see org.springframework.util.DigestUtils
|
||||
*/
|
||||
protected String generateETagHeaderValue(InputStream inputStream) throws IOException {
|
||||
StringBuilder builder = new StringBuilder("\"0");
|
||||
protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
|
||||
// length of W/ + 0 + " + 32bits md5 hash + "
|
||||
StringBuilder builder = new StringBuilder(37);
|
||||
if (isWeak) {
|
||||
builder.append("W/");
|
||||
}
|
||||
builder.append("\"0");
|
||||
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
|
||||
builder.append('"');
|
||||
return builder.toString();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,6 +73,26 @@ public class ShallowEtagHeaderFilterTests {
|
||||
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterNoMatchWeakETag() throws Exception {
|
||||
this.filter.setWriteWeakETag(true);
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
final byte[] responseBody = "Hello World".getBytes("UTF-8");
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertEquals("Invalid request passed", request, filterRequest);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertEquals("Invalid status", 200, response.getStatus());
|
||||
assertEquals("Invalid ETag header", "W/\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
|
||||
assertTrue("Invalid Content-Length header", response.getContentLength() > 0);
|
||||
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterMatch() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
@@ -94,6 +114,27 @@ public class ShallowEtagHeaderFilterTests {
|
||||
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterMatchWeakEtag() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
|
||||
request.addHeader("If-None-Match", "W/" + etag);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertEquals("Invalid request passed", request, filterRequest);
|
||||
byte[] responseBody = "Hello World".getBytes("UTF-8");
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
filterResponse.setContentLength(responseBody.length);
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertEquals("Invalid status", 304, response.getStatus());
|
||||
assertEquals("Invalid ETag header", "\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
|
||||
assertFalse("Response has Content-Length header", response.containsHeader("Content-Length"));
|
||||
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWriter() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
|
||||
Reference in New Issue
Block a user