This is an automated email from the ASF dual-hosted git repository. thiagohp pushed a commit to branch 5.9.x in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 3b2561b227c350b8b15134225338e2882515298f Author: Ben Weidig <[email protected]> AuthorDate: Sat Jul 18 11:13:28 2026 +0200 TAP5-2835: ContextAssetProtectionRule chain added, don't serve source maps in prod --- .../assets/ContextAssetRequestHandler.java | 15 ++- .../org/apache/tapestry5/modules/AssetsModule.java | 44 ++++++++- .../services/ContextAssetProtectionRule.java | 34 +++++++ .../assets/ContextAssetRequestHandlerTest.java | 104 ++++++++++++++++----- .../valid-asset.js | 1 + .../valid-asset.js.map | 1 + 6 files changed, 169 insertions(+), 30 deletions(-) diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java index fdb2f402c..1ba8e007f 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandler.java @@ -1,4 +1,4 @@ -// Copyright 2010, 2013 The Apache Software Foundation +// Copyright 2010, 2013, 2026 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.http.services.Request; import org.apache.tapestry5.http.services.Response; import org.apache.tapestry5.internal.services.ResourceStreamer; +import org.apache.tapestry5.services.ContextAssetProtectionRule; import org.apache.tapestry5.services.assets.AssetRequestHandler; import java.io.IOException; @@ -35,12 +36,16 @@ public class ContextAssetRequestHandler implements AssetRequestHandler private final Resource rootContextResource; + private final ContextAssetProtectionRule contextAssetProtectionRule; + private final Pattern illegal = Pattern.compile("^([\\\\/]*((web|meta)-inf.*)|(.*\\.tml$))", Pattern.CASE_INSENSITIVE); - public ContextAssetRequestHandler(ResourceStreamer resourceStreamer, Resource rootContextResource) + public ContextAssetRequestHandler(ResourceStreamer resourceStreamer, Resource rootContextResource, + ContextAssetProtectionRule contextAssetProtectionRule) { this.resourceStreamer = resourceStreamer; this.rootContextResource = rootContextResource; + this.contextAssetProtectionRule = contextAssetProtectionRule; } public boolean handleAssetRequest(Request request, Response response, String extraPath) throws IOException @@ -52,6 +57,12 @@ public class ContextAssetRequestHandler implements AssetRequestHandler return false; } + // TAP5-2835: Allow custom protection rules mirroring ClasspathAssetProtectionRule + if (contextAssetProtectionRule.block(path.resourcePath)) + { + return false; + } + return path.stream(rootContextResource.forFile(path.resourcePath)); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java index e634285c8..dc414c448 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/AssetsModule.java @@ -76,6 +76,7 @@ import org.apache.tapestry5.services.ClasspathAssetAliasManager; import org.apache.tapestry5.services.ClasspathAssetProtectionRule; import org.apache.tapestry5.services.ClasspathProvider; import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.ContextAssetProtectionRule; import org.apache.tapestry5.services.ContextProvider; import org.apache.tapestry5.services.Core; import org.apache.tapestry5.services.assets.AssetChecksumGenerator; @@ -331,7 +332,8 @@ public class AssetsModule ClasspathAssetAliasManager classpathAssetAliasManager, ResourceStreamer streamer, AssetSource assetSource, - ClasspathAssetProtectionRule classpathAssetProtectionRule) + ClasspathAssetProtectionRule classpathAssetProtectionRule, + ContextAssetProtectionRule contextAssetProtectionRule) { Map<String, String> mappings = classpathAssetAliasManager.getMappings(); @@ -343,7 +345,7 @@ public class AssetsModule } configuration.add(RequestConstants.CONTEXT_FOLDER, - new ContextAssetRequestHandler(streamer, contextAssetFactory.getRootResource())); + new ContextAssetRequestHandler(streamer, contextAssetFactory.getRootResource(), contextAssetProtectionRule)); configuration.add(RequestConstants.STACK_FOLDER, stackAssetRequestHandler); @@ -421,7 +423,9 @@ public class AssetsModule } public static void contributeClasspathAssetProtectionRule( - OrderedConfiguration<ClasspathAssetProtectionRule> configuration) + OrderedConfiguration<ClasspathAssetProtectionRule> configuration, + @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) + boolean productionMode) { ClasspathAssetProtectionRule classFileRule = (s) -> s.toLowerCase().endsWith(".class"); configuration.add("ClassFile", classFileRule); @@ -433,9 +437,39 @@ public class AssetsModule configuration.add("TemplateFile", tmlFileRule); ClasspathAssetProtectionRule folderRule = (s) -> isFolderToBlock(s); configuration.add("Folder", folderRule); + + // TAP5-2835: Don't serve source maps in production + if (productionMode) + { + ClasspathAssetProtectionRule mapFileRule = (s) -> s.toLowerCase().endsWith(".map"); + configuration.add("MapFile", mapFileRule); + } } - - final private static boolean isFolderToBlock(String path) + + @Primary + public static ContextAssetProtectionRule buildContextAssetProtectionRule( + List<ContextAssetProtectionRule> rules, ChainBuilder chainBuilder) + { + return chainBuilder.build(ContextAssetProtectionRule.class, rules); + } + + @Contribute(ContextAssetProtectionRule.class) + public static void contributeContextAssetProtectionRule( + OrderedConfiguration<ContextAssetProtectionRule> configuration, + + @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) + boolean productionMode) + { + // Source maps expose original, unminified source; don't let them leak out in production. + // Applications that want source maps served in production can override this contribution. + if (productionMode) + { + ContextAssetProtectionRule mapFileRule = (s) -> s.toLowerCase().endsWith(".map"); + configuration.add("MapFile", mapFileRule); + } + } + + final private static boolean isFolderToBlock(String path) { path = path.replace('\\', '/'); final int lastIndex = path.lastIndexOf('/'); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java new file mode 100644 index 000000000..a977740e1 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/ContextAssetProtectionRule.java @@ -0,0 +1,34 @@ +// Copyright 2026 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.services; + +import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration; + +/** + * Chain-of-responsibility service which defines rules for blocking access to context resources + * based on their paths. Access is blocked if any rule says it should be blocked. + * + * @see ClasspathAssetProtectionRule + * @since 5.10.0 + */ +@UsesOrderedConfiguration(ContextAssetProtectionRule.class) +public interface ContextAssetProtectionRule +{ + /** + * Tells whether the access to the resource with this path should be blocked or not. + * If this rule doesn't concern the given path, it should return false. + */ + boolean block(String path); +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java index b80c078ca..8b9dc54b8 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/assets/ContextAssetRequestHandlerTest.java @@ -1,4 +1,4 @@ -// Copyright 2010, 2013 The Apache Software Foundation +// Copyright 2010, 2013, 2026 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,36 +14,94 @@ package org.apache.tapestry5.internal.services.assets; -import org.apache.tapestry5.test.ioc.TestBase; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.tapestry5.commons.Resource; +import org.apache.tapestry5.internal.services.ResourceStreamer; +import org.apache.tapestry5.ioc.internal.util.ClasspathResource; +import org.apache.tapestry5.services.ContextAssetProtectionRule; +import org.apache.tapestry5.services.assets.StreamableResource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; +import java.util.Set; -public class ContextAssetRequestHandlerTest extends TestBase +class ContextAssetRequestHandlerTest { - @DataProvider - public Object[][] invalid_paths() + private static final ContextAssetProtectionRule NEVER_BLOCK = (path) -> false; + + private static final ContextAssetProtectionRule BLOCK_MAP_FILES = (path) -> path.toLowerCase().endsWith(".map"); + + private static final Resource CONTEXT_ROOT = new ClasspathResource(""); + + private static final ResourceStreamer ALWAYS_STREAM = new ResourceStreamer() { - return new Object[][] - { - {"web-Inf/classes/hibernate.cfg.xml"}, - {"Meta-Inf/MANIFEST.mf"}, - {"Index.tml"}, - {"folder/FolderIndex.TML"}, - {"\\WEB-INF/something.jpg"}, - {"\\//WEB-INF/something.jpg"}, - {"//WEB-INF/something.jpg"}, - {"//\\\\WEB-INF/something.jpg"} - }; + @Override + public boolean streamResource(Resource resource, String providedChecksum, Set<Options> options) + { + return true; + } + + @Override + public boolean streamResource(StreamableResource resource, String providedChecksum, Set<Options> options) + { + throw new UnsupportedOperationException(); + } + }; + + @ParameterizedTest + @ValueSource(strings = { + "web-Inf/classes/hibernate.cfg.xml", + "Meta-Inf/MANIFEST.mf", + "Index.tml", + "folder/FolderIndex.TML", + "\\WEB-INF/something.jpg", + "\\//WEB-INF/something.jpg", + "//WEB-INF/something.jpg", + "//\\\\WEB-INF/something.jpg" + }) + void ensureAssetsAreRejected(String path) throws IOException + { + // ARRANGE + ContextAssetRequestHandler handler = new ContextAssetRequestHandler(null, null, NEVER_BLOCK); + + // ACT + boolean providedResponse = handler.handleAssetRequest(null, null, "fake-checksum/" + path); + + // ASSERT + assertFalse(providedResponse, "Handler should return false for invalid path."); } - @Test(dataProvider = "invalid_paths") - public void ensure_assets_are_rejected(String path) throws IOException + @Test + void contextAssetProtectionRuleIsConsulted() throws IOException { - ContextAssetRequestHandler handler = new ContextAssetRequestHandler(null, null); + // ARRANGE + ContextAssetRequestHandler handler = new ContextAssetRequestHandler(null, null, BLOCK_MAP_FILES); + + // ACT + boolean providedResponse = handler.handleAssetRequest(null, null, "fake-checksum/app.js.map"); + + // ASSERT + assertFalse(providedResponse, "Handler should return false when the protection rule blocks the path."); + } + + @ParameterizedTest + @ValueSource(strings = { + "context-asset-request-handler-test/valid-asset.js", + "context-asset-request-handler-test/valid-asset.js.map" + }) + void validAssetsAreStreamedWhenNotBlocked(String path) throws IOException + { + // ARRANGE + ContextAssetRequestHandler handler = new ContextAssetRequestHandler(ALWAYS_STREAM, CONTEXT_ROOT, NEVER_BLOCK); + + // ACT + boolean providedResponse = handler.handleAssetRequest(null, null, "fake-checksum/" + path); - assertFalse(handler.handleAssetRequest(null, null, "fake-checksum/" + path), - "Handler should return false for invalid path."); + // ASSERT + assertTrue(providedResponse, "Handler should return true for a valid, existing asset that no rule blocks."); } } diff --git a/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js new file mode 100644 index 000000000..4a0efb4a5 --- /dev/null +++ b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js @@ -0,0 +1 @@ +// Fixture for ContextAssetRequestHandlerTest; content is irrelevant, only existence matters. diff --git a/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map new file mode 100644 index 000000000..b5415a8b4 --- /dev/null +++ b/tapestry-core/src/test/resources/context-asset-request-handler-test/valid-asset.js.map @@ -0,0 +1 @@ +{ "version": 3, "sources": [], "mappings": "" }
