Skip to content

Commit f3da2eb

Browse files
committed
[UNDERTOW-2191] - add predicate to alter directory listing per request basis
1 parent 29effcd commit f3da2eb

File tree

6 files changed

+300
-6
lines changed

6 files changed

+300
-6
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2023 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package io.undertow.server.handlers.resource;
19+
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
import io.undertow.server.HandlerWrapper;
26+
import io.undertow.server.HttpHandler;
27+
import io.undertow.server.HttpServerExchange;
28+
import io.undertow.server.handlers.builder.HandlerBuilder;
29+
import io.undertow.util.AttachmentKey;
30+
31+
/**
32+
* @author baranowb
33+
* Handler which enables/disabled per exchange listing.
34+
*/
35+
public class DirectoryListingEnableHandler implements HttpHandler {
36+
37+
private static final AttachmentKey<Boolean> ENABLE_DIRECTORY_LISTING = AttachmentKey.create(Boolean.class);
38+
/**
39+
* Handler that is called if no resource is found
40+
*/
41+
private final HttpHandler next;
42+
private final boolean allowsListing;
43+
44+
public DirectoryListingEnableHandler(HttpHandler next, boolean allowsListing) {
45+
super();
46+
this.next = next;
47+
this.allowsListing = allowsListing;
48+
}
49+
50+
@Override
51+
public void handleRequest(HttpServerExchange exchange) throws Exception {
52+
exchange.putAttachment(ENABLE_DIRECTORY_LISTING, this.allowsListing);
53+
if (this.next != null) {
54+
this.next.handleRequest(exchange);
55+
}
56+
}
57+
58+
public static boolean hasEnablerAttached(final HttpServerExchange exchange) {
59+
return exchange.getAttachment(ENABLE_DIRECTORY_LISTING) != null;
60+
}
61+
62+
public static boolean isDirectoryListingEnabled(final HttpServerExchange exchange) {
63+
return exchange.getAttachment(ENABLE_DIRECTORY_LISTING);
64+
}
65+
66+
public static class Builder implements HandlerBuilder {
67+
68+
@Override
69+
public String name() {
70+
return "directory-listing";
71+
}
72+
73+
@Override
74+
public Map<String, Class<?>> parameters() {
75+
Map<String, Class<?>> params = new HashMap<>();
76+
params.put("allow-listing", boolean.class);
77+
return params;
78+
}
79+
80+
@Override
81+
public Set<String> requiredParameters() {
82+
return Collections.singleton("allow-listing");
83+
}
84+
85+
@Override
86+
public String defaultParameter() {
87+
return "allow-listing";
88+
}
89+
90+
@Override
91+
public HandlerWrapper build(Map<String, Object> config) {
92+
return new Wrapper((Boolean) config.get("allow-listing"));
93+
}
94+
95+
}
96+
97+
private static class Wrapper implements HandlerWrapper {
98+
99+
private final boolean allowDirectoryListing;
100+
101+
private Wrapper(boolean allowDirectoryListing) {
102+
this.allowDirectoryListing = allowDirectoryListing;
103+
}
104+
105+
@Override
106+
public HttpHandler wrap(HttpHandler handler) {
107+
final DirectoryListingEnableHandler enableHandler = new DirectoryListingEnableHandler(handler,
108+
allowDirectoryListing);
109+
return enableHandler;
110+
}
111+
}
112+
113+
}

