001/*
002 * Copyright 2002-2020 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.servlet.support;
018
019import java.util.Enumeration;
020import javax.servlet.http.HttpServletRequest;
021
022import org.springframework.http.HttpRequest;
023import org.springframework.http.server.ServletServerHttpRequest;
024import org.springframework.util.Assert;
025import org.springframework.util.StringUtils;
026import org.springframework.web.context.request.RequestAttributes;
027import org.springframework.web.context.request.RequestContextHolder;
028import org.springframework.web.context.request.ServletRequestAttributes;
029import org.springframework.web.util.UriComponents;
030import org.springframework.web.util.UriComponentsBuilder;
031import org.springframework.web.util.UriUtils;
032import org.springframework.web.util.UrlPathHelper;
033
034/**
035 * UriComponentsBuilder with additional static factory methods to create links
036 * based on the current HttpServletRequest.
037 *
038 * <p><strong>Note:</strong> This class uses values from "Forwarded"
039 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
040 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers,
041 * if present, in order to reflect the client-originated protocol and address.
042 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a
043 * central place whether to extract and use, or to discard such headers.
044 * See the Spring Framework reference for more on this filter.
045 *
046 * @author Rossen Stoyanchev
047 * @since 3.1
048 */
049public class ServletUriComponentsBuilder extends UriComponentsBuilder {
050
051        private String originalPath;
052
053
054        /**
055         * Default constructor. Protected to prevent direct instantiation.
056         * @see #fromContextPath(HttpServletRequest)
057         * @see #fromServletMapping(HttpServletRequest)
058         * @see #fromRequest(HttpServletRequest)
059         * @see #fromCurrentContextPath()
060         * @see #fromCurrentServletMapping()
061         * @see #fromCurrentRequest()
062         */
063        protected ServletUriComponentsBuilder() {
064        }
065
066        /**
067         * Create a deep copy of the given ServletUriComponentsBuilder.
068         * @param other the other builder to copy from
069         */
070        protected ServletUriComponentsBuilder(ServletUriComponentsBuilder other) {
071                super(other);
072                this.originalPath = other.originalPath;
073        }
074
075
076        // Factory methods based on a HttpServletRequest
077
078        /**
079         * Prepare a builder from the host, port, scheme, and context path of the
080         * given HttpServletRequest.
081         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
082         * and "X-Forwarded-*" headers if found. See class-level docs.
083         * <p>As of 4.3.15, this method replaces the contextPath with the value
084         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
085         * {@code ForwardedHeaderFilter}.
086         */
087        public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) {
088                ServletUriComponentsBuilder builder = initFromRequest(request);
089                String forwardedPrefix = getForwardedPrefix(request);
090                builder.replacePath(forwardedPrefix != null ? forwardedPrefix : request.getContextPath());
091                return builder;
092        }
093
094        /**
095         * Prepare a builder from the host, port, scheme, context path, and
096         * servlet mapping of the given HttpServletRequest.
097         * <p>If the servlet is mapped by name, e.g. {@code "/main/*"}, the path
098         * will end with "/main". If the servlet is mapped otherwise, e.g.
099         * {@code "/"} or {@code "*.do"}, the result will be the same as
100         * if calling {@link #fromContextPath(HttpServletRequest)}.
101         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
102         * and "X-Forwarded-*" headers if found. See class-level docs.
103         * <p>As of 4.3.15, this method replaces the contextPath with the value
104         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
105         * {@code ForwardedHeaderFilter}.
106         */
107        public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
108                ServletUriComponentsBuilder builder = fromContextPath(request);
109                if (StringUtils.hasText(UrlPathHelper.defaultInstance.getPathWithinServletMapping(request))) {
110                        builder.path(request.getServletPath());
111                }
112                return builder;
113        }
114
115        /**
116         * Prepare a builder from the host, port, scheme, and path (but not the query)
117         * of the HttpServletRequest.
118         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
119         * and "X-Forwarded-*" headers if found. See class-level docs.
120         * <p>As of 4.3.15, this method replaces the contextPath with the value
121         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
122         * {@code ForwardedHeaderFilter}.
123         */
124        public static ServletUriComponentsBuilder fromRequestUri(HttpServletRequest request) {
125                ServletUriComponentsBuilder builder = initFromRequest(request);
126                builder.initPath(getRequestUriWithForwardedPrefix(request));
127                return builder;
128        }
129
130        /**
131         * Prepare a builder by copying the scheme, host, port, path, and
132         * query string of an HttpServletRequest.
133         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
134         * and "X-Forwarded-*" headers if found. See class-level docs.
135         * <p>As of 4.3.15, this method replaces the contextPath with the value
136         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
137         * {@code ForwardedHeaderFilter}.
138         */
139        public static ServletUriComponentsBuilder fromRequest(HttpServletRequest request) {
140                ServletUriComponentsBuilder builder = initFromRequest(request);
141                builder.initPath(getRequestUriWithForwardedPrefix(request));
142                builder.query(request.getQueryString());
143                return builder;
144        }
145
146        /**
147         * Initialize a builder with a scheme, host,and port (but not path and query).
148         */
149        private static ServletUriComponentsBuilder initFromRequest(HttpServletRequest request) {
150                HttpRequest httpRequest = new ServletServerHttpRequest(request);
151                UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build();
152                String scheme = uriComponents.getScheme();
153                String host = uriComponents.getHost();
154                int port = uriComponents.getPort();
155
156                ServletUriComponentsBuilder builder = new ServletUriComponentsBuilder();
157                builder.scheme(scheme);
158                builder.host(host);
159                if (("http".equals(scheme) && port != 80) || ("https".equals(scheme) && port != 443)) {
160                        builder.port(port);
161                }
162                return builder;
163        }
164
165        private static String getForwardedPrefix(HttpServletRequest request) {
166                String prefix = null;
167                Enumeration<String> names = request.getHeaderNames();
168                while (names.hasMoreElements()) {
169                        String name = names.nextElement();
170                        if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) {
171                                prefix = request.getHeader(name);
172                        }
173                }
174                if (prefix != null) {
175                        while (prefix.endsWith("/")) {
176                                prefix = prefix.substring(0, prefix.length() - 1);
177                        }
178                }
179                return prefix;
180        }
181
182        private static String getRequestUriWithForwardedPrefix(HttpServletRequest request) {
183                String path = request.getRequestURI();
184                String forwardedPrefix = getForwardedPrefix(request);
185                if (forwardedPrefix != null) {
186                        String contextPath = request.getContextPath();
187                        if (!StringUtils.isEmpty(contextPath) && !contextPath.equals("/") && path.startsWith(contextPath)) {
188                                path = path.substring(contextPath.length());
189                        }
190                        path = forwardedPrefix + path;
191                }
192                return path;
193        }
194
195
196        // Alternative methods relying on RequestContextHolder to find the request
197
198        /**
199         * Same as {@link #fromContextPath(HttpServletRequest)} except the
200         * request is obtained through {@link RequestContextHolder}.
201         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
202         * and "X-Forwarded-*" headers if found. See class-level docs.
203         * <p>As of 4.3.15, this method replaces the contextPath with the value
204         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
205         * {@code ForwardedHeaderFilter}.
206         */
207        public static ServletUriComponentsBuilder fromCurrentContextPath() {
208                return fromContextPath(getCurrentRequest());
209        }
210
211        /**
212         * Same as {@link #fromServletMapping(HttpServletRequest)} except the
213         * request is obtained through {@link RequestContextHolder}.
214         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
215         * and "X-Forwarded-*" headers if found. See class-level docs.
216         * <p>As of 4.3.15, this method replaces the contextPath with the value
217         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
218         * {@code ForwardedHeaderFilter}.
219         */
220        public static ServletUriComponentsBuilder fromCurrentServletMapping() {
221                return fromServletMapping(getCurrentRequest());
222        }
223
224        /**
225         * Same as {@link #fromRequestUri(HttpServletRequest)} except the
226         * request is obtained through {@link RequestContextHolder}.
227         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
228         * and "X-Forwarded-*" headers if found. See class-level docs.
229         * <p>As of 4.3.15, this method replaces the contextPath with the value
230         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
231         * {@code ForwardedHeaderFilter}.
232         */
233        public static ServletUriComponentsBuilder fromCurrentRequestUri() {
234                return fromRequestUri(getCurrentRequest());
235        }
236
237        /**
238         * Same as {@link #fromRequest(HttpServletRequest)} except the
239         * request is obtained through {@link RequestContextHolder}.
240         * <p><strong>Note:</strong> This method extracts values from "Forwarded"
241         * and "X-Forwarded-*" headers if found. See class-level docs.
242         * <p>As of 4.3.15, this method replaces the contextPath with the value
243         * of "X-Forwarded-Prefix" rather than prepending, thus aligning with
244         * {@code ForwardedHeaderFilter}.
245         */
246        public static ServletUriComponentsBuilder fromCurrentRequest() {
247                return fromRequest(getCurrentRequest());
248        }
249
250        /**
251         * Obtain current request through {@link RequestContextHolder}.
252         */
253        protected static HttpServletRequest getCurrentRequest() {
254                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
255                Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
256                return ((ServletRequestAttributes) attrs).getRequest();
257        }
258
259
260        private void initPath(String path) {
261                this.originalPath = path;
262                replacePath(path);
263        }
264
265        /**
266         * Remove any path extension from the {@link HttpServletRequest#getRequestURI()
267         * requestURI}. This method must be invoked before any calls to {@link #path(String)}
268         * or {@link #pathSegment(String...)}.
269         * <pre>
270         * GET http://www.foo.com/rest/books/6.json
271         *
272         * ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromRequestUri(this.request);
273         * String ext = builder.removePathExtension();
274         * String uri = builder.path("/pages/1.{ext}").buildAndExpand(ext).toUriString();
275         * assertEquals("http://www.foo.com/rest/books/6/pages/1.json", result);
276         * </pre>
277         * @return the removed path extension for possible re-use, or {@code null}
278         * @since 4.0
279         */
280        public String removePathExtension() {
281                String extension = null;
282                if (this.originalPath != null) {
283                        extension = UriUtils.extractFileExtension(this.originalPath);
284                        if (!StringUtils.isEmpty(extension)) {
285                                int end = this.originalPath.length() - (extension.length() + 1);
286                                replacePath(this.originalPath.substring(0, end));
287                        }
288                        this.originalPath = null;
289                }
290                return extension;
291        }
292
293        @Override
294        public ServletUriComponentsBuilder cloneBuilder() {
295                return new ServletUriComponentsBuilder(this);
296        }
297
298}