Skip to content

Commit

Permalink
[UNDERTOW-2191] - add predicate to alter directory listing per reques…
Browse files Browse the repository at this point in the history
…t basis
  • Loading branch information
baranowb committed Jul 3, 2023
1 parent 29effcd commit f3da2eb
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2023 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.server.handlers.resource;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.builder.HandlerBuilder;
import io.undertow.util.AttachmentKey;

/**
* @author baranowb
* Handler which enables/disabled per exchange listing.
*/
public class DirectoryListingEnableHandler implements HttpHandler {

private static final AttachmentKey<Boolean> ENABLE_DIRECTORY_LISTING = AttachmentKey.create(Boolean.class);
/**
* Handler that is called if no resource is found
*/
private final HttpHandler next;
private final boolean allowsListing;

public DirectoryListingEnableHandler(HttpHandler next, boolean allowsListing) {
super();
this.next = next;
this.allowsListing = allowsListing;
}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.putAttachment(ENABLE_DIRECTORY_LISTING, this.allowsListing);
if (this.next != null) {
this.next.handleRequest(exchange);
}
}

public static boolean hasEnablerAttached(final HttpServerExchange exchange) {
return exchange.getAttachment(ENABLE_DIRECTORY_LISTING) != null;
}

public static boolean isDirectoryListingEnabled(final HttpServerExchange exchange) {
return exchange.getAttachment(ENABLE_DIRECTORY_LISTING);
}

public static class Builder implements HandlerBuilder {

@Override
public String name() {
return "directory-listing";
}

@Override
public Map<String, Class<?>> parameters() {
Map<String, Class<?>> params = new HashMap<>();
params.put("allow-listing", boolean.class);
return params;
}

@Override
public Set<String> requiredParameters() {
return Collections.singleton("allow-listing");
}

@Override
public String defaultParameter() {
return "allow-listing";
}

@Override
public HandlerWrapper build(Map<String, Object> config) {
return new Wrapper((Boolean) config.get("allow-listing"));
}

}

private static class Wrapper implements HandlerWrapper {

private final boolean allowDirectoryListing;

private Wrapper(boolean allowDirectoryListing) {
this.allowDirectoryListing = allowDirectoryListing;
}

@Override
public HttpHandler wrap(HttpHandler handler) {
final DirectoryListingEnableHandler enableHandler = new DirectoryListingEnableHandler(handler,
allowDirectoryListing);
return enableHandler;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception {

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

if (directoryListingEnabled && DirectoryUtils.sendRequestedBlobs(exchange)) {
if (isDirectoryListingEnabledForExchange(exchange) && DirectoryUtils.sendRequestedBlobs(exchange)) {
return;
}

Expand Down Expand Up @@ -229,7 +229,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
return;
}
if (indexResource == null) {
if (directoryListingEnabled) {
if (isDirectoryListingEnabledForExchange(exchange)) {
DirectoryUtils.renderDirectoryListing(exchange, resource);
return;
} else {
Expand Down Expand Up @@ -382,6 +382,14 @@ public ResourceHandler setDirectoryListingEnabled(final boolean directoryListing
return this;
}

private boolean isDirectoryListingEnabledForExchange(final HttpServerExchange exchange) {
boolean listDirectories = directoryListingEnabled;
if(DirectoryListingEnableHandler.hasEnablerAttached(exchange)) {
listDirectories = DirectoryListingEnableHandler.isDirectoryListingEnabled(exchange);
}
return listDirectories;
}

public ResourceHandler addWelcomeFiles(String... files) {
this.welcomeFiles.addAll(Arrays.asList(files));
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ io.undertow.server.handlers.HttpContinueAcceptingHandler$Builder
io.undertow.server.handlers.form.EagerFormParsingHandler$Builder
io.undertow.server.handlers.SameSiteCookieHandler$Builder
io.undertow.server.handlers.SetErrorHandler$Builder
io.undertow.server.handlers.resource.DirectoryListingEnableHandler$Builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2023 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.server.handlers;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.builder.PredicatedHandlersParser;
import io.undertow.testutils.DefaultServer;
import io.undertow.testutils.TestHttpClient;
import io.undertow.util.StatusCodes;

/**
* Test basic resource serving via predicate handlers
*
* @author baranowb
*
*/
@RunWith(DefaultServer.class)
public class DirectoryListingEnablerTestCase {

private static final String DIR_PREFIXED = "prefix-resource-dir";
private static final String FILE_NAME_LEVEL_0 = "file0";
private static final String FILE_NAME_LEVEL_1 = "file1";
private static final String DIR_SUB = "sub_dir";
private static final String GIBBERISH = "Gibberish, what did you expect?";

private static final String TEST_PREFIX = "prefixToTest";
private static final String HEADER_SWITCH = "SwitchHeader";

@Test
public void testEnableOnResource() throws IOException {
final PathsRetainer pathsRetainer = createTestDir(DIR_PREFIXED, false);
DefaultServer.setRootHandler(Handlers.predicates(

PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}",
getClass().getClassLoader()),
new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
}
}));
testURLListing(pathsRetainer, false, false, StatusCodes.OK);
testURLListing(pathsRetainer, true, false, StatusCodes.FORBIDDEN);
testURLListing(pathsRetainer, true, true, StatusCodes.OK);
}

@Test
public void testEnableWithoutResource() throws IOException {
final PathsRetainer pathsRetainer = createTestDir(DIR_PREFIXED, false);
DefaultServer.setRootHandler(Handlers.predicates(

PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}"+
"\npath-prefix(/)-> { resource(location='" + pathsRetainer.root.toString() + "',allow-listing=false) }",
getClass().getClassLoader()),
new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
}
}));
testURLListing(pathsRetainer, false, false, StatusCodes.FORBIDDEN);
testURLListing(pathsRetainer, true, false, StatusCodes.FORBIDDEN);
testURLListing(pathsRetainer, true, true, StatusCodes.OK);
}

