001// Copyright 2010, 2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services.assets; 016 017import org.apache.tapestry5.commons.Resource; 018import org.apache.tapestry5.http.services.Request; 019import org.apache.tapestry5.http.services.Response; 020import org.apache.tapestry5.internal.services.ResourceStreamer; 021import org.apache.tapestry5.services.AssetSource; 022import org.apache.tapestry5.services.ClasspathAssetAliasManager; 023import org.apache.tapestry5.services.ClasspathAssetProtectionRule; 024import org.apache.tapestry5.services.assets.AssetRequestHandler; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028import java.io.File; 029import java.io.IOException; 030import java.net.URISyntaxException; 031import java.net.URL; 032 033/** 034 * A handler for asset requests for classpath assets (within a specific folder). 035 * Each mapping of the {@link ClasspathAssetAliasManager} gets one of these. 036 * 037 * @since 5.2.0 038 */ 039public class ClasspathAssetRequestHandler implements AssetRequestHandler 040{ 041 042 private final static Logger LOGGER = LoggerFactory.getLogger(ClasspathAssetRequestHandler.class); 043 044 private final ResourceStreamer streamer; 045 046 private final AssetSource assetSource; 047 048 private final String baseFolder; 049 050 private final ClasspathAssetProtectionRule classpathAssetProtectionRule; 051 052 public ClasspathAssetRequestHandler(ResourceStreamer streamer, 053 AssetSource assetSource, String baseFolder, 054 ClasspathAssetProtectionRule classpathAssetProtectionRule) 055 { 056 this.streamer = streamer; 057 this.assetSource = assetSource; 058 this.baseFolder = baseFolder; 059 this.classpathAssetProtectionRule = classpathAssetProtectionRule; 060 } 061 062 public boolean handleAssetRequest(Request request, Response response, String extraPath) throws IOException 063 { 064 ChecksumPath path = new ChecksumPath(streamer, baseFolder, extraPath); 065 066 // Resolve the resource first, then check the protection rule against the 067 // fully-normalized path that will actually be loaded and streamed (Resource#getPath()). 068 final Resource resource; 069 070 try 071 { 072 resource = assetSource.resourceForPath(path.resourcePath); 073 } 074 catch (IllegalStateException ex) 075 { 076 // The path normalized to a location above the classpath root (e.g. a "../" escape). 077 // Fail closed: treat it as blocked rather than surfacing it as a server error. 078 if (LOGGER.isWarnEnabled()) 079 { 080 LOGGER.warn("Blocked request for classpath asset '" + path.resourcePath + 081 "': " + ex.getMessage()); 082 } 083 return false; 084 } 085 086 final String resourcePath = resource.getPath(); 087 088 final boolean handled; 089 090 if (classpathAssetProtectionRule.block(resourcePath) && !path.resourcePath.equals(ChecksumPath.NON_EXISTING_RESOURCE)) 091 { 092 if (LOGGER.isWarnEnabled()) 093 { 094 LOGGER.warn("Blocked request for classpath asset '" + resourcePath + 095 "'. Contribute a new ClasspathAssetProtectionRule if you need this asset to be publicly accessible."); 096 } 097 handled = false; 098 } 099 else if (isDirectory(resource)) 100 { 101 if (LOGGER.isWarnEnabled()) 102 { 103 LOGGER.warn("Blocked request for classpath asset '" + resourcePath + 104 "': the path resolves to a directory, not a file."); 105 } 106 handled = false; 107 } 108 else 109 { 110 handled = path.stream(resource); 111 } 112 return handled; 113 } 114 115 /** 116 * Tells whether the resolved resource is a directory rather than a streamable file. A 117 * non-existent resource is not a directory (it is handled as a normal "not found" downstream). 118 */ 119 private static boolean isDirectory(Resource resource) 120 { 121 final URL url = resource.toURL(); 122 123 if (url == null) 124 { 125 return false; 126 } 127 128 if ("file".equalsIgnoreCase(url.getProtocol())) 129 { 130 try 131 { 132 return new File(url.toURI()).isDirectory(); 133 } 134 catch (URISyntaxException ex) 135 { 136 return new File(url.getPath()).isDirectory(); 137 } 138 } 139 140 // Inside archives (jar:, etc.) a directory entry, when the classloader exposes one at all, 141 // is conventionally represented with a trailing slash. 142 return url.getPath().endsWith("/"); 143 } 144}