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.servlet.resource;
018
019import java.io.IOException;
020import java.io.UnsupportedEncodingException;
021import java.net.URLDecoder;
022import java.nio.charset.Charset;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import javax.servlet.ServletException;
028import javax.servlet.ServletResponse;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034
035import org.springframework.beans.factory.InitializingBean;
036import org.springframework.context.ApplicationContext;
037import org.springframework.context.EmbeddedValueResolverAware;
038import org.springframework.core.io.Resource;
039import org.springframework.core.io.UrlResource;
040import org.springframework.core.io.support.ResourceRegion;
041import org.springframework.http.HttpHeaders;
042import org.springframework.http.HttpMethod;
043import org.springframework.http.HttpRange;
044import org.springframework.http.MediaType;
045import org.springframework.http.converter.ResourceHttpMessageConverter;
046import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
047import org.springframework.http.server.ServletServerHttpRequest;
048import org.springframework.http.server.ServletServerHttpResponse;
049import org.springframework.util.Assert;
050import org.springframework.util.ClassUtils;
051import org.springframework.util.CollectionUtils;
052import org.springframework.util.ObjectUtils;
053import org.springframework.util.ResourceUtils;
054import org.springframework.util.StringUtils;
055import org.springframework.util.StringValueResolver;
056import org.springframework.web.HttpRequestHandler;
057import org.springframework.web.accept.ContentNegotiationManager;
058import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
059import org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy;
060import org.springframework.web.context.request.ServletWebRequest;
061import org.springframework.web.cors.CorsConfiguration;
062import org.springframework.web.cors.CorsConfigurationSource;
063import org.springframework.web.servlet.HandlerMapping;
064import org.springframework.web.servlet.support.WebContentGenerator;
065import org.springframework.web.util.UrlPathHelper;
066
067/**
068 * {@code HttpRequestHandler} that serves static resources in an optimized way
069 * according to the guidelines of Page Speed, YSlow, etc.
070 *
071 * <p>The {@linkplain #setLocations "locations"} property takes a list of Spring
072 * {@link Resource} locations from which static resources are allowed to be served
073 * by this handler. Resources could be served from a classpath location, e.g.
074 * "classpath:/META-INF/public-web-resources/", allowing convenient packaging
075 * and serving of resources such as .js, .css, and others in jar files.
076 *
077 * <p>This request handler may also be configured with a
078 * {@link #setResourceResolvers(List) resourcesResolver} and
079 * {@link #setResourceTransformers(List) resourceTransformer} chains to support
080 * arbitrary resolution and transformation of resources being served. By default
081 * a {@link PathResourceResolver} simply finds resources based on the configured
082 * "locations". An application can configure additional resolvers and transformers
083 * such as the {@link VersionResourceResolver} which can resolve and prepare URLs
084 * for resources with a version in the URL.
085 *
086 * <p>This handler also properly evaluates the {@code Last-Modified} header
087 * (if present) so that a {@code 304} status code will be returned as appropriate,
088 * avoiding unnecessary overhead for resources that are already cached by the client.
089 *
090 * @author Keith Donald
091 * @author Jeremy Grelle
092 * @author Juergen Hoeller
093 * @author Arjen Poutsma
094 * @author Brian Clozel
095 * @author Rossen Stoyanchev
096 * @since 3.0.4
097 */
098public class ResourceHttpRequestHandler extends WebContentGenerator
099                implements HttpRequestHandler, EmbeddedValueResolverAware, InitializingBean, CorsConfigurationSource {
100
101        // Servlet 3.1 setContentLengthLong(long) available?
102        private static final boolean contentLengthLongAvailable =
103                        ClassUtils.hasMethod(ServletResponse.class, "setContentLengthLong", long.class);
104
105        private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);
106
107        private static final String URL_RESOURCE_CHARSET_PREFIX = "[charset=";
108
109
110        private final List<String> locationValues = new ArrayList<String>(4);
111
112        private final List<Resource> locations = new ArrayList<Resource>(4);
113
114        private final Map<Resource, Charset> locationCharsets = new HashMap<Resource, Charset>(4);
115
116        private final List<ResourceResolver> resourceResolvers = new ArrayList<ResourceResolver>(4);
117
118        private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>(4);
119
120        private ResourceHttpMessageConverter resourceHttpMessageConverter;
121
122        private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter;
123
124        private ContentNegotiationManager contentNegotiationManager;
125
126        private PathExtensionContentNegotiationStrategy contentNegotiationStrategy;
127
128        private CorsConfiguration corsConfiguration;
129
130        private UrlPathHelper urlPathHelper;
131
132        private StringValueResolver embeddedValueResolver;
133
134
135        public ResourceHttpRequestHandler() {
136                super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
137        }
138
139
140        /**
141         * An alternative to {@link #setLocations(List)} that accepts a list of
142         * String-based location values, with support for {@link UrlResource}'s
143         * (e.g. files or HTTP URLs) with a special prefix to indicate the charset
144         * to use when appending relative paths. For example
145         * {@code "[charset=Windows-31J]https://example.org/path"}.
146         * @since 4.3.13
147         */
148        public void setLocationValues(List<String> locationValues) {
149                Assert.notNull(locationValues, "Location values list must not be null");
150                this.locationValues.clear();
151                this.locationValues.addAll(locationValues);
152        }
153
154        /**
155         * Set the {@code List} of {@code Resource} locations to use as sources
156         * for serving static resources.
157         * @see #setLocationValues(List)
158         */
159        public void setLocations(List<Resource> locations) {
160                Assert.notNull(locations, "Locations list must not be null");
161                this.locations.clear();
162                this.locations.addAll(locations);
163        }
164
165        /**
166         * Return the configured {@code List} of {@code Resource} locations.
167         * <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
168         * instead of loaded Resource-based locations, this method will return
169         * empty until after initialization via {@link #afterPropertiesSet()}.
170         * @see #setLocationValues
171         * @see #setLocations
172         */
173        public List<Resource> getLocations() {
174                return this.locations;
175        }
176
177        /**
178         * Configure the list of {@link ResourceResolver}s to use.
179         * <p>By default {@link PathResourceResolver} is configured. If using this property,
180         * it is recommended to add {@link PathResourceResolver} as the last resolver.
181         */
182        public void setResourceResolvers(List<ResourceResolver> resourceResolvers) {
183                this.resourceResolvers.clear();
184                if (resourceResolvers != null) {
185                        this.resourceResolvers.addAll(resourceResolvers);
186                }
187        }
188
189        /**
190         * Return the list of configured resource resolvers.
191         */
192        public List<ResourceResolver> getResourceResolvers() {
193                return this.resourceResolvers;
194        }
195
196        /**
197         * Configure the list of {@link ResourceTransformer}s to use.
198         * <p>By default no transformers are configured for use.
199         */
200        public void setResourceTransformers(List<ResourceTransformer> resourceTransformers) {
201                this.resourceTransformers.clear();
202                if (resourceTransformers != null) {
203                        this.resourceTransformers.addAll(resourceTransformers);
204                }
205        }
206
207        /**
208         * Return the list of configured resource transformers.
209         */
210        public List<ResourceTransformer> getResourceTransformers() {
211                return this.resourceTransformers;
212        }
213
214        /**
215         * Configure the {@link ResourceHttpMessageConverter} to use.
216         * <p>By default a {@link ResourceHttpMessageConverter} will be configured.
217         * @since 4.3
218         */
219        public void setResourceHttpMessageConverter(ResourceHttpMessageConverter messageConverter) {
220                this.resourceHttpMessageConverter = messageConverter;
221        }
222
223        /**
224         * Return the configured resource converter.
225         * @since 4.3
226         */
227        public ResourceHttpMessageConverter getResourceHttpMessageConverter() {
228                return this.resourceHttpMessageConverter;
229        }
230
231        /**
232         * Configure the {@link ResourceRegionHttpMessageConverter} to use.
233         * <p>By default a {@link ResourceRegionHttpMessageConverter} will be configured.
234         * @since 4.3
235         */
236        public void setResourceRegionHttpMessageConverter(ResourceRegionHttpMessageConverter messageConverter) {
237                this.resourceRegionHttpMessageConverter = messageConverter;
238        }
239
240        /**
241         * Return the configured resource region converter.
242         * @since 4.3
243         */
244        public ResourceRegionHttpMessageConverter getResourceRegionHttpMessageConverter() {
245                return this.resourceRegionHttpMessageConverter;
246        }
247
248        /**
249         * Configure a {@code ContentNegotiationManager} to help determine the
250         * media types for resources being served. If the manager contains a path
251         * extension strategy it will be checked for registered file extension.
252         * @since 4.3
253         */
254        public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
255                this.contentNegotiationManager = contentNegotiationManager;
256        }
257
258        /**
259         * Return the configured content negotiation manager.
260         * @since 4.3
261         */
262        public ContentNegotiationManager getContentNegotiationManager() {
263                return this.contentNegotiationManager;
264        }
265
266        /**
267         * Specify the CORS configuration for resources served by this handler.
268         * <p>By default this is not set in which allows cross-origin requests.
269         */
270        public void setCorsConfiguration(CorsConfiguration corsConfiguration) {
271                this.corsConfiguration = corsConfiguration;
272        }
273
274        /**
275         * Return the specified CORS configuration.
276         */
277        @Override
278        public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
279                return this.corsConfiguration;
280        }
281
282        /**
283         * Provide a reference to the {@link UrlPathHelper} used to map requests to
284         * static resources. This helps to derive information about the lookup path
285         * such as whether it is decoded or not.
286         * @since 4.3.13
287         */
288        public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
289                this.urlPathHelper = urlPathHelper;
290        }
291
292        /**
293         * The configured {@link UrlPathHelper}.
294         * @since 4.3.13
295         */
296        public UrlPathHelper getUrlPathHelper() {
297                return this.urlPathHelper;
298        }
299
300        @Override
301        public void setEmbeddedValueResolver(StringValueResolver resolver) {
302                this.embeddedValueResolver = resolver;
303        }
304
305
306        @Override
307        public void afterPropertiesSet() throws Exception {
308                resolveResourceLocations();
309
310                if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
311                        logger.warn("Locations list is empty. No resources will be served unless a " +
312                                        "custom ResourceResolver is configured as an alternative to PathResourceResolver.");
313                }
314
315                if (this.resourceResolvers.isEmpty()) {
316                        this.resourceResolvers.add(new PathResourceResolver());
317                }
318
319                initAllowedLocations();
320
321                if (this.resourceHttpMessageConverter == null) {
322                        this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
323                }
324                if (this.resourceRegionHttpMessageConverter == null) {
325                        this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
326                }
327
328                this.contentNegotiationStrategy = initContentNegotiationStrategy();
329        }
330
331        private void resolveResourceLocations() {
332                if (CollectionUtils.isEmpty(this.locationValues)) {
333                        return;
334                }
335                else if (!CollectionUtils.isEmpty(this.locations)) {
336                        throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " +
337                                        "String-based \"locationValues\", but not both.");
338                }
339
340                ApplicationContext applicationContext = getApplicationContext();
341                for (String location : this.locationValues) {
342                        if (this.embeddedValueResolver != null) {
343                                String resolvedLocation = this.embeddedValueResolver.resolveStringValue(location);
344                                if (resolvedLocation == null) {
345                                        throw new IllegalArgumentException("Location resolved to null: " + location);
346                                }
347                                location = resolvedLocation;
348                        }
349                        Charset charset = null;
350                        location = location.trim();
351                        if (location.startsWith(URL_RESOURCE_CHARSET_PREFIX)) {
352                                int endIndex = location.indexOf(']', URL_RESOURCE_CHARSET_PREFIX.length());
353                                if (endIndex == -1) {
354                                        throw new IllegalArgumentException("Invalid charset syntax in location: " + location);
355                                }
356                                String value = location.substring(URL_RESOURCE_CHARSET_PREFIX.length(), endIndex);
357                                charset = Charset.forName(value);
358                                location = location.substring(endIndex + 1);
359                        }
360                        Resource resource = applicationContext.getResource(location);
361                        this.locations.add(resource);
362                        if (charset != null) {
363                                if (!(resource instanceof UrlResource)) {
364                                        throw new IllegalArgumentException("Unexpected charset for non-UrlResource: " + resource);
365                                }
366                                this.locationCharsets.put(resource, charset);
367                        }
368                }
369        }
370
371        /**
372         * Look for a {@code PathResourceResolver} among the configured resource
373         * resolvers and set its {@code allowedLocations} property (if empty) to
374         * match the {@link #setLocations locations} configured on this class.
375         */
376        protected void initAllowedLocations() {
377                if (CollectionUtils.isEmpty(this.locations)) {
378                        return;
379                }
380                for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
381                        if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
382                                PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i);
383                                if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) {
384                                        pathResolver.setAllowedLocations(getLocations().toArray(new Resource[getLocations().size()]));
385                                }
386                                if (this.urlPathHelper != null) {
387                                        pathResolver.setLocationCharsets(this.locationCharsets);
388                                        pathResolver.setUrlPathHelper(this.urlPathHelper);
389                                }
390                                break;
391                        }
392                }
393        }
394
395        /**
396         * Initialize the content negotiation strategy depending on the {@code ContentNegotiationManager}
397         * setup and the availability of a {@code ServletContext}.
398         * @see ServletPathExtensionContentNegotiationStrategy
399         * @see PathExtensionContentNegotiationStrategy
400         */
401        protected PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() {
402                Map<String, MediaType> mediaTypes = null;
403                if (getContentNegotiationManager() != null) {
404                        PathExtensionContentNegotiationStrategy strategy =
405                                        getContentNegotiationManager().getStrategy(PathExtensionContentNegotiationStrategy.class);
406                        if (strategy != null) {
407                                mediaTypes = new HashMap<String, MediaType>(strategy.getMediaTypes());
408                        }
409                }
410                return (getServletContext() != null ?
411                                new ServletPathExtensionContentNegotiationStrategy(getServletContext(), mediaTypes) :
412                                new PathExtensionContentNegotiationStrategy(mediaTypes));
413        }
414
415
416        /**
417         * Processes a resource request.
418         * <p>Checks for the existence of the requested resource in the configured list of locations.
419         * If the resource does not exist, a {@code 404} response will be returned to the client.
420         * If the resource exists, the request will be checked for the presence of the
421         * {@code Last-Modified} header, and its value will be compared against the last-modified
422         * timestamp of the given resource, returning a {@code 304} status code if the
423         * {@code Last-Modified} value  is greater. If the resource is newer than the
424         * {@code Last-Modified} value, or the header is not present, the content resource
425         * of the resource will be written to the response with caching headers
426         * set to expire one year in the future.
427         */
428        @Override
429        public void handleRequest(HttpServletRequest request, HttpServletResponse response)
430                        throws ServletException, IOException {
431
432                // For very general mappings (e.g. "/") we need to check 404 first
433                Resource resource = getResource(request);
434                if (resource == null) {
435                        logger.trace("No matching resource found - returning 404");
436                        response.sendError(HttpServletResponse.SC_NOT_FOUND);
437                        return;
438                }
439
440                if (HttpMethod.OPTIONS.matches(request.getMethod())) {
441                        response.setHeader("Allow", getAllowHeader());
442                        return;
443                }
444
445                // Supported methods and required session
446                checkRequest(request);
447
448                // Header phase
449                if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
450                        logger.trace("Resource not modified - returning 304");
451                        return;
452                }
453
454                // Apply cache settings, if any
455                prepareResponse(response);
456
457                // Check the media type for the resource
458                MediaType mediaType = getMediaType(request, resource);
459                if (mediaType != null) {
460                        if (logger.isTraceEnabled()) {
461                                logger.trace("Determined media type '" + mediaType + "' for " + resource);
462                        }
463                }
464                else {
465                        if (logger.isTraceEnabled()) {
466                                logger.trace("No media type found for " + resource + " - not sending a content-type header");
467                        }
468                }
469
470                // Content phase
471                if (METHOD_HEAD.equals(request.getMethod())) {
472                        setHeaders(response, resource, mediaType);
473                        logger.trace("HEAD request - skipping content");
474                        return;
475                }
476
477                ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
478                if (request.getHeader(HttpHeaders.RANGE) == null) {
479                        setHeaders(response, resource, mediaType);
480                        this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
481                }
482                else {
483                        response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
484                        ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
485                        try {
486                                List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
487                                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
488                                if (httpRanges.size() == 1) {
489                                        ResourceRegion resourceRegion = httpRanges.get(0).toResourceRegion(resource);
490                                        this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage);
491                                }
492                                else {
493                                        this.resourceRegionHttpMessageConverter.write(
494                                                        HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
495                                }
496                        }
497                        catch (IllegalArgumentException ex) {
498                                response.setHeader("Content-Range", "bytes */" + resource.contentLength());
499                                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
500                        }
501                }
502        }
503
504        protected Resource getResource(HttpServletRequest request) throws IOException {
505                String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
506                if (path == null) {
507                        throw new IllegalStateException("Required request attribute '" +
508                                        HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
509                }
510
511                path = processPath(path);
512                if (!StringUtils.hasText(path) || isInvalidPath(path)) {
513                        if (logger.isTraceEnabled()) {
514                                logger.trace("Ignoring invalid resource path [" + path + "]");
515                        }
516                        return null;
517                }
518                if (isInvalidEncodedPath(path)) {
519                        if (logger.isTraceEnabled()) {
520                                logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]");
521                        }
522                        return null;
523                }
524
525                ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers());
526                Resource resource = resolveChain.resolveResource(request, path, getLocations());
527                if (resource == null || getResourceTransformers().isEmpty()) {
528                        return resource;
529                }
530
531                ResourceTransformerChain transformChain =
532                                new DefaultResourceTransformerChain(resolveChain, getResourceTransformers());
533                resource = transformChain.transform(request, resource);
534                return resource;
535        }
536
537        /**
538         * Process the given resource path.
539         * <p>The default implementation replaces:
540         * <ul>
541         * <li>Backslash with forward slash.
542         * <li>Duplicate occurrences of slash with a single slash.
543         * <li>Any combination of leading slash and control characters (00-1F and 7F)
544         * with a single "/" or "". For example {@code "  / // foo/bar"}
545         * becomes {@code "/foo/bar"}.
546         * </ul>
547         * @since 3.2.12
548         */
549        protected String processPath(String path) {
550                path = StringUtils.replace(path, "\\", "/");
551                path = cleanDuplicateSlashes(path);
552                return cleanLeadingSlash(path);
553        }
554
555        private String cleanDuplicateSlashes(String path) {
556                StringBuilder sb = null;
557                char prev = 0;
558                for (int i = 0; i < path.length(); i++) {
559                        char curr = path.charAt(i);
560                        try {
561                                if ((curr == '/') && (prev == '/')) {
562                                        if (sb == null) {
563                                                sb = new StringBuilder(path.substring(0, i));
564                                        }
565                                        continue;
566                                }
567                                if (sb != null) {
568                                        sb.append(path.charAt(i));
569                                }
570                        }
571                        finally {
572                                prev = curr;
573                        }
574                }
575                return sb != null ? sb.toString() : path;
576        }
577
578        private String cleanLeadingSlash(String path) {
579                boolean slash = false;
580                for (int i = 0; i < path.length(); i++) {
581                        if (path.charAt(i) == '/') {
582                                slash = true;
583                        }
584                        else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
585                                if (i == 0 || (i == 1 && slash)) {
586                                        return path;
587                                }
588                                path = (slash ? "/" + path.substring(i) : path.substring(i));
589                                if (logger.isTraceEnabled()) {
590                                        logger.trace("Path after trimming leading '/' and control characters: [" + path + "]");
591                                }
592                                return path;
593                        }
594                }
595                return (slash ? "/" : "");
596        }
597
598        /**
599         * Check whether the given path contains invalid escape sequences.
600         * @param path the path to validate
601         * @return {@code true} if the path is invalid, {@code false} otherwise
602         */
603        private boolean isInvalidEncodedPath(String path) {
604                if (path.contains("%")) {
605                        try {
606                                // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
607                                String decodedPath = URLDecoder.decode(path, "UTF-8");
608                                if (isInvalidPath(decodedPath)) {
609                                        return true;
610                                }
611                                decodedPath = processPath(decodedPath);
612                                if (isInvalidPath(decodedPath)) {
613                                        return true;
614                                }
615                        }
616                        catch (IllegalArgumentException ex) {
617                                // Should never happen...
618                        }
619                        catch (UnsupportedEncodingException ex) {
620                                // Should never happen...
621                        }
622                }
623                return false;
624        }
625
626        /**
627         * Identifies invalid resource paths. By default rejects:
628         * <ul>
629         * <li>Paths that contain "WEB-INF" or "META-INF"
630         * <li>Paths that contain "../" after a call to
631         * {@link org.springframework.util.StringUtils#cleanPath}.
632         * <li>Paths that represent a {@link org.springframework.util.ResourceUtils#isUrl
633         * valid URL} or would represent one after the leading slash is removed.
634         * </ul>
635         * <p><strong>Note:</strong> this method assumes that leading, duplicate '/'
636         * or control characters (e.g. white space) have been trimmed so that the
637         * path starts predictably with a single '/' or does not have one.
638         * @param path the path to validate
639         * @return {@code true} if the path is invalid, {@code false} otherwise
640         * @since 3.0.6
641         */
642        protected boolean isInvalidPath(String path) {
643                if (path.contains("WEB-INF") || path.contains("META-INF")) {
644                        if (logger.isTraceEnabled()) {
645                                logger.trace("Path with \"WEB-INF\" or \"META-INF\": [" + path + "]");
646                        }
647                        return true;
648                }
649                if (path.contains(":/")) {
650                        String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
651                        if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
652                                if (logger.isTraceEnabled()) {
653                                        logger.trace("Path represents URL or has \"url:\" prefix: [" + path + "]");
654                                }
655                                return true;
656                        }
657                }
658                if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {
659                        if (logger.isTraceEnabled()) {
660                                logger.trace("Path contains \"../\" after call to StringUtils#cleanPath: [" + path + "]");
661                        }
662                        return true;
663                }
664                return false;
665        }
666
667        /**
668         * Determine the media type for the given request and the resource matched
669         * to it. This implementation tries to determine the MediaType based on the
670         * file extension of the Resource via
671         * {@link ServletPathExtensionContentNegotiationStrategy#getMediaTypeForResource}.
672         * @param request the current request
673         * @param resource the resource to check
674         * @return the corresponding media type, or {@code null} if none found
675         */
676        @SuppressWarnings("deprecation")
677        protected MediaType getMediaType(HttpServletRequest request, Resource resource) {
678                // For backwards compatibility
679                MediaType mediaType = getMediaType(resource);
680                if (mediaType != null) {
681                        return mediaType;
682                }
683                return this.contentNegotiationStrategy.getMediaTypeForResource(resource);
684        }
685
686        /**
687         * Determine an appropriate media type for the given resource.
688         * @param resource the resource to check
689         * @return the corresponding media type, or {@code null} if none found
690         * @deprecated as of 4.3 this method is deprecated; please override
691         * {@link #getMediaType(HttpServletRequest, Resource)} instead.
692         */
693        @Deprecated
694        protected MediaType getMediaType(Resource resource) {
695                return null;
696        }
697
698        /**
699         * Set headers on the given servlet response.
700         * Called for GET requests as well as HEAD requests.
701         * @param response current servlet response
702         * @param resource the identified resource (never {@code null})
703         * @param mediaType the resource's media type (never {@code null})
704         * @throws IOException in case of errors while setting the headers
705         */
706        protected void setHeaders(HttpServletResponse response, Resource resource, MediaType mediaType) throws IOException {
707                long length = resource.contentLength();
708                if (length > Integer.MAX_VALUE) {
709                        if (contentLengthLongAvailable) {
710                                response.setContentLengthLong(length);
711                        }
712                        else {
713                                response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(length));
714                        }
715                }
716                else {
717                        response.setContentLength((int) length);
718                }
719
720                if (mediaType != null) {
721                        response.setContentType(mediaType.toString());
722                }
723                if (resource instanceof EncodedResource) {
724                        response.setHeader(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
725                }
726                if (resource instanceof VersionedResource) {
727                        response.setHeader(HttpHeaders.ETAG, "\"" + ((VersionedResource) resource).getVersion() + "\"");
728                }
729                response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
730        }
731
732
733        @Override
734        public String toString() {
735                return "ResourceHttpRequestHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
736        }
737
738}