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}