From 7ae729480ecfc04ff3e1dd27fded2def1a8d7faf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 7 Oct 2016 22:46:56 +0200 Subject: [PATCH] Resolve absolute resource links in ResourceTransformers This commit adapts the fix for SPR-14597 commited to spring-webmvc to the spring-web-reactive module. Issue: SPR-14597 --- .../resource/AppCacheManifestTransformer.java | 3 +- .../resource/CssLinkResourceTransformer.java | 3 +- .../resource/ResourceTransformerSupport.java | 19 +++++++- .../AppCacheManifestTransformerTests.java | 45 ++++++++++++++---- .../CssLinkResourceTransformerTests.java | 46 +++++++++++++------ 5 files changed, 88 insertions(+), 28 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java index 6d500ed972..e8ae7b2b20 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java @@ -144,7 +144,8 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { return Mono.just(new LineOutput(info.getLine(), null)); } - Mono pathMono = resolveUrlPath(info.getLine(), exchange, resource, chain) + String link = toAbsolutePath(info.getLine(), exchange.getRequest()); + Mono pathMono = resolveUrlPath(link, exchange, resource, chain) .doOnNext(path -> { if (logger.isTraceEnabled()) { logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java index 2465c79fdb..bcae71eb43 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java @@ -102,7 +102,8 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { .concatMap(segment -> { String segmentContent = segment.getContent(fullContent); if (segment.isLink() && !hasScheme(segmentContent)) { - return resolveUrlPath(segmentContent, exchange, newResource, transformerChain) + String link = toAbsolutePath(segmentContent, exchange.getRequest()); + return resolveUrlPath(link, exchange, newResource, transformerChain) .defaultIfEmpty(segmentContent); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceTransformerSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceTransformerSupport.java index 7c48cf5614..5dc1e1b491 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceTransformerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/resource/ResourceTransformerSupport.java @@ -21,6 +21,8 @@ import java.util.Collections; import reactor.core.publisher.Mono; import org.springframework.core.io.Resource; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; /** @@ -72,7 +74,7 @@ public abstract class ResourceTransformerSupport implements ResourceTransformer if (resourcePath.startsWith("/")) { // full resource path ResourceUrlProvider urlProvider = getResourceUrlProvider(); - return (urlProvider != null ? urlProvider.getForRequestUrl(exchange, resourcePath) : null); + return (urlProvider != null ? urlProvider.getForRequestUrl(exchange, resourcePath) : Mono.empty()); } else { // try resolving as relative path @@ -81,4 +83,19 @@ public abstract class ResourceTransformerSupport implements ResourceTransformer } } + /** + * Transform the given relative request path to an absolute path, + * taking the path of the given request as a point of reference. + * The resulting path is also cleaned from sequences like "path/..". + * + * @param path the relative path to transform + * @param request the referer request + * @return the absolute request path for the given resource path + */ + protected String toAbsolutePath(String path, ServerHttpRequest request) { + String requestPath = request.getURI().getPath(); + String absolutePath = StringUtils.applyRelativePath(requestPath, path); + return StringUtils.cleanPath(absolutePath); + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java index e7c0a3d0e5..30104fd342 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java @@ -61,18 +61,35 @@ public class AppCacheManifestTransformerTests { @Before public void setup() { - this.transformer = new AppCacheManifestTransformer(); - this.chain = mock(ResourceTransformerChain.class); + ClassPathResource allowedLocation = new ClassPathResource("test/", getClass()); + ResourceWebHandler resourceHandler = new ResourceWebHandler(); + ResourceUrlProvider resourceUrlProvider = new ResourceUrlProvider(); + resourceUrlProvider.setHandlerMap(Collections.singletonMap("/static/**", resourceHandler)); - MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, ""); - ServerHttpResponse response = new MockServerHttpResponse(); - WebSessionManager manager = new DefaultWebSessionManager(); - this.exchange = new DefaultServerWebExchange(request, response, manager); + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy())); + PathResourceResolver pathResolver = new PathResourceResolver(); + pathResolver.setAllowedLocations(allowedLocation); + List resolvers = Arrays.asList(versionResolver, pathResolver); + ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers); + + CssLinkResourceTransformer cssLinkResourceTransformer = new CssLinkResourceTransformer(); + cssLinkResourceTransformer.setResourceUrlProvider(resourceUrlProvider); + List transformers = Arrays.asList(cssLinkResourceTransformer); + this.chain = new DefaultResourceTransformerChain(resolverChain, transformers); + this.transformer = new AppCacheManifestTransformer(); + this.transformer.setResourceUrlProvider(resourceUrlProvider); + + resourceHandler.setResourceResolvers(resolvers); + resourceHandler.setResourceTransformers(transformers); + resourceHandler.setLocations(Collections.singletonList(allowedLocation)); } @Test public void noTransformIfExtensionNoMatch() throws Exception { + initExchange(HttpMethod.GET, "/static/foobar.file"); + this.chain = mock(ResourceTransformerChain.class); Resource resource = mock(Resource.class); given(resource.getFilename()).willReturn("foobar.file"); given(this.chain.transform(this.exchange, resource)).willReturn(Mono.just(resource)); @@ -83,6 +100,8 @@ public class AppCacheManifestTransformerTests { @Test public void syntaxErrorInManifest() throws Exception { + initExchange(HttpMethod.GET, "/static/error.appcache"); + this.chain = mock(ResourceTransformerChain.class); Resource resource = new ClassPathResource("test/error.appcache", getClass()); given(this.chain.transform(this.exchange, resource)).willReturn(Mono.just(resource)); @@ -92,7 +111,7 @@ public class AppCacheManifestTransformerTests { @Test public void transformManifest() throws Exception { - + initExchange(HttpMethod.GET, "/static/test.appcache"); VersionResourceResolver versionResolver = new VersionResourceResolver(); versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy())); @@ -112,11 +131,11 @@ public class AppCacheManifestTransformerTests { String content = new String(bytes, "UTF-8"); assertThat("should rewrite resource links", content, - Matchers.containsString("foo-e36d2e05253c6c7085a91522ce43a0b4.css")); + Matchers.containsString("/static/foo-e36d2e05253c6c7085a91522ce43a0b4.css")); assertThat("should rewrite resource links", content, - Matchers.containsString("bar-11e16cf79faee7ac698c805cf28248d2.css")); + Matchers.containsString("/static/bar-11e16cf79faee7ac698c805cf28248d2.css")); assertThat("should rewrite resource links", content, - Matchers.containsString("js/bar-bd508c62235b832d960298ca6c0b7645.js")); + Matchers.containsString("/static/js/bar-bd508c62235b832d960298ca6c0b7645.js")); assertThat("should not rewrite external resources", content, Matchers.containsString("//example.org/style.css")); @@ -127,4 +146,10 @@ public class AppCacheManifestTransformerTests { Matchers.containsString("# Hash: 4bf0338bcbeb0a5b3a4ec9ed8864107d")); } + private void initExchange(HttpMethod method, String url) { + MockServerHttpRequest request = new MockServerHttpRequest(method, url); + ServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, response, manager); + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java index a3739f5b77..faeacb2ab9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java @@ -52,39 +52,45 @@ public class CssLinkResourceTransformerTests { @Before public void setUp() { + ClassPathResource allowedLocation = new ClassPathResource("test/", getClass()); + ResourceWebHandler resourceHandler = new ResourceWebHandler(); + + ResourceUrlProvider resourceUrlProvider = new ResourceUrlProvider(); + resourceUrlProvider.setHandlerMap(Collections.singletonMap("/static/**", resourceHandler)); + VersionResourceResolver versionResolver = new VersionResourceResolver(); versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy())); - PathResourceResolver pathResolver = new PathResourceResolver(); - pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass())); - + pathResolver.setAllowedLocations(allowedLocation); List resolvers = Arrays.asList(versionResolver, pathResolver); - List transformers = Collections.singletonList(new CssLinkResourceTransformer()); + CssLinkResourceTransformer cssLinkResourceTransformer = new CssLinkResourceTransformer(); + cssLinkResourceTransformer.setResourceUrlProvider(resourceUrlProvider); + List transformers = Collections.singletonList(cssLinkResourceTransformer); + + resourceHandler.setResourceResolvers(resolvers); + resourceHandler.setResourceTransformers(transformers); + resourceHandler.setLocations(Collections.singletonList(allowedLocation)); ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers); this.transformerChain = new DefaultResourceTransformerChain(resolverChain, transformers); - - MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, ""); - ServerHttpResponse response = new MockServerHttpResponse(); - WebSessionManager manager = new DefaultWebSessionManager(); - this.exchange = new DefaultServerWebExchange(request, response, manager); } @Test public void transform() throws Exception { + initExchange(HttpMethod.GET, "/static/main.css"); Resource css = new ClassPathResource("test/main.css", getClass()); TransformedResource actual = (TransformedResource) this.transformerChain.transform(this.exchange, css) .blockMillis(5000); String expected = "\n" + - "@import url(\"bar-11e16cf79faee7ac698c805cf28248d2.css\");\n" + - "@import url('bar-11e16cf79faee7ac698c805cf28248d2.css');\n" + - "@import url(bar-11e16cf79faee7ac698c805cf28248d2.css);\n\n" + - "@import \"foo-e36d2e05253c6c7085a91522ce43a0b4.css\";\n" + - "@import 'foo-e36d2e05253c6c7085a91522ce43a0b4.css';\n\n" + - "body { background: url(\"images/image-f448cd1d5dba82b774f3202c878230b3.png\") }\n"; + "@import url(\"/static/bar-11e16cf79faee7ac698c805cf28248d2.css\");\n" + + "@import url('/static/bar-11e16cf79faee7ac698c805cf28248d2.css');\n" + + "@import url(/static/bar-11e16cf79faee7ac698c805cf28248d2.css);\n\n" + + "@import \"/static/foo-e36d2e05253c6c7085a91522ce43a0b4.css\";\n" + + "@import '/static/foo-e36d2e05253c6c7085a91522ce43a0b4.css';\n\n" + + "body { background: url(\"/static/images/image-f448cd1d5dba82b774f3202c878230b3.png\") }\n"; String result = new String(actual.getByteArray(), "UTF-8"); result = StringUtils.deleteAny(result, "\r"); @@ -93,6 +99,7 @@ public class CssLinkResourceTransformerTests { @Test public void transformNoLinks() throws Exception { + initExchange(HttpMethod.GET, "/static/foo.css"); Resource expected = new ClassPathResource("test/foo.css", getClass()); Resource actual = this.transformerChain.transform(this.exchange, expected).blockMillis(5000); assertSame(expected, actual); @@ -100,6 +107,7 @@ public class CssLinkResourceTransformerTests { @Test public void transformExtLinksNotAllowed() throws Exception { + initExchange(HttpMethod.GET, "/static/external.css"); ResourceResolverChain resolverChain = Mockito.mock(DefaultResourceResolverChain.class); ResourceTransformerChain transformerChain = new DefaultResourceTransformerChain(resolverChain, Collections.singletonList(new CssLinkResourceTransformer())); @@ -125,9 +133,17 @@ public class CssLinkResourceTransformerTests { @Test public void transformWithNonCssResource() throws Exception { + initExchange(HttpMethod.GET, "/static/images/image.png"); Resource expected = new ClassPathResource("test/images/image.png", getClass()); Resource actual = this.transformerChain.transform(this.exchange, expected).blockMillis(5000); assertSame(expected, actual); } + private void initExchange(HttpMethod method, String url) { + MockServerHttpRequest request = new MockServerHttpRequest(method, url); + ServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, response, manager); + } + }