Skip to content

Commit

Permalink
Fix static file decoding in vertx-http
Browse files Browse the repository at this point in the history
  • Loading branch information
ia3andy committed Jan 24, 2025
1 parent ea4fb8a commit 7899c9d
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.vertx.http;

import java.io.IOException;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -49,4 +51,42 @@ protected void assertEncodedResponse(String path) {
.statusCode(200);
}

@Test
public void shouldGetFileWithSpecialCharacters() throws IOException {
RestAssured.get("/l'équipe.pdf")
.then()
.header("Content-Type", Matchers.is("application/pdf"))
.statusCode(200);
}

@Test
public void shouldGetFileWithSpaces() throws IOException {
RestAssured.get("/static file.txt")
.then()
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
.statusCode(200);
}

@Test
public void shouldGetFileWithSpacesAndQuery() throws IOException {
RestAssured.get("/static file.txt?foo=bar")
.then()
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
.statusCode(200);
}

@Test
public void shouldWorkWithEncodedSlash() throws IOException {
RestAssured.given().urlEncodingEnabled(false).get("/dir%2Ffile.txt")
.then()
.statusCode(200);
}

@Test
public void shouldWorkWithDoubleDot() throws IOException {
RestAssured.given().urlEncodingEnabled(false).get("/hello/../static-file.html")
.then()
.statusCode(200);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public void accept(BuildChainBuilder buildChainBuilder) {
@Override
public void execute(BuildContext context) {
final Path file = resolveResource("/static-file.html");
context.produce(new GeneratedStaticResourceBuildItem("/static file.txt", file));
context.produce(new GeneratedStaticResourceBuildItem("/l'équipe.pdf", file));
context.produce(new GeneratedStaticResourceBuildItem(
"/default.html", file));
context.produce(new GeneratedStaticResourceBuildItem("/hello-from-generated-static-resource.html",
Expand Down Expand Up @@ -89,6 +91,44 @@ public void shouldGetHiddenFiles() {
.statusCode(200);
}

@Test
public void shouldGetFileWithSpecialCharacters() throws IOException {
RestAssured.get("/l'équipe.pdf")
.then()
.header("Content-Type", Matchers.is("application/pdf"))
.statusCode(200);
}

@Test
public void shouldGetFileWithSpaces() throws IOException {
RestAssured.get("/static file.txt")
.then()
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
.statusCode(200);
}

@Test
public void shouldGetFileWithSpacesAndQuery() throws IOException {
RestAssured.get("/static file.txt?foo=bar")
.then()
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
.statusCode(200);
}

@Test
public void shouldWorkWithEncodedSlash() throws IOException {
RestAssured.given().urlEncodingEnabled(false).get("/quarkus-openapi-generator%2Fdefault.html")
.then()
.statusCode(200);
}

@Test
public void shouldWorkWithDoubleDot() throws IOException {
RestAssured.given().urlEncodingEnabled(false).get("/hello/../static-file.html")
.then()
.statusCode(200);
}

@Test
public void shouldGetTheIndexPageCorrectly() throws IOException {
final String result = Files.readString(resolveResource("/static-file.html"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public class StaticResourcesTest extends AbstractStaticResourcesTest {
.withApplicationRoot((jar) -> jar
.add(new StringAsset("quarkus.http.enable-compression=true\n"),
"application.properties")
.addAsResource("static-file.html", "META-INF/resources/dir/file.txt")
.addAsResource("static-file.html", "META-INF/resources/l'équipe.pdf")
.addAsResource("static-file.html", "META-INF/resources/static file.txt")
.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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public class StaticResourcesDevModeTest extends AbstractStaticResourcesTest {
.withApplicationRoot((jar) -> jar
.add(new StringAsset("quarkus.http.enable-compression=true\n"),
"application.properties")
.addAsResource("static-file.html", "META-INF/resources/dir/file.txt")
.addAsResource("static-file.html", "META-INF/resources/l'équipe.pdf")
.addAsResource("static-file.html", "META-INF/resources/static file.txt")
.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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public class GeneratedStaticResourcesRecorder {
public static final String META_INF_RESOURCES = "META-INF/resources";
private final RuntimeValue<HttpConfiguration> httpConfiguration;
private final HttpBuildTimeConfig httpBuildTimeConfig;
private Set<String> compressMediaTypes = Set.of();

public GeneratedStaticResourcesRecorder(RuntimeValue<HttpConfiguration> httpConfiguration,
HttpBuildTimeConfig httpBuildTimeConfig) {
Expand All @@ -30,16 +29,13 @@ public GeneratedStaticResourcesRecorder(RuntimeValue<HttpConfiguration> httpConf
public Handler<RoutingContext> createHandler(Set<String> generatedClasspathResources,
Map<String, String> generatedFilesResources) {

if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
}
StaticResourcesConfig config = httpConfiguration.getValue().staticResources;

DevClasspathStaticHandlerOptions options = new DevClasspathStaticHandlerOptions.Builder()
.indexPage(config.indexPage)
.enableCompression(httpBuildTimeConfig.enableCompression)
.compressMediaTypes(compressMediaTypes)
.defaultEncoding(config.contentEncoding).build();
.httpBuildTimeConfig(httpBuildTimeConfig)
.defaultEncoding(config.contentEncoding)
.build();
return new DevStaticHandler(generatedClasspathResources,
generatedFilesResources,
options);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.vertx.http.runtime;

import java.net.URI;
import java.util.Set;

import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.impl.MimeMapping;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.StaticHandler;

public final class RoutingUtils {

private RoutingUtils() throws IllegalAccessException {
throw new IllegalAccessException("Avoid direct instantiation");
}

/**
* Get the normalized and decoded path:
* - normalize based on RFC3986
* - convert % encoded characters to their non encoded form (using {@link java.net.URI})
* - invalid if the path contains '?' (query section of the path)
*
* @param ctx the RoutingContext
* @return the normalized and decoded path or null if not valid
*/
public static String getNormalizedAndDecodedPath(RoutingContext ctx) {
String normalizedPath = ctx.normalizedPath();
if (normalizedPath.contains("?")) {
return null;
}
return URI.create(normalizedPath).getPath();
}

/**
* Normalize and decode the path then strip the mount point from it
*
* @param ctx the RoutingContext
* @return the normalized and decoded path without the mount point or null if not valid
*/
public static String resolvePath(RoutingContext ctx) {
String path = getNormalizedAndDecodedPath(ctx);
if (path == null) {
return null;
}
return (ctx.mountPoint() == null) ? path
: path.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());
}

/**
* Enabled compression by removing CONTENT_ENCODING header as specified in Vert.x when the media-type should be compressed
* and config enable compression.
*
* @param config
* @param compressMediaTypes
* @param ctx
* @param path
*/
public static void compressIfNeeded(HttpBuildTimeConfig config, Set<String> compressMediaTypes, RoutingContext ctx,
String path) {
if (config.enableCompression && isCompressed(compressMediaTypes, path)) {
// VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if compression is enabled.
// Handlers can remove the "Content-Encoding: identity" header to enable compression.
ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING);
}
}

private static boolean isCompressed(Set<String> compressMediaTypes, String path) {
if (compressMediaTypes.isEmpty()) {
return false;
}
final String resourcePath = path.endsWith("/") ? path + StaticHandler.DEFAULT_INDEX_PAGE : path;
final String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
return contentType != null && compressMediaTypes.contains(contentType);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package io.quarkus.vertx.http.runtime;

import static io.quarkus.vertx.http.runtime.RoutingUtils.*;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
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;
Expand All @@ -26,22 +27,25 @@ public class StaticResourcesRecorder {

final RuntimeValue<HttpConfiguration> httpConfiguration;
final HttpBuildTimeConfig httpBuildTimeConfig;
private Set<String> compressMediaTypes = Set.of();
private final Set<String> compressMediaTypes;

public StaticResourcesRecorder(RuntimeValue<HttpConfiguration> httpConfiguration,
HttpBuildTimeConfig httpBuildTimeConfig) {
this.httpConfiguration = httpConfiguration;
this.httpBuildTimeConfig = httpBuildTimeConfig;
if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
} else {
this.compressMediaTypes = Set.of();
}
}

public static void setHotDeploymentResources(List<Path> resources) {
hotDeploymentResourcePaths = resources;
}

public Consumer<Route> start(Set<String> knownPaths) {
if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
}

List<Handler<RoutingContext>> handlers = new ArrayList<>();
StaticResourcesConfig config = httpConfiguration.getValue().staticResources;

Expand All @@ -58,7 +62,12 @@ public Consumer<Route> start(Set<String> knownPaths) {
@Override
public void handle(RoutingContext ctx) {
try {
compressIfNeeded(ctx, ctx.normalizedPath());
String path = getNormalizedAndDecodedPath(ctx);
if (path == null) {
ctx.fail(HttpResponseStatus.BAD_REQUEST.code());
return;
}
compressIfNeeded(httpBuildTimeConfig, compressMediaTypes, ctx, path);
staticHandler.handle(ctx);
} catch (Exception e) {
// on Windows, the drive in file path screws up cache lookup
Expand Down Expand Up @@ -88,13 +97,14 @@ public void handle(RoutingContext ctx) {
handlers.add(new Handler<>() {
@Override
public void handle(RoutingContext ctx) {
String rel = ctx.mountPoint() == null ? ctx.normalizedPath()
: 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());
String rel = resolvePath(ctx);
if (rel == null) {
ctx.fail(HttpResponseStatus.BAD_REQUEST.code());
return;
}
// 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);
compressIfNeeded(httpBuildTimeConfig, compressMediaTypes, ctx, rel);
staticHandler.handle(ctx);
} else {
// make sure we don't lose the correct TCCL to Vert.x...
Expand All @@ -121,21 +131,4 @@ public void accept(Route route) {
};
}

private void compressIfNeeded(RoutingContext ctx, String path) {
if (httpBuildTimeConfig.enableCompression && isCompressed(path)) {
// VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if compression is enabled.
// Handlers can remove the "Content-Encoding: identity" header to enable compression.
ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING);
}
}

private boolean isCompressed(String path) {
if (compressMediaTypes.isEmpty()) {
return false;
}
final String resourcePath = path.endsWith("/") ? path + StaticHandler.DEFAULT_INDEX_PAGE : path;
final String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
return contentType != null && compressMediaTypes.contains(contentType);
}

}
Loading

0 comments on commit 7899c9d

Please sign in to comment.