private void testURLListing(final PathsRetainer pathsRetainer, boolean includeHeader, boolean enable, int statusCode) throws IOException {

try(TestHttpClient client = new TestHttpClient();){
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() +"/");
if(includeHeader) {
if(enable) {
get.addHeader(HEADER_SWITCH, "enable");
} else {
get.addHeader(HEADER_SWITCH, "disable");
}
}
HttpResponse result = client.execute(get);
Assert.assertEquals(statusCode, result.getStatusLine().getStatusCode());
if(statusCode != StatusCodes.OK) {
return;
}
String bodyToTest = EntityUtils.toString(result.getEntity());
//this is not optimal...
Assert.assertTrue(bodyToTest + "\n" + pathsRetainer.sub.getFileName(), bodyToTest.contains("href='/"+pathsRetainer.sub.getFileName()+"/'>"+pathsRetainer.sub.getFileName()+"</a>"));
Assert.assertTrue(bodyToTest + "\n" + pathsRetainer.rootFile.getFileName(), bodyToTest.contains("href='/"+pathsRetainer.rootFile.getFileName()+"'>"+pathsRetainer.rootFile.getFileName()+"</a>"));
}
}
private PathsRetainer createTestDir(final String dirName, final boolean prefixDirectory) throws IOException {
final FileAttribute<?>[] attribs = new FileAttribute<?>[] {};
final PathsRetainer pathsRetainer = new PathsRetainer();
Path dir = Files.createTempDirectory(dirName);
if (prefixDirectory) {
//dont use temp, as it will add random stuff
//parent is already temp
File f = dir.toFile();
f = new File(f,TEST_PREFIX);
Assert.assertTrue(f.mkdir());
pathsRetainer.root = dir;
dir = f.toPath();
} else {
pathsRetainer.root = dir;
}

Path file = Files.createTempFile(dir, FILE_NAME_LEVEL_0,".txt", attribs);
pathsRetainer.rootFile = file;
writeGibberish(file);
final Path subdir = Files.createTempDirectory(dir, DIR_SUB);
pathsRetainer.sub = subdir;
file = Files.createTempFile(subdir, FILE_NAME_LEVEL_1,".txt", attribs);
pathsRetainer.subFile = file;
writeGibberish(file);
return pathsRetainer;
}

private void writeGibberish(final Path p) throws IOException {
Files.write(p,GIBBERISH.getBytes());
}
private static class PathsRetainer{
private Path root;
private Path rootFile;
private Path sub;
private Path subFile;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.resource.DefaultResourceSupplier;
import io.undertow.server.handlers.resource.DirectoryListingEnableHandler;
import io.undertow.server.handlers.resource.DirectoryUtils;
import io.undertow.server.handlers.resource.PreCompressedResourceSupplier;
import io.undertow.server.handlers.resource.RangeAwareResource;
Expand Down Expand Up @@ -176,7 +177,11 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res
}
return;
} else if (resource.isDirectory()) {
if (directoryListingEnabled) {
boolean listDirectories = this.directoryListingEnabled;
if(DirectoryListingEnableHandler.hasEnablerAttached(exchange)) {
listDirectories = DirectoryListingEnableHandler.isDirectoryListingEnabled(exchange);
}
if (listDirectories) {
if ("css".equals(req.getQueryString())) {
resp.setContentType("text/css");
resp.getWriter().write(DirectoryUtils.Blobs.FILE_CSS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import java.util.Date;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.builder.PredicatedHandlersParser;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.FilterInfo;
Expand Down Expand Up @@ -53,10 +55,12 @@

/**
* @author Stuart Douglas
* @author baranowb
*/
@RunWith(DefaultServer.class)
public class DefaultServletTestCase {

private static final String HEADER_SWITCH = "SwitchHeader";

@BeforeClass
public static void setup() throws ServletException {
Expand Down Expand Up @@ -89,8 +93,11 @@ public static void setup() throws ServletException {
DeploymentManager manager = container.addDeployment(builder);
manager.deploy();
root.addPrefixPath(builder.getContextPath(), manager.start());

DefaultServer.setRootHandler(root);
HttpHandler httpHandler = Handlers.predicates(
PredicatedHandlersParser.parse("contains[value=%{i,"+HEADER_SWITCH+"},search='enable'] -> { directory-listing(allow-listing=true)}"
+ "\ncontains[value=%{i,"+HEADER_SWITCH+"},search='disable'] -> { directory-listing(allow-listing=false)}",
DefaultServletTestCase.class.getClassLoader()), root);
DefaultServer.setRootHandler(httpHandler);
}

@Test
Expand Down Expand Up @@ -319,6 +326,12 @@ public void testDirectoryListing() throws IOException {
MatcherAssert.assertThat(result.getFirstHeader(Headers.CONTENT_TYPE_STRING).getValue(), CoreMatchers.startsWith("text/css"));
MatcherAssert.assertThat(HttpClientUtils.readResponse(result), CoreMatchers.containsString("data:image/png;base64"));
}

HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/path");
get.addHeader(HEADER_SWITCH, "disable");
try (CloseableHttpResponse result = client.execute(get);) {
Assert.assertEquals(StatusCodes.FORBIDDEN, result.getStatusLine().getStatusCode());
}
} finally {
client.getConnectionManager().shutdown();
}
Expand Down

0 comments on commit f3da2eb

Please sign in to comment.