Skip to content

Commit ad6c51d

Browse files
authored
Merge pull request #45835 from ia3andy/fix-decoding-static
Fix static file path decoding in vertx-http
2 parents ff6047f + 7899c9d commit ad6c51d

File tree

9 files changed

+219
-112
lines changed

9 files changed

+219
-112
lines changed

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.vertx.http;
22

3+
import java.io.IOException;
4+
35
import org.hamcrest.Matchers;
46
import org.junit.jupiter.api.Test;
57

@@ -49,4 +51,42 @@ protected void assertEncodedResponse(String path) {
4951
.statusCode(200);
5052
}
5153

54+
@Test
55+
public void shouldGetFileWithSpecialCharacters() throws IOException {
56+
RestAssured.get("/l'équipe.pdf")
57+
.then()
58+
.header("Content-Type", Matchers.is("application/pdf"))
59+
.statusCode(200);
60+
}
61+
62+
@Test
63+
public void shouldGetFileWithSpaces() throws IOException {
64+
RestAssured.get("/static file.txt")
65+
.then()
66+
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
67+
.statusCode(200);
68+
}
69+
70+
@Test
71+
public void shouldGetFileWithSpacesAndQuery() throws IOException {
72+
RestAssured.get("/static file.txt?foo=bar")
73+
.then()
74+
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
75+
.statusCode(200);
76+
}
77+
78+
@Test
79+
public void shouldWorkWithEncodedSlash() throws IOException {
80+
RestAssured.given().urlEncodingEnabled(false).get("/dir%2Ffile.txt")
81+
.then()
82+
.statusCode(200);
83+
}
84+
85+
@Test
86+
public void shouldWorkWithDoubleDot() throws IOException {
87+
RestAssured.given().urlEncodingEnabled(false).get("/hello/../static-file.html")
88+
.then()
89+
.statusCode(200);
90+
}
91+
5292
}

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticFileResourcesTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public void accept(BuildChainBuilder buildChainBuilder) {
3636
@Override
3737
public void execute(BuildContext context) {
3838
final Path file = resolveResource("/static-file.html");
39+
context.produce(new GeneratedStaticResourceBuildItem("/static file.txt", file));
40+
context.produce(new GeneratedStaticResourceBuildItem("/l'équipe.pdf", file));
3941
context.produce(new GeneratedStaticResourceBuildItem(
4042
"/default.html", file));
4143
context.produce(new GeneratedStaticResourceBuildItem("/hello-from-generated-static-resource.html",
@@ -89,6 +91,44 @@ public void shouldGetHiddenFiles() {
8991
.statusCode(200);
9092
}
9193

94+
@Test
95+
public void shouldGetFileWithSpecialCharacters() throws IOException {
96+
RestAssured.get("/l'équipe.pdf")
97+
.then()
98+
.header("Content-Type", Matchers.is("application/pdf"))
99+
.statusCode(200);
100+
}
101+
102+
@Test
103+
public void shouldGetFileWithSpaces() throws IOException {
104+
RestAssured.get("/static file.txt")
105+
.then()
106+
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
107+
.statusCode(200);
108+
}
109+
110+
@Test
111+
public void shouldGetFileWithSpacesAndQuery() throws IOException {
112+
RestAssured.get("/static file.txt?foo=bar")
113+
.then()
114+
.header("Content-Type", Matchers.is("text/plain;charset=UTF-8"))
115+
.statusCode(200);
116+
}
117+
118+
@Test
119+
public void shouldWorkWithEncodedSlash() throws IOException {
120+
RestAssured.given().urlEncodingEnabled(false).get("/quarkus-openapi-generator%2Fdefault.html")
121+
.then()
122+
.statusCode(200);
123+
}
124+
125+
@Test
126+
public void shouldWorkWithDoubleDot() throws IOException {
127+
RestAssured.given().urlEncodingEnabled(false).get("/hello/../static-file.html")
128+
.then()
129+
.statusCode(200);
130+
}
131+
92132
@Test
93133
public void shouldGetTheIndexPageCorrectly() throws IOException {
94134
final String result = Files.readString(resolveResource("/static-file.html"));

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public class StaticResourcesTest extends AbstractStaticResourcesTest {
1212
.withApplicationRoot((jar) -> jar
1313
.add(new StringAsset("quarkus.http.enable-compression=true\n"),
1414
"application.properties")
15+
.addAsResource("static-file.html", "META-INF/resources/dir/file.txt")
16+
.addAsResource("static-file.html", "META-INF/resources/l'équipe.pdf")
17+
.addAsResource("static-file.html", "META-INF/resources/static file.txt")
1518
.addAsResource("static-file.html", "META-INF/resources/static-file.html")
1619
.addAsResource("static-file.html", "META-INF/resources/.hidden-file.html")
1720
.addAsResource("static-file.html", "META-INF/resources/index.html")

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public class StaticResourcesDevModeTest extends AbstractStaticResourcesTest {
1616
.withApplicationRoot((jar) -> jar
1717
.add(new StringAsset("quarkus.http.enable-compression=true\n"),
1818
"application.properties")
19+
.addAsResource("static-file.html", "META-INF/resources/dir/file.txt")
20+
.addAsResource("static-file.html", "META-INF/resources/l'équipe.pdf")
21+
.addAsResource("static-file.html", "META-INF/resources/static file.txt")
1922
.addAsResource("static-file.html", "META-INF/resources/static-file.html")
2023
.addAsResource("static-file.html", "META-INF/resources/.hidden-file.html")
2124
.addAsResource("static-file.html", "META-INF/resources/index.html")

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/GeneratedStaticResourcesRecorder.java

+3-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public class GeneratedStaticResourcesRecorder {
1919
public static final String META_INF_RESOURCES = "META-INF/resources";
2020
private final RuntimeValue<HttpConfiguration> httpConfiguration;
2121
private final HttpBuildTimeConfig httpBuildTimeConfig;
22-
private Set<String> compressMediaTypes = Set.of();
2322

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

33-
if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
34-
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
35-
}
3632
StaticResourcesConfig config = httpConfiguration.getValue().staticResources;
3733

3834
DevClasspathStaticHandlerOptions options = new DevClasspathStaticHandlerOptions.Builder()
3935
.indexPage(config.indexPage)
40-
.enableCompression(httpBuildTimeConfig.enableCompression)
41-
.compressMediaTypes(compressMediaTypes)
42-
.defaultEncoding(config.contentEncoding).build();
36+
.httpBuildTimeConfig(httpBuildTimeConfig)
37+
.defaultEncoding(config.contentEncoding)
38+
.build();
4339
return new DevStaticHandler(generatedClasspathResources,
4440
generatedFilesResources,
4541
options);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.quarkus.vertx.http.runtime;
2+
3+
import java.net.URI;
4+
import java.util.Set;
5+
6+
import io.vertx.core.http.HttpHeaders;
7+
import io.vertx.core.http.impl.MimeMapping;
8+
import io.vertx.ext.web.RoutingContext;
9+
import io.vertx.ext.web.handler.StaticHandler;
10+
11+
public final class RoutingUtils {
12+
13+
private RoutingUtils() throws IllegalAccessException {
14+
throw new IllegalAccessException("Avoid direct instantiation");
15+
}
16+
17+
/**
18+
* Get the normalized and decoded path:
19+
* - normalize based on RFC3986
20+
* - convert % encoded characters to their non encoded form (using {@link java.net.URI})
21+
* - invalid if the path contains '?' (query section of the path)
22+
*
23+
* @param ctx the RoutingContext
24+
* @return the normalized and decoded path or null if not valid
25+
*/
26+
public static String getNormalizedAndDecodedPath(RoutingContext ctx) {
27+
String normalizedPath = ctx.normalizedPath();
28+
if (normalizedPath.contains("?")) {
29+
return null;
30+
}
31+
return URI.create(normalizedPath).getPath();
32+
}
33+
34+
/**
35+
* Normalize and decode the path then strip the mount point from it
36+
*
37+
* @param ctx the RoutingContext
38+
* @return the normalized and decoded path without the mount point or null if not valid
39+
*/
40+
public static String resolvePath(RoutingContext ctx) {
41+
String path = getNormalizedAndDecodedPath(ctx);
42+
if (path == null) {
43+
return null;
44+
}
45+
return (ctx.mountPoint() == null) ? path
46+
: path.substring(
47+
// let's be extra careful here in case Vert.x normalizes the mount points at
48+
// some point
49+
ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length());
50+
}
51+
52+
/**
53+
* Enabled compression by removing CONTENT_ENCODING header as specified in Vert.x when the media-type should be compressed
54+
* and config enable compression.
55+
*
56+
* @param config
57+
* @param compressMediaTypes
58+
* @param ctx
59+
* @param path
60+
*/
61+
public static void compressIfNeeded(HttpBuildTimeConfig config, Set<String> compressMediaTypes, RoutingContext ctx,
62+
String path) {
63+
if (config.enableCompression && isCompressed(compressMediaTypes, path)) {
64+
// VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if compression is enabled.
65+
// Handlers can remove the "Content-Encoding: identity" header to enable compression.
66+
ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING);
67+
}
68+
}
69+
70+
private static boolean isCompressed(Set<String> compressMediaTypes, String path) {
71+
if (compressMediaTypes.isEmpty()) {
72+
return false;
73+
}
74+
final String resourcePath = path.endsWith("/") ? path + StaticHandler.DEFAULT_INDEX_PAGE : path;
75+
final String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
76+
return contentType != null && compressMediaTypes.contains(contentType);
77+
}
78+
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java

+22-29
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package io.quarkus.vertx.http.runtime;
22

3+
import static io.quarkus.vertx.http.runtime.RoutingUtils.*;
4+
35
import java.nio.file.Path;
46
import java.util.ArrayList;
57
import java.util.List;
68
import java.util.Set;
79
import java.util.function.Consumer;
810

11+
import io.netty.handler.codec.http.HttpResponseStatus;
912
import io.quarkus.runtime.RuntimeValue;
1013
import io.quarkus.runtime.annotations.Recorder;
1114
import io.vertx.core.Handler;
12-
import io.vertx.core.http.HttpHeaders;
1315
import io.vertx.core.http.HttpMethod;
14-
import io.vertx.core.http.impl.MimeMapping;
1516
import io.vertx.ext.web.Route;
1617
import io.vertx.ext.web.RoutingContext;
1718
import io.vertx.ext.web.handler.FileSystemAccess;
@@ -26,22 +27,25 @@ public class StaticResourcesRecorder {
2627

2728
final RuntimeValue<HttpConfiguration> httpConfiguration;
2829
final HttpBuildTimeConfig httpBuildTimeConfig;
29-
private Set<String> compressMediaTypes = Set.of();
30+
private final Set<String> compressMediaTypes;
3031

3132
public StaticResourcesRecorder(RuntimeValue<HttpConfiguration> httpConfiguration,
3233
HttpBuildTimeConfig httpBuildTimeConfig) {
3334
this.httpConfiguration = httpConfiguration;
3435
this.httpBuildTimeConfig = httpBuildTimeConfig;
36+
if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
37+
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
38+
} else {
39+
this.compressMediaTypes = Set.of();
40+
}
3541
}
3642

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

4147
public Consumer<Route> start(Set<String> knownPaths) {
42-
if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) {
43-
this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get());
44-
}
48+
4549
List<Handler<RoutingContext>> handlers = new ArrayList<>();
4650
StaticResourcesConfig config = httpConfiguration.getValue().staticResources;
4751

@@ -58,7 +62,12 @@ public Consumer<Route> start(Set<String> knownPaths) {
5862
@Override
5963
public void handle(RoutingContext ctx) {
6064
try {
61-
compressIfNeeded(ctx, ctx.normalizedPath());
65+
String path = getNormalizedAndDecodedPath(ctx);
66+
if (path == null) {
67+
ctx.fail(HttpResponseStatus.BAD_REQUEST.code());
68+
return;
69+
}
70+
compressIfNeeded(httpBuildTimeConfig, compressMediaTypes, ctx, path);
6271
staticHandler.handle(ctx);
6372
} catch (Exception e) {
6473
// on Windows, the drive in file path screws up cache lookup
@@ -88,13 +97,14 @@ public void handle(RoutingContext ctx) {
8897
handlers.add(new Handler<>() {
8998
@Override
9099
public void handle(RoutingContext ctx) {
91-
String rel = ctx.mountPoint() == null ? ctx.normalizedPath()
92-
: ctx.normalizedPath().substring(
93-
// let's be extra careful here in case Vert.x normalizes the mount points at some point
94-
ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length());
100+
String rel = resolvePath(ctx);
101+
if (rel == null) {
102+
ctx.fail(HttpResponseStatus.BAD_REQUEST.code());
103+
return;
104+
}
95105
// check effective path, otherwise the index page when path ends with '/'
96106
if (knownPaths.contains(rel) || (rel.endsWith("/") && knownPaths.contains(rel.concat(indexPage)))) {
97-
compressIfNeeded(ctx, rel);
107+
compressIfNeeded(httpBuildTimeConfig, compressMediaTypes, ctx, rel);
98108
staticHandler.handle(ctx);
99109
} else {
100110
// make sure we don't lose the correct TCCL to Vert.x...
@@ -121,21 +131,4 @@ public void accept(Route route) {
121131
};
122132
}
123133

124-
private void compressIfNeeded(RoutingContext ctx, String path) {
125-
if (httpBuildTimeConfig.enableCompression && isCompressed(path)) {
126-
// VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if compression is enabled.
127-
// Handlers can remove the "Content-Encoding: identity" header to enable compression.
128-
ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING);
129-
}
130-
}
131-
132-
private boolean isCompressed(String path) {
133-
if (compressMediaTypes.isEmpty()) {
134-
return false;
135-
}
136-
final String resourcePath = path.endsWith("/") ? path + StaticHandler.DEFAULT_INDEX_PAGE : path;
137-
final String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
138-
return contentType != null && compressMediaTypes.contains(contentType);
139-
}
140-
141134
}

0 commit comments

Comments
 (0)