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.util.Collections; 021import java.util.Enumeration; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Set; 026import java.util.function.Supplier; 027 028import javax.servlet.FilterChain; 029import javax.servlet.ServletException; 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletRequestWrapper; 032import javax.servlet.http.HttpServletResponse; 033import javax.servlet.http.HttpServletResponseWrapper; 034 035import org.springframework.http.HttpRequest; 036import org.springframework.http.HttpStatus; 037import org.springframework.http.server.ServletServerHttpRequest; 038import org.springframework.lang.Nullable; 039import org.springframework.util.CollectionUtils; 040import org.springframework.util.LinkedCaseInsensitiveMap; 041import org.springframework.util.StringUtils; 042import org.springframework.web.util.UriComponents; 043import org.springframework.web.util.UriComponentsBuilder; 044import org.springframework.web.util.UrlPathHelper; 045 046/** 047 * Extract values from "Forwarded" and "X-Forwarded-*" headers, wrap the request 048 * and response, and make they reflect the client-originated protocol and 049 * address in the following methods: 050 * <ul> 051 * <li>{@link HttpServletRequest#getServerName() getServerName()} 052 * <li>{@link HttpServletRequest#getServerPort() getServerPort()} 053 * <li>{@link HttpServletRequest#getScheme() getScheme()} 054 * <li>{@link HttpServletRequest#isSecure() isSecure()} 055 * <li>{@link HttpServletResponse#sendRedirect(String) sendRedirect(String)}. 056 * </ul> 057 * 058 * <p>This filter can also be used in a {@link #setRemoveOnly removeOnly} mode 059 * where "Forwarded" and "X-Forwarded-*" headers are eliminated, and not used. 060 * 061 * @author Rossen Stoyanchev 062 * @author Edd煤 Mel茅ndez 063 * @author Rob Winch 064 * @since 4.3 065 * @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a> 066 */ 067public class ForwardedHeaderFilter extends OncePerRequestFilter { 068 069 private static final Set<String> FORWARDED_HEADER_NAMES = 070 Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(6, Locale.ENGLISH)); 071 072 static { 073 FORWARDED_HEADER_NAMES.add("Forwarded"); 074 FORWARDED_HEADER_NAMES.add("X-Forwarded-Host"); 075 FORWARDED_HEADER_NAMES.add("X-Forwarded-Port"); 076 FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto"); 077 FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); 078 FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl"); 079 } 080 081 082 private boolean removeOnly; 083 084 private boolean relativeRedirects; 085 086 087 /** 088 * Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are 089 * removed only and the information in them ignored. 090 * @param removeOnly whether to discard and ignore forwarded headers 091 * @since 4.3.9 092 */ 093 public void setRemoveOnly(boolean removeOnly) { 094 this.removeOnly = removeOnly; 095 } 096 097 /** 098 * Use this property to enable relative redirects as explained in 099 * {@link RelativeRedirectFilter}, and also using the same response wrapper 100 * as that filter does, or if both are configured, only one will wrap. 101 * <p>By default, if this property is set to false, in which case calls to 102 * {@link HttpServletResponse#sendRedirect(String)} are overridden in order 103 * to turn relative into absolute URLs, also taking into account forwarded 104 * headers. 105 * @param relativeRedirects whether to use relative redirects 106 * @since 4.3.10 107 */ 108 public void setRelativeRedirects(boolean relativeRedirects) { 109 this.relativeRedirects = relativeRedirects; 110 } 111 112 113 @Override 114 protected boolean shouldNotFilter(HttpServletRequest request) { 115 for (String headerName : FORWARDED_HEADER_NAMES) { 116 if (request.getHeader(headerName) != null) { 117 return false; 118 } 119 } 120 return true; 121 } 122 123 @Override 124 protected boolean shouldNotFilterAsyncDispatch() { 125 return false; 126 } 127 128 @Override 129 protected boolean shouldNotFilterErrorDispatch() { 130 return false; 131 } 132 133 @Override 134 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 135 FilterChain filterChain) throws ServletException, IOException { 136 137 if (this.removeOnly) { 138 ForwardedHeaderRemovingRequest wrappedRequest = new ForwardedHeaderRemovingRequest(request); 139 filterChain.doFilter(wrappedRequest, response); 140 } 141 else { 142 HttpServletRequest wrappedRequest = 143 new ForwardedHeaderExtractingRequest(request); 144 145 HttpServletResponse wrappedResponse = this.relativeRedirects ? 146 RelativeRedirectResponseWrapper.wrapIfNecessary(response, HttpStatus.SEE_OTHER) : 147 new ForwardedHeaderExtractingResponse(response, wrappedRequest); 148 149 filterChain.doFilter(wrappedRequest, wrappedResponse); 150 } 151 } 152 153 @Override 154 protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, 155 FilterChain filterChain) throws ServletException, IOException { 156 157 doFilterInternal(request, response, filterChain); 158 } 159 160 /** 161 * Hide "Forwarded" or "X-Forwarded-*" headers. 162 */ 163 private static class ForwardedHeaderRemovingRequest extends HttpServletRequestWrapper { 164 165 private final Map<String, List<String>> headers; 166 167 public ForwardedHeaderRemovingRequest(HttpServletRequest request) { 168 super(request); 169 this.headers = initHeaders(request); 170 } 171 172 private static Map<String, List<String>> initHeaders(HttpServletRequest request) { 173 Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<>(Locale.ENGLISH); 174 Enumeration<String> names = request.getHeaderNames(); 175 while (names.hasMoreElements()) { 176 String name = names.nextElement(); 177 if (!FORWARDED_HEADER_NAMES.contains(name)) { 178 headers.put(name, Collections.list(request.getHeaders(name))); 179 } 180 } 181 return headers; 182 } 183 184 // Override header accessors to not expose forwarded headers 185 186 @Override 187 @Nullable 188 public String getHeader(String name) { 189 List<String> value = this.headers.get(name); 190 return (CollectionUtils.isEmpty(value) ? null : value.get(0)); 191 } 192 193 @Override 194 public Enumeration<String> getHeaders(String name) { 195 List<String> value = this.headers.get(name); 196 return (Collections.enumeration(value != null ? value : Collections.emptySet())); 197 } 198 199 @Override 200 public Enumeration<String> getHeaderNames() { 201 return Collections.enumeration(this.headers.keySet()); 202 } 203 } 204 205 206 /** 207 * Extract and use "Forwarded" or "X-Forwarded-*" headers. 208 */ 209 private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRemovingRequest { 210 211 @Nullable 212 private final String scheme; 213 214 private final boolean secure; 215 216 @Nullable 217 private final String host; 218 219 private final int port; 220 221 private final ForwardedPrefixExtractor forwardedPrefixExtractor; 222 223 224 ForwardedHeaderExtractingRequest(HttpServletRequest request) { 225 super(request); 226 227 HttpRequest httpRequest = new ServletServerHttpRequest(request); 228 UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); 229 int port = uriComponents.getPort(); 230 231 this.scheme = uriComponents.getScheme(); 232 this.secure = "https".equals(this.scheme); 233 this.host = uriComponents.getHost(); 234 this.port = (port == -1 ? (this.secure ? 443 : 80) : port); 235 236 String baseUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port); 237 Supplier<HttpServletRequest> delegateRequest = () -> (HttpServletRequest) getRequest(); 238 this.forwardedPrefixExtractor = new ForwardedPrefixExtractor(delegateRequest, baseUrl); 239 } 240 241 242 @Override 243 @Nullable 244 public String getScheme() { 245 return this.scheme; 246 } 247 248 @Override 249 @Nullable 250 public String getServerName() { 251 return this.host; 252 } 253 254 @Override 255 public int getServerPort() { 256 return this.port; 257 } 258 259 @Override 260 public boolean isSecure() { 261 return this.secure; 262 } 263 264 @Override 265 public String getContextPath() { 266 return this.forwardedPrefixExtractor.getContextPath(); 267 } 268 269 @Override 270 public String getRequestURI() { 271 return this.forwardedPrefixExtractor.getRequestUri(); 272 } 273 274 @Override 275 public StringBuffer getRequestURL() { 276 return this.forwardedPrefixExtractor.getRequestUrl(); 277 } 278 } 279 280 281 /** 282 * Responsible for the contextPath, requestURI, and requestURL with forwarded 283 * headers in mind, and also taking into account changes to the path of the 284 * underlying delegate request (e.g. on a Servlet FORWARD). 285 */ 286 private static class ForwardedPrefixExtractor { 287 288 private final Supplier<HttpServletRequest> delegate; 289 290 private final String baseUrl; 291 292 private String actualRequestUri; 293 294 @Nullable 295 private final String forwardedPrefix; 296 297 @Nullable 298 private String requestUri; 299 300 private String requestUrl; 301 302 303 /** 304 * Constructor with required information. 305 * @param delegateRequest supplier for the current 306 * {@link HttpServletRequestWrapper#getRequest() delegate request} which 307 * may change during a forward (e.g. Tomcat. 308 * @param baseUrl the host, scheme, and port based on forwarded headers 309 */ 310 public ForwardedPrefixExtractor(Supplier<HttpServletRequest> delegateRequest, String baseUrl) { 311 this.delegate = delegateRequest; 312 this.baseUrl = baseUrl; 313 this.actualRequestUri = delegateRequest.get().getRequestURI(); 314 315 this.forwardedPrefix = initForwardedPrefix(delegateRequest.get()); 316 this.requestUri = initRequestUri(); 317 this.requestUrl = initRequestUrl(); // Keep the order: depends on requestUri 318 } 319 320 @Nullable 321 private static String initForwardedPrefix(HttpServletRequest request) { 322 String result = null; 323 Enumeration<String> names = request.getHeaderNames(); 324 while (names.hasMoreElements()) { 325 String name = names.nextElement(); 326 if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) { 327 result = request.getHeader(name); 328 } 329 } 330 if (result != null) { 331 while (result.endsWith("/")) { 332 result = result.substring(0, result.length() - 1); 333 } 334 } 335 return result; 336 } 337 338 @Nullable 339 private String initRequestUri() { 340 if (this.forwardedPrefix != null) { 341 return this.forwardedPrefix + 342 UrlPathHelper.rawPathInstance.getPathWithinApplication(this.delegate.get()); 343 } 344 return null; 345 } 346 347 private String initRequestUrl() { 348 return this.baseUrl + (this.requestUri != null ? this.requestUri : this.delegate.get().getRequestURI()); 349 } 350 351 352 public String getContextPath() { 353 return this.forwardedPrefix == null ? this.delegate.get().getContextPath() : this.forwardedPrefix; 354 } 355 356 public String getRequestUri() { 357 if (this.requestUri == null) { 358 return this.delegate.get().getRequestURI(); 359 } 360 recalculatePathsIfNecessary(); 361 return this.requestUri; 362 } 363 364 public StringBuffer getRequestUrl() { 365 recalculatePathsIfNecessary(); 366 return new StringBuffer(this.requestUrl); 367 } 368 369 private void recalculatePathsIfNecessary() { 370 if (!this.actualRequestUri.equals(this.delegate.get().getRequestURI())) { 371 // Underlying path change (e.g. Servlet FORWARD). 372 this.actualRequestUri = this.delegate.get().getRequestURI(); 373 this.requestUri = initRequestUri(); 374 this.requestUrl = initRequestUrl(); // Keep the order: depends on requestUri 375 } 376 } 377 } 378 379 380 private static class ForwardedHeaderExtractingResponse extends HttpServletResponseWrapper { 381 382 private static final String FOLDER_SEPARATOR = "/"; 383 384 private final HttpServletRequest request; 385 386 387 ForwardedHeaderExtractingResponse(HttpServletResponse response, HttpServletRequest request) { 388 super(response); 389 this.request = request; 390 } 391 392 393 @Override 394 public void sendRedirect(String location) throws IOException { 395 396 UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(location); 397 UriComponents uriComponents = builder.build(); 398 399 // Absolute location 400 if (uriComponents.getScheme() != null) { 401 super.sendRedirect(location); 402 return; 403 } 404 405 // Network-path reference 406 if (location.startsWith("//")) { 407 String scheme = this.request.getScheme(); 408 super.sendRedirect(builder.scheme(scheme).toUriString()); 409 return; 410 } 411 412 String path = uriComponents.getPath(); 413 if (path != null) { 414 // Relative to Servlet container root or to current request 415 path = (path.startsWith(FOLDER_SEPARATOR) ? path : 416 StringUtils.applyRelativePath(this.request.getRequestURI(), path)); 417 } 418 419 String result = UriComponentsBuilder 420 .fromHttpRequest(new ServletServerHttpRequest(this.request)) 421 .replacePath(path) 422 .replaceQuery(uriComponents.getQuery()) 423 .fragment(uriComponents.getFragment()) 424 .build().normalize().toUriString(); 425 426 super.sendRedirect(result); 427 } 428 } 429 430}