diff options
Diffstat (limited to 'spring-web/src/main/java/org/springframework/web/filter')
4 files changed, 371 insertions, 15 deletions
diff --git a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java index 4062bf6d..6a9af746 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java @@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.util.ContentCachingRequestWrapper; @@ -72,6 +73,8 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter private boolean includeClientInfo = false; + private boolean includeHeaders = false; + private boolean includePayload = false; private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH; @@ -120,6 +123,24 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter } /** + * Set whether the request headers should be included in the log message. + * <p>Should be configured using an {@code <init-param>} for parameter name + * "includeHeaders" in the filter definition in {@code web.xml}. + * @since 4.3 + */ + public void setIncludeHeaders(boolean includeHeaders) { + this.includeHeaders = includeHeaders; + } + + /** + * Return whether the request headers should be included in the log message. + * @since 4.3 + */ + public boolean isIncludeHeaders() { + return this.includeHeaders; + } + + /** * Set whether the request payload (body) should be included in the log message. * <p>Should be configured using an {@code <init-param>} for parameter name * "includePayload" in the filter definition in {@code web.xml}. @@ -276,6 +297,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter } } + if (isIncludeHeaders()) { + msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders()); + } + if (isIncludePayload()) { ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); diff --git a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java index 56a7c899..fc564f3f 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java @@ -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. @@ -46,7 +46,9 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { private String encoding; - private boolean forceEncoding = false; + private boolean forceRequestEncoding = false; + + private boolean forceResponseEncoding = false; /** @@ -77,9 +79,26 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { * @see #setForceEncoding */ public CharacterEncodingFilter(String encoding, boolean forceEncoding) { + this(encoding, forceEncoding, forceEncoding); + } + + /** + * Create a {@code CharacterEncodingFilter} for the given encoding. + * @param encoding the encoding to apply + * @param forceRequestEncoding whether the specified encoding is supposed to + * override existing request encodings + * @param forceResponseEncoding whether the specified encoding is supposed to + * override existing response encodings + * @since 4.3 + * @see #setEncoding + * @see #setForceRequestEncoding(boolean) + * @see #setForceResponseEncoding(boolean) + */ + public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) { Assert.hasLength(encoding, "Encoding must not be empty"); this.encoding = encoding; - this.forceEncoding = forceEncoding; + this.forceRequestEncoding = forceRequestEncoding; + this.forceResponseEncoding = forceResponseEncoding; } @@ -95,15 +114,69 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { } /** + * Return the configured encoding for requests and/or responses + * @since 4.3 + */ + public String getEncoding() { + return this.encoding; + } + + /** * Set whether the configured {@link #setEncoding encoding} of this filter * is supposed to override existing request and response encodings. * <p>Default is "false", i.e. do not modify the encoding if * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()} * returns a non-null value. Switch this to "true" to enforce the specified * encoding in any case, applying it as default response encoding as well. + * <p>This is the equivalent to setting both {@link #setForceRequestEncoding(boolean)} + * and {@link #setForceResponseEncoding(boolean)}. + * @see #setForceRequestEncoding(boolean) + * @see #setForceResponseEncoding(boolean) */ public void setForceEncoding(boolean forceEncoding) { - this.forceEncoding = forceEncoding; + this.forceRequestEncoding = forceEncoding; + this.forceResponseEncoding = forceEncoding; + } + + /** + * Set whether the configured {@link #setEncoding encoding} of this filter + * is supposed to override existing request encodings. + * <p>Default is "false", i.e. do not modify the encoding if + * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()} + * returns a non-null value. Switch this to "true" to enforce the specified + * encoding in any case. + * @since 4.3 + */ + public void setForceRequestEncoding(boolean forceRequestEncoding) { + this.forceRequestEncoding = forceRequestEncoding; + } + + /** + * Return whether the encoding should be forced on requests + * @since 4.3 + */ + public boolean isForceRequestEncoding() { + return this.forceRequestEncoding; + } + + /** + * Set whether the configured {@link #setEncoding encoding} of this filter + * is supposed to override existing response encodings. + * <p>Default is "false", i.e. do not modify the encoding. + * Switch this to "true" to enforce the specified encoding + * for responses in any case. + * @since 4.3 + */ + public void setForceResponseEncoding(boolean forceResponseEncoding) { + this.forceResponseEncoding = forceResponseEncoding; + } + + /** + * Return whether the encoding should be forced on responses. + * @since 4.3 + */ + public boolean isForceResponseEncoding() { + return this.forceResponseEncoding; } @@ -112,10 +185,13 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) { - request.setCharacterEncoding(this.encoding); - if (this.forceEncoding) { - response.setCharacterEncoding(this.encoding); + String encoding = getEncoding(); + if (encoding != null) { + if (isForceRequestEncoding() || request.getCharacterEncoding() == null) { + request.setCharacterEncoding(encoding); + } + if (isForceResponseEncoding()) { + response.setCharacterEncoding(encoding); } } filterChain.doFilter(request, response); diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java new file mode 100644 index 00000000..a1196ec2 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -0,0 +1,225 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.filter; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UrlPathHelper; + +/** + * Filter that wraps the request in order to override its + * {@link HttpServletRequest#getServerName() getServerName()}, + * {@link HttpServletRequest#getServerPort() getServerPort()}, + * {@link HttpServletRequest#getScheme() getScheme()}, and + * {@link HttpServletRequest#isSecure() isSecure()} methods with values derived + * from "Forwarded" or "X-Forwarded-*" headers. In effect the wrapped request + * reflects the client-originated protocol and address. + * + * @author Rossen Stoyanchev + * @author EddĂș MelĂ©ndez + * @since 4.3 + */ +public class ForwardedHeaderFilter extends OncePerRequestFilter { + + private static final Set<String> FORWARDED_HEADER_NAMES = + Collections.newSetFromMap(new LinkedCaseInsensitiveMap<Boolean>(5, Locale.ENGLISH)); + + static { + FORWARDED_HEADER_NAMES.add("Forwarded"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Host"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Port"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); + } + + + private final UrlPathHelper pathHelper = new UrlPathHelper(); + + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (FORWARDED_HEADER_NAMES.contains(name)) { + return false; + } + } + return true; + } + + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return false; + } + + @Override + protected boolean shouldNotFilterErrorDispatch() { + return false; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + filterChain.doFilter(new ForwardedHeaderRequestWrapper(request, this.pathHelper), response); + } + + + private static class ForwardedHeaderRequestWrapper extends HttpServletRequestWrapper { + + private final String scheme; + + private final boolean secure; + + private final String host; + + private final int port; + + private final String contextPath; + + private final String requestUri; + + private final StringBuffer requestUrl; + + private final Map<String, List<String>> headers; + + public ForwardedHeaderRequestWrapper(HttpServletRequest request, UrlPathHelper pathHelper) { + super(request); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + int port = uriComponents.getPort(); + + this.scheme = uriComponents.getScheme(); + this.secure = "https".equals(scheme); + this.host = uriComponents.getHost(); + this.port = (port == -1 ? (this.secure ? 443 : 80) : port); + + String prefix = getForwardedPrefix(request); + this.contextPath = (prefix != null ? prefix : request.getContextPath()); + this.requestUri = this.contextPath + pathHelper.getPathWithinApplication(request); + this.requestUrl = new StringBuffer(this.scheme + "://" + this.host + + (port == -1 ? "" : ":" + port) + this.requestUri); + this.headers = initHeaders(request); + } + + private static String getForwardedPrefix(HttpServletRequest request) { + String prefix = null; + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) { + prefix = request.getHeader(name); + } + } + if (prefix != null) { + while (prefix.endsWith("/")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + } + return prefix; + } + + /** + * Copy the headers excluding any {@link #FORWARDED_HEADER_NAMES}. + */ + private static Map<String, List<String>> initHeaders(HttpServletRequest request) { + Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<List<String>>(Locale.ENGLISH); + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (!FORWARDED_HEADER_NAMES.contains(name)) { + headers.put(name, Collections.list(request.getHeaders(name))); + } + } + return headers; + } + + @Override + public String getScheme() { + return this.scheme; + } + + @Override + public String getServerName() { + return this.host; + } + + @Override + public int getServerPort() { + return this.port; + } + + @Override + public boolean isSecure() { + return this.secure; + } + + @Override + public String getContextPath() { + return this.contextPath; + } + + @Override + public String getRequestURI() { + return this.requestUri; + } + + @Override + public StringBuffer getRequestURL() { + return this.requestUrl; + } + + // Override header accessors to not expose forwarded headers + + @Override + public String getHeader(String name) { + List<String> value = this.headers.get(name); + return (CollectionUtils.isEmpty(value) ? null : value.get(0)); + } + + @Override + public Enumeration<String> getHeaders(String name) { + List<String> value = this.headers.get(name); + return (Collections.enumeration(value != null ? value : Collections.<String>emptySet())); + } + + @Override + public Enumeration<String> getHeaderNames() { + return Collections.enumeration(this.headers.keySet()); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index b3fbe838..c94791a5 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -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. @@ -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"); } @@ -144,7 +165,10 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, int responseStatusCode, InputStream inputStream) { - if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) { + String method = request.getMethod(); + if (responseStatusCode >= 200 && responseStatusCode < 300 && + (HttpMethod.GET.matches(method) || HttpMethod.HEAD.matches(method))) { + String cacheControl = null; if (servlet3Present) { cacheControl = response.getHeader(HEADER_CACHE_CONTROL); @@ -160,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(); |