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}