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}