001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.filter; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.PrintWriter; 022import javax.servlet.FilterChain; 023import javax.servlet.ServletException; 024import javax.servlet.ServletOutputStream; 025import javax.servlet.ServletRequest; 026import javax.servlet.http.HttpServletRequest; 027import javax.servlet.http.HttpServletResponse; 028 029import org.springframework.http.HttpMethod; 030import org.springframework.util.Assert; 031import org.springframework.util.ClassUtils; 032import org.springframework.util.DigestUtils; 033import org.springframework.web.util.ContentCachingResponseWrapper; 034import org.springframework.web.util.WebUtils; 035 036/** 037 * {@link javax.servlet.Filter} that generates an {@code ETag} value based on the 038 * content on the response. This ETag is compared to the {@code If-None-Match} 039 * header of the request. If these headers are equal, the response content is 040 * not sent, but rather a {@code 304 "Not Modified"} status instead. 041 * 042 * <p>Since the ETag is based on the response content, the response 043 * (e.g. a {@link org.springframework.web.servlet.View}) is still rendered. 044 * As such, this filter only saves bandwidth, not server performance. 045 * 046 * @author Arjen Poutsma 047 * @author Rossen Stoyanchev 048 * @author Brian Clozel 049 * @author Juergen Hoeller 050 * @since 3.0 051 */ 052public class ShallowEtagHeaderFilter extends OncePerRequestFilter { 053 054 private static final String HEADER_ETAG = "ETag"; 055 056 private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; 057 058 private static final String HEADER_CACHE_CONTROL = "Cache-Control"; 059 060 private static final String DIRECTIVE_NO_STORE = "no-store"; 061 062 private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING"; 063 064 065 /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ 066 private static final boolean servlet3Present = 067 ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class); 068 069 private boolean writeWeakETag = false; 070 071 072 /** 073 * Set whether the ETag value written to the response should be weak, as per RFC 7232. 074 * <p>Should be configured using an {@code <init-param>} for parameter name 075 * "writeWeakETag" in the filter definition in {@code web.xml}. 076 * @since 4.3 077 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">RFC 7232 section 2.3</a> 078 */ 079 public void setWriteWeakETag(boolean writeWeakETag) { 080 this.writeWeakETag = writeWeakETag; 081 } 082 083 /** 084 * Return whether the ETag value written to the response should be weak, as per RFC 7232. 085 * @since 4.3 086 */ 087 public boolean isWriteWeakETag() { 088 return this.writeWeakETag; 089 } 090 091 092 /** 093 * The default value is {@code false} so that the filter may delay the generation 094 * of an ETag until the last asynchronously dispatched thread. 095 */ 096 @Override 097 protected boolean shouldNotFilterAsyncDispatch() { 098 return false; 099 } 100 101 @Override 102 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 103 throws ServletException, IOException { 104 105 HttpServletResponse responseToUse = response; 106 if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) { 107 responseToUse = new HttpStreamingAwareContentCachingResponseWrapper(response, request); 108 } 109 110 filterChain.doFilter(request, responseToUse); 111 112 if (!isAsyncStarted(request) && !isContentCachingDisabled(request)) { 113 updateResponse(request, responseToUse); 114 } 115 } 116 117 private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { 118 ContentCachingResponseWrapper responseWrapper = 119 WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); 120 Assert.notNull(responseWrapper, "ContentCachingResponseWrapper not found"); 121 HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse(); 122 int statusCode = responseWrapper.getStatusCode(); 123 124 if (rawResponse.isCommitted()) { 125 responseWrapper.copyBodyToResponse(); 126 } 127 else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) { 128 String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag); 129 rawResponse.setHeader(HEADER_ETAG, responseETag); 130 String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); 131 if (requestETag != null && ("*".equals(requestETag) || responseETag.equals(requestETag) || 132 responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", "")))) { 133 if (logger.isTraceEnabled()) { 134 logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304"); 135 } 136 rawResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 137 } 138 else { 139 if (logger.isTraceEnabled()) { 140 logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag + 141 "], sending normal response"); 142 } 143 responseWrapper.copyBodyToResponse(); 144 } 145 } 146 else { 147 if (logger.isTraceEnabled()) { 148 logger.trace("Response with status code [" + statusCode + "] not eligible for ETag"); 149 } 150 responseWrapper.copyBodyToResponse(); 151 } 152 } 153 154 /** 155 * Indicates whether the given request and response are eligible for ETag generation. 156 * <p>The default implementation returns {@code true} if all conditions match: 157 * <ul> 158 * <li>response status codes in the {@code 2xx} series</li> 159 * <li>request method is a GET</li> 160 * <li>response Cache-Control header is not set or does not contain a "no-store" directive</li> 161 * </ul> 162 * @param request the HTTP request 163 * @param response the HTTP response 164 * @param responseStatusCode the HTTP response status code 165 * @param inputStream the response body 166 * @return {@code true} if eligible for ETag generation, {@code false} otherwise 167 */ 168 protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, 169 int responseStatusCode, InputStream inputStream) { 170 171 String method = request.getMethod(); 172 if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(method)) { 173 String cacheControl = null; 174 if (servlet3Present) { 175 cacheControl = response.getHeader(HEADER_CACHE_CONTROL); 176 } 177 if (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE)) { 178 return true; 179 } 180 } 181 return false; 182 } 183 184 /** 185 * Generate the ETag header value from the given response body byte array. 186 * <p>The default implementation generates an MD5 hash. 187 * @param inputStream the response body as an InputStream 188 * @param isWeak whether the generated ETag should be weak 189 * @return the ETag header value 190 * @see org.springframework.util.DigestUtils 191 */ 192 protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException { 193 // length of W/ + " + 0 + 32bits md5 hash + " 194 StringBuilder builder = new StringBuilder(37); 195 if (isWeak) { 196 builder.append("W/"); 197 } 198 builder.append("\"0"); 199 DigestUtils.appendMd5DigestAsHex(inputStream, builder); 200 builder.append('"'); 201 return builder.toString(); 202 } 203 204 205 /** 206 * This method can be used to disable the content caching response wrapper 207 * of the ShallowEtagHeaderFilter. This can be done before the start of HTTP 208 * streaming for example where the response will be written to asynchronously 209 * and not in the context of a Servlet container thread. 210 * @since 4.2 211 */ 212 public static void disableContentCaching(ServletRequest request) { 213 Assert.notNull(request, "ServletRequest must not be null"); 214 request.setAttribute(STREAMING_ATTRIBUTE, true); 215 } 216 217 private static boolean isContentCachingDisabled(HttpServletRequest request) { 218 return (request.getAttribute(STREAMING_ATTRIBUTE) != null); 219 } 220 221 222 private static class HttpStreamingAwareContentCachingResponseWrapper extends ContentCachingResponseWrapper { 223 224 private final HttpServletRequest request; 225 226 public HttpStreamingAwareContentCachingResponseWrapper(HttpServletResponse response, HttpServletRequest request) { 227 super(response); 228 this.request = request; 229 } 230 231 @Override 232 public ServletOutputStream getOutputStream() throws IOException { 233 return (useRawResponse() ? getResponse().getOutputStream() : super.getOutputStream()); 234 } 235 236 @Override 237 public PrintWriter getWriter() throws IOException { 238 return (useRawResponse() ? getResponse().getWriter() : super.getWriter()); 239 } 240 241 private boolean useRawResponse() { 242 return isContentCachingDisabled(this.request); 243 } 244 } 245 246}