core/src/main/java/io/undertow/server/handlers/resource/ResourceHandler.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception {
165165

166166
private void serveResource(final HttpServerExchange exchange, final boolean sendContent) throws Exception {
167167

168-
if (directoryListingEnabled && DirectoryUtils.sendRequestedBlobs(exchange)) {
168+
if (isDirectoryListingEnabledForExchange(exchange) && DirectoryUtils.sendRequestedBlobs(exchange)) {
169169
return;
170170
}
171171

@@ -229,7 +229,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
229229
return;
230230
}
231231
if (indexResource == null) {
232-
if (directoryListingEnabled) {
232+
if (isDirectoryListingEnabledForExchange(exchange)) {
233233
DirectoryUtils.renderDirectoryListing(exchange, resource);
234234
return;
235235
} else {
@@ -382,6 +382,14 @@ public ResourceHandler setDirectoryListingEnabled(final boolean directoryListing
382382
return this;
383383
}
384384

385+
private boolean isDirectoryListingEnabledForExchange(final HttpServerExchange exchange) {
386+
boolean listDirectories = directoryListingEnabled;
387+
if(DirectoryListingEnableHandler.hasEnablerAttached(exchange)) {
388+
listDirectories = DirectoryListingEnableHandler.isDirectoryListingEnabled(exchange);
389+
}
390+
return listDirectories;
391+
}
392+
385393
public ResourceHandler addWelcomeFiles(String... files) {
386394
this.welcomeFiles.addAll(Arrays.asList(files));
387395
return this;

core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ io.undertow.server.handlers.HttpContinueAcceptingHandler$Builder
4343
io.undertow.server.handlers.form.EagerFormParsingHandler$Builder
4444
io.undertow.server.handlers.SameSiteCookieHandler$Builder
4545
io.undertow.server.handlers.SetErrorHandler$Builder
46+
io.undertow.server.handlers.resource.DirectoryListingEnableHandler$Builder
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2023 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package io.undertow.server.handlers;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.attribute.FileAttribute;
25+
26+
import org.apache.http.HttpResponse;
27+
import org.apache.http.client.methods.HttpGet;
28+
import org.apache.http.util.EntityUtils;
29+
import org.junit.Assert;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
33+
import io.undertow.Handlers;
34+
import io.undertow.server.HttpHandler;
35+
import io.undertow.server.HttpServerExchange;
36+
import io.undertow.server.handlers.builder.PredicatedHandlersParser;
37+
import io.undertow.testutils.DefaultServer;
38+
import io.undertow.testutils.TestHttpClient;
39+
import io.undertow.util.StatusCodes;
40+
41+
/**
42+
* Test basic resource serving via predicate handlers
43+
*
44+
* @author baranowb
45+
*
46+
*/
47+
@RunWith(DefaultServer.class)
48+
public class DirectoryListingEnablerTestCase {
49+
50+
private static final String DIR_PREFIXED = "prefix-resource-dir";
51+
private static final String FILE_NAME_LEVEL_0 = "file0";
52+
private static final String FILE_NAME_LEVEL_1 = "file1";
53+
private static final String DIR_SUB = "sub_dir";
54+
private static final String GIBBERISH = "Gibberish, what did you expect?";
55+
56+
private static final String TEST_PREFIX = "prefixToTest";
57+
private static final String HEADER_SWITCH = "SwitchHeader";
58+
59+
@Test
60+
public void testEnableOnResource() throws IOException {
61+
final PathsRetainer pathsRetainer = createTestDir(DIR_PREFIXED, false);
62+
DefaultServer.setRootHandler(Handlers.predicates(
63+
64+
PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
65+
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}",
66+
getClass().getClassLoader()),
67+
new HttpHandler() {
68+
@Override
69+
public void handleRequest(HttpServerExchange exchange) throws Exception {
70+
}
71+
}));
72+
testURLListing(pathsRetainer, false, false, StatusCodes.OK);
73+
testURLListing(pathsRetainer, true, false, StatusCodes.FORBIDDEN);
74+
testURLListing(pathsRetainer, true, true, StatusCodes.OK);
75+
}
76+
77+
@Test
78+
public void testEnableWithoutResource() throws IOException {
79+
final PathsRetainer pathsRetainer = createTestDir(DIR_PREFIXED, false);
80+
DefaultServer.setRootHandler(Handlers.predicates(
81+
82+
PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
83+
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}"+
84+
"\npath-prefix(/)-> { resource(location='" + pathsRetainer.root.toString() + "',allow-listing=false) }",
85+
getClass().getClassLoader()),
86+
new HttpHandler() {
87+
@Override
88+
public void handleRequest(HttpServerExchange exchange) throws Exception {
89+
}
90+
}));
91+
testURLListing(pathsRetainer, false, false, StatusCodes.FORBIDDEN);
92+
testURLListing(pathsRetainer, true, false, StatusCodes.FORBIDDEN);
93+
testURLListing(pathsRetainer, true, true, StatusCodes.OK);
94+
}
95+
96+
private void testURLListing(final PathsRetainer pathsRetainer, boolean includeHeader, boolean enable, int statusCode) throws IOException {
97+
98+
try(TestHttpClient client = new TestHttpClient();){
99+
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() +"/");
100+
if(includeHeader) {
101+
if(enable) {
102+
get.addHeader(HEADER_SWITCH, "enable");
103+
} else {
104+
get.addHeader(HEADER_SWITCH, "disable");
105+
}
106+
}
107+
HttpResponse result = client.execute(get);
108+
Assert.assertEquals(statusCode, result.getStatusLine().getStatusCode());
109+
if(statusCode != StatusCodes.OK) {
110+
return;
111+
}
112+
String bodyToTest = EntityUtils.toString(result.getEntity());
113+
//this is not optimal...
114+
Assert.assertTrue(bodyToTest + "\n" + pathsRetainer.sub.getFileName(), bodyToTest.contains("href='/"+pathsRetainer.sub.getFileName()+"/'>"+pathsRetainer.sub.getFileName()+"</a>"));
115+
Assert.assertTrue(bodyToTest + "\n" + pathsRetainer.rootFile.getFileName(), bodyToTest.contains("href='/"+pathsRetainer.rootFile.getFileName()+"'>"+pathsRetainer.rootFile.getFileName()+"</a>"));
116+
}
117+
}
118+
private PathsRetainer createTestDir(final String dirName, final boolean prefixDirectory) throws IOException {
119+
final FileAttribute<?>[] attribs = new FileAttribute<?>[] {};
120+
final PathsRetainer pathsRetainer = new PathsRetainer();
121+
Path dir = Files.createTempDirectory(dirName);
122+
if (prefixDirectory) {
123+
//dont use temp, as it will add random stuff
124+
//parent is already temp
125+
File f = dir.toFile();
126+
f = new File(f,TEST_PREFIX);
127+
Assert.assertTrue(f.mkdir());
128+
pathsRetainer.root = dir;
129+
dir = f.toPath();
130+
} else {
131+
pathsRetainer.root = dir;
132+
}
133+
134+
Path file = Files.createTempFile(dir, FILE_NAME_LEVEL_0,".txt", attribs);
135+
pathsRetainer.rootFile = file;
136+
writeGibberish(file);
137+
final Path subdir = Files.createTempDirectory(dir, DIR_SUB);
138+
pathsRetainer.sub = subdir;
139+
file = Files.createTempFile(subdir, FILE_NAME_LEVEL_1,".txt", attribs);
140+
pathsRetainer.subFile = file;
141+
writeGibberish(file);
142+
return pathsRetainer;
143+
}
144+
145+
private void writeGibberish(final Path p) throws IOException {
146+
Files.write(p,GIBBERISH.getBytes());
147+
}
148+
private static class PathsRetainer{
149+
private Path root;
150+
private Path rootFile;
151+
private Path sub;
152+
private Path subFile;
153+
}
154+
}

servlet/src/main/java/io/undertow/servlet/handlers/DefaultServlet.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.undertow.io.Sender;
2323
import io.undertow.server.HttpServerExchange;
2424
import io.undertow.server.handlers.resource.DefaultResourceSupplier;
25+
import io.undertow.server.handlers.resource.DirectoryListingEnableHandler;
2526
import io.undertow.server.handlers.resource.DirectoryUtils;
2627
import io.undertow.server.handlers.resource.PreCompressedResourceSupplier;
2728
import io.undertow.server.handlers.resource.RangeAwareResource;
@@ -176,7 +177,11 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res
176177
}
177178
return;
178179
} else if (resource.isDirectory()) {
179-
if (directoryListingEnabled) {
180+
boolean listDirectories = this.directoryListingEnabled;
181+
if(DirectoryListingEnableHandler.hasEnablerAttached(exchange)) {
182+
listDirectories = DirectoryListingEnableHandler.isDirectoryListingEnabled(exchange);
183+
}
184+
if (listDirectories) {
180185
if ("css".equals(req.getQueryString())) {
181186
resp.setContentType("text/css");
182187
resp.getWriter().write(DirectoryUtils.Blobs.FILE_CSS);

servlet/src/test/java/io/undertow/servlet/test/defaultservlet/DefaultServletTestCase.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
import java.util.Date;
2323
import jakarta.servlet.DispatcherType;
2424
import jakarta.servlet.ServletException;
25-
25+
import io.undertow.Handlers;
26+
import io.undertow.server.HttpHandler;
2627
import io.undertow.server.handlers.PathHandler;
28+
import io.undertow.server.handlers.builder.PredicatedHandlersParser;
2729
import io.undertow.servlet.api.DeploymentInfo;
2830
import io.undertow.servlet.api.DeploymentManager;
2931
import io.undertow.servlet.api.FilterInfo;
@@ -53,10 +55,12 @@
5355

5456
/**
5557
* @author Stuart Douglas
58+
* @author baranowb
5659
*/
5760
@RunWith(DefaultServer.class)
5861
public class DefaultServletTestCase {
5962

63+
private static final String HEADER_SWITCH = "SwitchHeader";
6064

6165
@BeforeClass
6266
public static void setup() throws ServletException {
@@ -89,8 +93,11 @@ public static void setup() throws ServletException {
8993
DeploymentManager manager = container.addDeployment(builder);
9094
manager.deploy();
9195
root.addPrefixPath(builder.getContextPath(), manager.start());
92-
93-
DefaultServer.setRootHandler(root);
96+
HttpHandler httpHandler = Handlers.predicates(
97+
PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
98+
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}",
99+
DefaultServletTestCase.class.getClassLoader()), root);
100+
DefaultServer.setRootHandler(httpHandler);
94101
}
95102

96103
@Test
@@ -319,6 +326,12 @@ public void testDirectoryListing() throws IOException {
319326
MatcherAssert.assertThat(result.getFirstHeader(Headers.CONTENT_TYPE_STRING).getValue(), CoreMatchers.startsWith("text/css"));
320327
MatcherAssert.assertThat(HttpClientUtils.readResponse(result), CoreMatchers.containsString("data:image/png;base64"));
321328
}
329+
330+
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/path");
331+
get.addHeader(HEADER_SWITCH, "disable");
332+
try (CloseableHttpResponse result = client.execute(get);) {
333+
Assert.assertEquals(StatusCodes.FORBIDDEN, result.getStatusLine().getStatusCode());
334+
}
322335
} finally {
323336
client.getConnectionManager().shutdown();
324337
}

0 commit comments

Comments
 (0)