001// Copyright 2010, 2013, 2026 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.ContextAssetProtectionRule; 022import org.apache.tapestry5.services.assets.AssetRequestHandler; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import java.io.IOException; 027import java.util.regex.Pattern; 028 029/** 030 * Handles requests for context assets, screening out attempt to 031 * access anything under WEB-INF or META-INF. 032 * 033 * @since 5.2.0 034 */ 035public class ContextAssetRequestHandler implements AssetRequestHandler 036{ 037 private final static Logger LOGGER = LoggerFactory.getLogger(ContextAssetRequestHandler.class); 038 039 private final ResourceStreamer resourceStreamer; 040 041 private final Resource rootContextResource; 042 043 private final ContextAssetProtectionRule contextAssetProtectionRule; 044 045 private final Pattern illegal = Pattern.compile("^([\\\\/]*((web|meta)-inf.*)|(.*\\.tml$))", Pattern.CASE_INSENSITIVE); 046 047 public ContextAssetRequestHandler(ResourceStreamer resourceStreamer, Resource rootContextResource, 048 ContextAssetProtectionRule contextAssetProtectionRule) 049 { 050 this.resourceStreamer = resourceStreamer; 051 this.rootContextResource = rootContextResource; 052 this.contextAssetProtectionRule = contextAssetProtectionRule; 053 } 054 055 public boolean handleAssetRequest(Request request, Response response, String extraPath) throws IOException 056 { 057 ChecksumPath path = new ChecksumPath(resourceStreamer, null, extraPath); 058 059 final Resource resource; 060 061 try 062 { 063 resource = rootContextResource.forFile(path.resourcePath); 064 } 065 catch (IllegalStateException ex) 066 { 067 // Fail closed: treat it as blocked rather than surfacing it as a server error. 068 if (LOGGER.isWarnEnabled()) 069 { 070 LOGGER.warn("Blocked request for context asset '" + path.resourcePath + 071 "': " + ex.getMessage()); 072 } 073 return false; 074 } 075 076 final String resourcePath = resource.getPath(); 077 078 if (illegal.matcher(resourcePath).matches()) 079 { 080 if (LOGGER.isWarnEnabled()) 081 { 082 LOGGER.warn("Blocked request for context asset '" + resourcePath + 083 "': the path resolves to a protected location (WEB-INF, META-INF or a template)."); 084 } 085 return false; 086 } 087 088 // TAP5-2835: Allow custom protection rules mirroring ClasspathAssetProtectionRule 089 if (contextAssetProtectionRule.block(resourcePath)) 090 { 091 if (LOGGER.isWarnEnabled()) 092 { 093 LOGGER.warn("Blocked request for context asset '" + resourcePath + 094 "'. Contribute a new ContextAssetProtectionRule if you need this asset to be publicly accessible."); 095 } 096 return false; 097 } 098 099 return path.stream(resource); 100 } 101 102}