001/*
002 * Copyright 2002-2019 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.server.adapter;
018
019import java.net.URI;
020import java.util.Collections;
021import java.util.Locale;
022import java.util.Set;
023import java.util.function.Function;
024
025import org.springframework.context.ApplicationContext;
026import org.springframework.http.HttpHeaders;
027import org.springframework.http.server.reactive.ServerHttpRequest;
028import org.springframework.lang.Nullable;
029import org.springframework.util.LinkedCaseInsensitiveMap;
030import org.springframework.web.util.UriComponentsBuilder;
031
032/**
033 * Extract values from "Forwarded" and "X-Forwarded-*" headers to override
034 * the request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects
035 * the client-originated protocol and address.
036 *
037 * <p>Alternatively if {@link #setRemoveOnly removeOnly} is set to "true",
038 * then "Forwarded" and "X-Forwarded-*" headers are only removed, and not used.
039 *
040 * <p>An instance of this class is typically declared as a bean with the name
041 * "forwardedHeaderTransformer" and detected by
042 * {@link WebHttpHandlerBuilder#applicationContext(ApplicationContext)}, or it
043 * can also be registered directly via
044 * {@link WebHttpHandlerBuilder#forwardedHeaderTransformer(ForwardedHeaderTransformer)}.
045 *
046 * @author Rossen Stoyanchev
047 * @since 5.1
048 * @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
049 */
050public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> {
051
052        static final Set<String> FORWARDED_HEADER_NAMES =
053                        Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
054
055        static {
056                FORWARDED_HEADER_NAMES.add("Forwarded");
057                FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
058                FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
059                FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
060                FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
061                FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
062        }
063
064
065        private boolean removeOnly;
066
067
068        /**
069         * Enable mode in which any "Forwarded" or "X-Forwarded-*" headers are
070         * removed only and the information in them ignored.
071         * @param removeOnly whether to discard and ignore forwarded headers
072         */
073        public void setRemoveOnly(boolean removeOnly) {
074                this.removeOnly = removeOnly;
075        }
076
077        /**
078         * Whether the "remove only" mode is on.
079         * @see #setRemoveOnly
080         */
081        public boolean isRemoveOnly() {
082                return this.removeOnly;
083        }
084
085
086        /**
087         * Apply and remove, or remove Forwarded type headers.
088         * @param request the request
089         */
090        @Override
091        public ServerHttpRequest apply(ServerHttpRequest request) {
092                if (hasForwardedHeaders(request)) {
093                        ServerHttpRequest.Builder builder = request.mutate();
094                        if (!this.removeOnly) {
095                                URI uri = UriComponentsBuilder.fromHttpRequest(request).build(true).toUri();
096                                builder.uri(uri);
097                                String prefix = getForwardedPrefix(request);
098                                if (prefix != null) {
099                                        builder.path(prefix + uri.getRawPath());
100                                        builder.contextPath(prefix);
101                                }
102                        }
103                        removeForwardedHeaders(builder);
104                        request = builder.build();
105                }
106                return request;
107        }
108
109        /**
110         * Whether the request has any Forwarded headers.
111         * @param request the request
112         */
113        protected boolean hasForwardedHeaders(ServerHttpRequest request) {
114                HttpHeaders headers = request.getHeaders();
115                for (String headerName : FORWARDED_HEADER_NAMES) {
116                        if (headers.containsKey(headerName)) {
117                                return true;
118                        }
119                }
120                return false;
121        }
122
123        private void removeForwardedHeaders(ServerHttpRequest.Builder builder) {
124                builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove));
125        }
126
127
128        @Nullable
129        private static String getForwardedPrefix(ServerHttpRequest request) {
130                HttpHeaders headers = request.getHeaders();
131                String prefix = headers.getFirst("X-Forwarded-Prefix");
132                if (prefix != null) {
133                        int endIndex = prefix.length();
134                        while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
135                                endIndex--;
136                        }
137                        prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
138                }
139                return prefix;
140        }
141
142}