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