From 72c93e807da7deeddee940250902603f3ccb643a Mon Sep 17 00:00:00 2001 From: baranowb Date: Fri, 30 Jun 2023 13:07:32 +0200 Subject: [PATCH] [UNDERTOW-2191] - add predicate to alter directory listing per request basis --- .../DirectoryListingEnableHandler.java | 113 +++++++++++++ .../handlers/resource/ResourceHandler.java | 12 +- ...tow.server.handlers.builder.HandlerBuilder | 1 + .../DirectoryListingEnablerTestCase.java | 155 ++++++++++++++++++ .../servlet/handlers/DefaultServlet.java | 7 +- .../DefaultServletTestCase.java | 19 ++- 6 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/io/undertow/server/handlers/resource/DirectoryListingEnableHandler.java create mode 100644 core/src/test/java/io/undertow/server/handlers/DirectoryListingEnablerTestCase.java diff --git a/core/src/main/java/io/undertow/server/handlers/resource/DirectoryListingEnableHandler.java b/core/src/main/java/io/undertow/server/handlers/resource/DirectoryListingEnableHandler.java new file mode 100644 index 0000000000..fb3da2d2ef --- /dev/null +++ b/core/src/main/java/io/undertow/server/handlers/resource/DirectoryListingEnableHandler.java @@ -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 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> parameters() { + Map> params = new HashMap<>(); + params.put("allow-listing", boolean.class); + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("allow-listing"); + } + + @Override + public String defaultParameter() { + return "allow-listing"; + } + + @Override + public HandlerWrapper build(Map 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; + } + } + +} diff --git a/core/src/main/java/io/undertow/server/handlers/resource/ResourceHandler.java b/core/src/main/java/io/undertow/server/handlers/resource/ResourceHandler.java index ee1a65a80f..d10ed63fc6 100644 --- a/core/src/main/java/io/undertow/server/handlers/resource/ResourceHandler.java +++ b/core/src/main/java/io/undertow/server/handlers/resource/ResourceHandler.java @@ -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; } @@ -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 { @@ -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; diff --git a/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder b/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder index 0c2affdbe4..563502ff77 100644 --- a/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder +++ b/core/src/main/resources/META-INF/services/io.undertow.server.handlers.builder.HandlerBuilder @@ -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 diff --git a/core/src/test/java/io/undertow/server/handlers/DirectoryListingEnablerTestCase.java b/core/src/test/java/io/undertow/server/handlers/DirectoryListingEnablerTestCase.java new file mode 100644 index 0000000000..dfc5c22b09 --- /dev/null +++ b/core/src/test/java/io/undertow/server/handlers/DirectoryListingEnablerTestCase.java @@ -0,0 +1,155 @@ +/* + * 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)}"+ + "\npath-prefix(/)-> { resource(location='" + pathsRetainer.root.toString() + "',allow-listing=true) }", + 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()+"")); + Assert.assertTrue(bodyToTest + "\n" + pathsRetainer.rootFile.getFileName(), bodyToTest.contains("href='/"+pathsRetainer.rootFile.getFileName()+"'>"+pathsRetainer.rootFile.getFileName()+"")); + } + } + 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; + } +} \ No newline at end of file diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/DefaultServlet.java b/servlet/src/main/java/io/undertow/servlet/handlers/DefaultServlet.java index 93a346f3c8..421822c490 100644 --- a/servlet/src/main/java/io/undertow/servlet/handlers/DefaultServlet.java +++ b/servlet/src/main/java/io/undertow/servlet/handlers/DefaultServlet.java @@ -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; @@ -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); diff --git a/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/DefaultServletTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/DefaultServletTestCase.java index 196c590394..91d1ac90e6 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/DefaultServletTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/DefaultServletTestCase.java @@ -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; @@ -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 { @@ -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 @@ -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(); }