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.servlet.resource;
018
019import java.io.IOException;
020
021import javax.servlet.FilterChain;
022import javax.servlet.ServletException;
023import javax.servlet.ServletRequest;
024import javax.servlet.ServletResponse;
025import javax.servlet.http.HttpServletRequest;
026import javax.servlet.http.HttpServletRequestWrapper;
027import javax.servlet.http.HttpServletResponse;
028import javax.servlet.http.HttpServletResponseWrapper;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032
033import org.springframework.lang.Nullable;
034import org.springframework.util.StringUtils;
035import org.springframework.web.filter.GenericFilterBean;
036import org.springframework.web.util.UrlPathHelper;
037
038/**
039 * A filter that wraps the {@link HttpServletResponse} and overrides its
040 * {@link HttpServletResponse#encodeURL(String) encodeURL} method in order to
041 * translate internal resource request URLs into public URL paths for external use.
042 *
043 * @author Jeremy Grelle
044 * @author Rossen Stoyanchev
045 * @author Sam Brannen
046 * @author Brian Clozel
047 * @since 4.1
048 */
049public class ResourceUrlEncodingFilter extends GenericFilterBean {
050
051        private static final Log logger = LogFactory.getLog(ResourceUrlEncodingFilter.class);
052
053
054        @Override
055        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
056                        throws ServletException, IOException {
057
058                if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
059                        throw new ServletException("ResourceUrlEncodingFilter only supports HTTP requests");
060                }
061                ResourceUrlEncodingRequestWrapper wrappedRequest =
062                                new ResourceUrlEncodingRequestWrapper((HttpServletRequest) request);
063                ResourceUrlEncodingResponseWrapper wrappedResponse =
064                                new ResourceUrlEncodingResponseWrapper(wrappedRequest, (HttpServletResponse) response);
065                filterChain.doFilter(wrappedRequest, wrappedResponse);
066        }
067
068
069        private static class ResourceUrlEncodingRequestWrapper extends HttpServletRequestWrapper {
070
071                @Nullable
072                private ResourceUrlProvider resourceUrlProvider;
073
074                @Nullable
075                private Integer indexLookupPath;
076
077                private String prefixLookupPath = "";
078
079                ResourceUrlEncodingRequestWrapper(HttpServletRequest request) {
080                        super(request);
081                }
082
083                @Override
084                public void setAttribute(String name, Object value) {
085                        super.setAttribute(name, value);
086                        if (ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR.equals(name)) {
087                                if (value instanceof ResourceUrlProvider) {
088                                        initLookupPath((ResourceUrlProvider) value);
089                                }
090                        }
091                }
092
093                private void initLookupPath(ResourceUrlProvider urlProvider) {
094                        this.resourceUrlProvider = urlProvider;
095                        if (this.indexLookupPath == null) {
096                                UrlPathHelper pathHelper = this.resourceUrlProvider.getUrlPathHelper();
097                                String requestUri = pathHelper.getRequestUri(this);
098                                String lookupPath = pathHelper.getLookupPathForRequest(this);
099                                this.indexLookupPath = requestUri.lastIndexOf(lookupPath);
100                                if (this.indexLookupPath == -1) {
101                                        throw new LookupPathIndexException(lookupPath, requestUri);
102                                }
103                                this.prefixLookupPath = requestUri.substring(0, this.indexLookupPath);
104                                if (StringUtils.matchesCharacter(lookupPath, '/') && !StringUtils.matchesCharacter(requestUri, '/')) {
105                                        String contextPath = pathHelper.getContextPath(this);
106                                        if (requestUri.equals(contextPath)) {
107                                                this.indexLookupPath = requestUri.length();
108                                                this.prefixLookupPath = requestUri;
109                                        }
110                                }
111                        }
112                }
113
114                @Nullable
115                public String resolveUrlPath(String url) {
116                        if (this.resourceUrlProvider == null) {
117                                logger.trace("ResourceUrlProvider not available via request attribute " +
118                                                ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR);
119                                return null;
120                        }
121                        if (this.indexLookupPath != null && url.startsWith(this.prefixLookupPath)) {
122                                int suffixIndex = getEndPathIndex(url);
123                                String suffix = url.substring(suffixIndex);
124                                String lookupPath = url.substring(this.indexLookupPath, suffixIndex);
125                                lookupPath = this.resourceUrlProvider.getForLookupPath(lookupPath);
126                                if (lookupPath != null) {
127                                        return this.prefixLookupPath + lookupPath + suffix;
128                                }
129                        }
130                        return null;
131                }
132
133                private int getEndPathIndex(String path) {
134                        int end = path.indexOf('?');
135                        int fragmentIndex = path.indexOf('#');
136                        if (fragmentIndex != -1 && (end == -1 || fragmentIndex < end)) {
137                                end = fragmentIndex;
138                        }
139                        if (end == -1) {
140                                end = path.length();
141                        }
142                        return end;
143                }
144        }
145
146
147        private static class ResourceUrlEncodingResponseWrapper extends HttpServletResponseWrapper {
148
149                private final ResourceUrlEncodingRequestWrapper request;
150
151                ResourceUrlEncodingResponseWrapper(ResourceUrlEncodingRequestWrapper request, HttpServletResponse wrapped) {
152                        super(wrapped);
153                        this.request = request;
154                }
155
156                @Override
157                public String encodeURL(String url) {
158                        String urlPath = this.request.resolveUrlPath(url);
159                        if (urlPath != null) {
160                                return super.encodeURL(urlPath);
161                        }
162                        return super.encodeURL(url);
163                }
164        }
165
166
167        /**
168         * Runtime exception to get far enough (to ResourceUrlProviderExposingInterceptor)
169         * where it can be re-thrown as ServletRequestBindingException to result in
170         * a 400 response.
171         */
172        @SuppressWarnings("serial")
173        static class LookupPathIndexException extends IllegalArgumentException {
174
175                LookupPathIndexException(String lookupPath, String requestUri) {
176                        super("Failed to find lookupPath '" + lookupPath + "' within requestUri '" + requestUri + "'. " +
177                                        "This could be because the path has invalid encoded characters or isn't normalized.");
178                }
179        }
180
181}