View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.resource;
18  
19  import java.io.IOException;
20  import java.net.URLDecoder;
21  import java.util.Arrays;
22  import java.util.List;
23  import javax.servlet.http.HttpServletRequest;
24  
25  import org.springframework.core.io.ClassPathResource;
26  import org.springframework.core.io.Resource;
27  import org.springframework.core.io.UrlResource;
28  import org.springframework.util.StringUtils;
29  import org.springframework.web.context.support.ServletContextResource;
30  
31  /**
32   * A simple {@code ResourceResolver} that tries to find a resource under the given
33   * locations matching to the request path.
34   *
35   * <p>This resolver does not delegate to the {@code ResourceResolverChain} and is
36   * expected to be configured at the end in a chain of resolvers.
37   *
38   * @author Jeremy Grelle
39   * @author Rossen Stoyanchev
40   * @author Sam Brannen
41   * @since 4.1
42   */
43  public class PathResourceResolver extends AbstractResourceResolver {
44  
45  	private Resource[] allowedLocations;
46  
47  
48  	/**
49  	 * By default when a Resource is found, the path of the resolved resource is
50  	 * compared to ensure it's under the input location where it was found.
51  	 * However sometimes that may not be the case, e.g. when
52  	 * {@link org.springframework.web.servlet.resource.CssLinkResourceTransformer}
53  	 * resolves public URLs of links it contains, the CSS file is the location
54  	 * and the resources being resolved are css files, images, fonts and others
55  	 * located in adjacent or parent directories.
56  	 * <p>This property allows configuring a complete list of locations under
57  	 * which resources must be so that if a resource is not under the location
58  	 * relative to which it was found, this list may be checked as well.
59  	 * <p>By default {@link ResourceHttpRequestHandler} initializes this property
60  	 * to match its list of locations.
61  	 * @param locations the list of allowed locations
62  	 * @since 4.1.2
63  	 * @see ResourceHttpRequestHandler#initAllowedLocations()
64  	 */
65  	public void setAllowedLocations(Resource... locations) {
66  		this.allowedLocations = locations;
67  	}
68  
69  	public Resource[] getAllowedLocations() {
70  		return this.allowedLocations;
71  	}
72  
73  
74  	@Override
75  	protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
76  			List<? extends Resource> locations, ResourceResolverChain chain) {
77  
78  		return getResource(requestPath, locations);
79  	}
80  
81  	@Override
82  	protected String resolveUrlPathInternal(String resourcePath, List<? extends Resource> locations,
83  			ResourceResolverChain chain) {
84  
85  		return (getResource(resourcePath, locations) != null ? resourcePath : null);
86  	}
87  
88  	private Resource getResource(String resourcePath, List<? extends Resource> locations) {
89  		for (Resource location : locations) {
90  			try {
91  				if (logger.isTraceEnabled()) {
92  					logger.trace("Checking location: " + location);
93  				}
94  				Resource resource = getResource(resourcePath, location);
95  				if (resource != null) {
96  					if (logger.isTraceEnabled()) {
97  						logger.trace("Found match: " + resource);
98  					}
99  					return resource;
100 				}
101 				else if (logger.isTraceEnabled()) {
102 					logger.trace("No match for location: " + location);
103 				}
104 			}
105 			catch (IOException ex) {
106 				logger.trace("Failure checking for relative resource - trying next location", ex);
107 			}
108 		}
109 		return null;
110 	}
111 
112 	/**
113 	 * Find the resource under the given location.
114 	 * <p>The default implementation checks if there is a readable
115 	 * {@code Resource} for the given path relative to the location.
116 	 * @param resourcePath the path to the resource
117 	 * @param location the location to check
118 	 * @return the resource, or {@code null} if none found
119 	 */
120 	protected Resource getResource(String resourcePath, Resource location) throws IOException {
121 		Resource resource = location.createRelative(resourcePath);
122 		if (resource.exists() && resource.isReadable()) {
123 			if (checkResource(resource, location)) {
124 				return resource;
125 			}
126 			else if (logger.isTraceEnabled()) {
127 				logger.trace("Resource path=\"" + resourcePath + "\" was successfully resolved " +
128 						"but resource=\"" +	resource.getURL() + "\" is neither under the " +
129 						"current location=\"" + location.getURL() + "\" nor under any of the " +
130 						"allowed locations=" + Arrays.asList(getAllowedLocations()));
131 			}
132 		}
133 		return null;
134 	}
135 
136 	/**
137 	 * Perform additional checks on a resolved resource beyond checking whether the
138 	 * resources exists and is readable. The default implementation also verifies
139 	 * the resource is either under the location relative to which it was found or
140 	 * is under one of the {@link #setAllowedLocations allowed locations}.
141 	 * @param resource the resource to check
142 	 * @param location the location relative to which the resource was found
143 	 * @return "true" if resource is in a valid location, "false" otherwise.
144 	 * @since 4.1.2
145 	 */
146 	protected boolean checkResource(Resource resource, Resource location) throws IOException {
147 		if (isResourceUnderLocation(resource, location)) {
148 			return true;
149 		}
150 		if (getAllowedLocations() != null) {
151 			for (Resource current : getAllowedLocations()) {
152 				if (isResourceUnderLocation(resource, current)) {
153 					return true;
154 				}
155 			}
156 		}
157 		return false;
158 	}
159 
160 	private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
161 		if (!resource.getClass().equals(location.getClass())) {
162 			return false;
163 		}
164 		String resourcePath;
165 		String locationPath;
166 		if (resource instanceof UrlResource) {
167 			resourcePath = resource.getURL().toExternalForm();
168 			locationPath = StringUtils.cleanPath(location.getURL().toString());
169 		}
170 		else if (resource instanceof ClassPathResource) {
171 			resourcePath = ((ClassPathResource) resource).getPath();
172 			locationPath = StringUtils.cleanPath(((ClassPathResource) location).getPath());
173 		}
174 		else if (resource instanceof ServletContextResource) {
175 			resourcePath = ((ServletContextResource) resource).getPath();
176 			locationPath = StringUtils.cleanPath(((ServletContextResource) location).getPath());
177 		}
178 		else {
179 			resourcePath = resource.getURL().getPath();
180 			locationPath = StringUtils.cleanPath(location.getURL().getPath());
181 		}
182 		if(locationPath.equals(resourcePath)) {
183 			return true;
184 		}
185 		locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
186 		if (!resourcePath.startsWith(locationPath)) {
187 			return false;
188 		}
189 		if (resourcePath.contains("%")) {
190 			// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
191 			if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) {
192 				if (logger.isTraceEnabled()) {
193 					logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath);
194 				}
195 				return false;
196 			}
197 		}
198 		return true;
199 	}
200 
201 }