diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 65d52e55c4617..4a98d84efe846 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -78,6 +78,13 @@ TIP: By default, the following list of media types is compressed: `text/html`, ` NOTE: If the client does not support HTTP compression then the response body is not compressed. +[[static-resources-config]] +=== Other Configurations + +Additionally, the index page for static resources can be changed from default `index.html`, the hidden files (e.g. dot files) can be indicated as not served, the range requests can be disabled, and the caching support (e.g. caching headers and file properties cache) can be configured. + +include::{generated-dir}/config/quarkus-vertx-http-config-group-static-resources-config.adoc[leveloffset=+1, opts=optional] + [[context-path]] == Configuring the Context path diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index 238f4203f402f..c737487120cb4 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -12,12 +12,10 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import io.quarkus.arc.deployment.BeanContainerBuildItem; -import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -30,7 +28,7 @@ import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; -import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; import io.quarkus.vertx.http.runtime.StaticResourcesRecorder; /** @@ -38,107 +36,40 @@ */ public class StaticResourcesProcessor { - @Deprecated - public static final class StaticResourcesBuildItem extends SimpleBuildItem { - - private final Set entries; - - public StaticResourcesBuildItem(Set entries) { - this.entries = entries; - } - - public Set getEntries() { - return entries; - } - - public Set getPaths() { - Set paths = new HashSet<>(entries.size()); - for (Entry entry : entries) { - paths.add(entry.getPath()); - } - return paths; - } - - public static class Entry { - private final String path; - private final boolean isDirectory; - - public Entry(String path, boolean isDirectory) { - this.path = path; - this.isDirectory = isDirectory; - } - - public String getPath() { - return path; - } - - public boolean isDirectory() { - return isDirectory; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Entry entry = (Entry) o; - return isDirectory == entry.isDirectory && path.equals(entry.path); - } - - @Override - public int hashCode() { - return Objects.hash(path, isDirectory); - } - } - - } - @BuildStep void collectStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem applicationArchivesBuildItem, List additionalStaticResources, - Optional deprecatedStaticResources, - BuildProducer staticResources) throws Exception { + BuildProducer staticResources) throws Exception { if (capabilities.isPresent(Capability.SERVLET)) { // Servlet container handles static resources return; } - // Copy deprecated build item - Set paths = getClasspathResources( - applicationArchivesBuildItem); - if (deprecatedStaticResources.isPresent()) { - Set deprecatedEntries = deprecatedStaticResources.get().getEntries(); - for (StaticResourcesBuildItem.Entry deprecatedEntry : deprecatedEntries) { - paths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(deprecatedEntry.getPath(), - deprecatedEntry.isDirectory())); - } - } + Set paths = getClasspathResources(applicationArchivesBuildItem); for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { - paths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); + paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); } if (!paths.isEmpty()) { - staticResources.produce(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem(paths)); + staticResources.produce(new StaticResourcesBuildItem(paths)); } } @BuildStep @Record(RUNTIME_INIT) - public void runtimeInit(Optional staticResources, - StaticResourcesRecorder recorder, CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, - BuildProducer defaultRoutes, HttpConfiguration config) { + public void runtimeInit(Optional staticResources, StaticResourcesRecorder recorder, + CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, + BuildProducer defaultRoutes) { if (staticResources.isPresent()) { defaultRoutes.produce(new DefaultRouteBuildItem(recorder.start(staticResources.get().getPaths()))); } } @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - public void nativeImageResource(Optional staticResources, + public void nativeImageResource(Optional staticResources, BuildProducer producer) { if (staticResources.isPresent()) { - Set entries = staticResources.get() - .getEntries(); + Set entries = staticResources.get().getEntries(); List metaInfResources = new ArrayList<>(entries.size()); - for (io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry entry : entries) { + for (StaticResourcesBuildItem.Entry entry : entries) { if (entry.isDirectory()) { // TODO: do we perhaps want to register the whole directory? continue; @@ -157,10 +88,9 @@ public void nativeImageResource(Optional getClasspathResources( - ApplicationArchivesBuildItem applicationArchivesBuildItem) + private Set getClasspathResources(ApplicationArchivesBuildItem applicationArchivesBuildItem) throws Exception { - Set knownPaths = new HashSet<>(); + Set knownPaths = new HashSet<>(); for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { i.accept(tree -> { @@ -178,34 +108,19 @@ private Set return knownPaths; } - private void collectKnownPaths(Path resource, - Set knownPaths) { + private void collectKnownPaths(Path resource, Set knownPaths) { try { Files.walkFileTree(resource, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path p, BasicFileAttributes attrs) throws IOException { - String simpleName = p.getFileName().toString(); String file = resource.relativize(p).toString(); - if (simpleName.equals("index.html") || simpleName.equals("index.htm")) { - Path parent = resource.relativize(p).getParent(); - if (parent == null) { - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry("/", true)); - } else { - String parentString = parent.toString(); - if (!parentString.startsWith("/")) { - parentString = "/" + parentString; - } - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry( - parentString + "/", true)); - } - } if (!file.startsWith("/")) { file = "/" + file; } // Windows has a backslash file = file.replace('\\', '/'); - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(file, false)); + knownPaths.add(new StaticResourcesBuildItem.Entry(file, false)); return FileVisitResult.CONTINUE; } }); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java index c60ad3f28cb52..2fa199e967f5e 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java @@ -9,22 +9,17 @@ public abstract class AbstractStaticResourcesTest { @Test public void shouldEncodeHtmlPage() { - RestAssured.when().get("/static-file.html") - .then() - .header("Content-Encoding", "gzip") - .header("Transfer-Encoding", "chunked") - .body(Matchers.containsString("This is the title of the webpage!")) - .statusCode(200); + assertEncodedResponse("/static-file.html"); } @Test public void shouldEncodeRootPage() { - RestAssured.when().get("/") - .then() - .header("Content-Encoding", "gzip") - .header("Transfer-Encoding", "chunked") - .body(Matchers.containsString("This is the title of the webpage!")) - .statusCode(200); + assertEncodedResponse("/"); + } + + @Test + public void shouldEncodeHiddenHtmlPage() { + assertEncodedResponse("/.hidden-file.html"); } @Test @@ -36,4 +31,22 @@ public void shouldNotEncodeSVG() { .statusCode(200); } + @Test + public void shouldReturnRangeSupport() { + RestAssured.when().head("/") + .then() + .header("Accept-Ranges", "bytes") + .header("Content-Length", Integer::parseInt, Matchers.greaterThan(0)) + .statusCode(200); + } + + protected void assertEncodedResponse(String path) { + RestAssured.when().get(path) + .then() + .header("Content-Encoding", "gzip") + .header("Transfer-Encoding", "chunked") + .body(Matchers.containsString("This is the title of the webpage!")) + .statusCode(200); + } + } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java new file mode 100644 index 0000000000000..852381648e108 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java @@ -0,0 +1,31 @@ +package io.quarkus.vertx.http; + +import static org.hamcrest.Matchers.nullValue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class StaticResourcesCachingDisabledTest { + + @RegisterExtension + final static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .add(new StringAsset( + "quarkus.http.static-resources.caching-enabled=false\n"), + "application.properties") + .addAsResource("static-file.html", "META-INF/resources/index.html")); + + @Test + public void shouldNotContainCachingHeaders() { + RestAssured.when().get("/") + .then() + .header("Cache-Control", nullValue()) + .header("Last-Modified", nullValue()) + .statusCode(200); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java new file mode 100644 index 0000000000000..b5e065e919769 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java @@ -0,0 +1,52 @@ +package io.quarkus.vertx.http; + +import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class StaticResourcesCustomizedPagesTest { + + @RegisterExtension + final static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .add(new StringAsset("" + + "quarkus.http.static-resources.index-page=default.html\n" + + "quarkus.http.static-resources.include-hidden=false\n" + + "quarkus.http.static-resources.enable-range-support=false\n"), + "application.properties") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") + .addAsResource("static-file.html", "META-INF/resources/default.html")); + + @Test + public void shouldContainCachingHeaders() { + RestAssured.when().get("/") + .then() + .header("Cache-Control", containsStringIgnoringCase("max-age=")) + .header("Last-Modified", notNullValue()) + .statusCode(200); + } + + @Test + public void shouldNotReturnHiddenHtmlPage() { + RestAssured.when().get("/.hidden-file.html") + .then() + .statusCode(404); + } + + @Test + public void shouldNotReturnRangeSupport() { + RestAssured.when().head("/") + .then() + .header("Accept-Ranges", nullValue()) + .header("Content-Length", nullValue()) + .statusCode(200); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java index 81df34794aa72..d1a7f6c623c53 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java @@ -13,6 +13,7 @@ public class StaticResourcesTest extends AbstractStaticResourcesTest { .add(new StringAsset("quarkus.http.enable-compression=true\n"), "application.properties") .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") .addAsResource("static-file.html", "META-INF/resources/index.html") .addAsResource("static-file.html", "META-INF/resources/image.svg")); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java index 3c358a610b5e9..c4b8ced645ee0 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java @@ -17,6 +17,7 @@ public class StaticResourcesDevModeTest extends AbstractStaticResourcesTest { .add(new StringAsset("quarkus.http.enable-compression=true\n"), "application.properties") .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") .addAsResource("static-file.html", "META-INF/resources/index.html") .addAsResource("static-file.html", "META-INF/resources/image.svg")); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 86e6eb7d89581..e62490a3dddbe 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -91,6 +91,11 @@ public class HttpConfiguration { */ public ServerSslConfig ssl; + /** + * The Static Resources config + */ + public StaticResourcesConfig staticResources; + /** * When set to {@code true}, the HTTP server automatically sends `100 CONTINUE` * response when the request expects it (with the `Expect: 100-Continue` header). diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java new file mode 100644 index 0000000000000..94ff031402d81 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java @@ -0,0 +1,53 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class StaticResourcesConfig { + + /** + * Set the index page when serving static resources. + */ + @ConfigItem(defaultValue = "index.html") + public String indexPage; + + /** + * Set whether hidden files should be served. + */ + @ConfigItem(defaultValue = "true") + public boolean includeHidden; + + /** + * Set whether range requests (resumable downloads; media streaming) should be enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enableRangeSupport; + + /** + * Set whether cache handling is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean cachingEnabled; + + /** + * Set the cache entry timeout. The default is {@code 30} seconds. + */ + @ConfigItem(defaultValue = "30S") + public Duration cacheEntryTimeout; + + /** + * Set value for max age in caching headers. The default is {@code 24} hours. + */ + @ConfigItem(defaultValue = "24H") + public Duration maxAge; + + /** + * Set the max cache size. + */ + @ConfigItem(defaultValue = "10000") + public int maxCacheSize; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index c21561bb2d0f3..e6e3a42c10af8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -13,6 +13,7 @@ import io.vertx.core.http.impl.MimeMapping; import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.FileSystemAccess; import io.vertx.ext.web.handler.StaticHandler; @Recorder @@ -41,15 +42,17 @@ public Consumer start(Set knownPaths) { this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get()); } List> handlers = new ArrayList<>(); + StaticResourcesConfig config = httpConfiguration.getValue().staticResources; if (hotDeploymentResourcePaths != null && !hotDeploymentResourcePaths.isEmpty()) { for (Path resourcePath : hotDeploymentResourcePaths) { String root = resourcePath.toAbsolutePath().toString(); - StaticHandler staticHandler = StaticHandler.create(); - staticHandler.setCachingEnabled(false); - staticHandler.setAllowRootFileSystemAccess(true); - staticHandler.setWebRoot(root); - staticHandler.setDefaultContentEncoding("UTF-8"); + StaticHandler staticHandler = StaticHandler.create(FileSystemAccess.ROOT, root) + .setDefaultContentEncoding("UTF-8") + .setCachingEnabled(false) + .setIndexPage(config.indexPage) + .setIncludeHidden(config.includeHidden) + .setEnableRangeSupport(config.enableRangeSupport); handlers.add(new Handler<>() { @Override public void handle(RoutingContext ctx) { @@ -67,7 +70,20 @@ public void handle(RoutingContext ctx) { } if (!knownPaths.isEmpty()) { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); - StaticHandler staticHandler = StaticHandler.create(META_INF_RESOURCES).setDefaultContentEncoding("UTF-8"); + StaticHandler staticHandler = StaticHandler.create(META_INF_RESOURCES) + .setDefaultContentEncoding("UTF-8") + .setCachingEnabled(config.cachingEnabled) + .setIndexPage(config.indexPage) + .setIncludeHidden(config.includeHidden) + .setEnableRangeSupport(config.enableRangeSupport) + .setMaxCacheSize(config.maxCacheSize) + .setCacheEntryTimeout(config.cacheEntryTimeout.toMillis()) + .setMaxAgeSeconds(config.maxAge.toSeconds()); + // normalize index page like StaticHandler because its not expose + // TODO: create a converter to normalize filename in config.indexPage? + final String indexPage = (config.indexPage.charAt(0) == '/') + ? config.indexPage.substring(1) + : config.indexPage; handlers.add(new Handler<>() { @Override public void handle(RoutingContext ctx) { @@ -75,7 +91,8 @@ public void handle(RoutingContext ctx) { : ctx.normalizedPath().substring( // let's be extra careful here in case Vert.x normalizes the mount points at some point ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length()); - if (knownPaths.contains(rel)) { + // check effective path, otherwise the index page when path ends with '/' + if (knownPaths.contains(rel) || (rel.endsWith("/") && knownPaths.contains(rel.concat(indexPage)))) { compressIfNeeded(ctx, rel); staticHandler.handle(ctx); } else {