Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UNDERTOW-2216] fix resource serving via predicates + test #1465

Merged
merged 1 commit into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/src/main/java/io/undertow/attribute/RequestURLAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

package io.undertow.attribute;

import java.util.Map;

import io.undertow.predicate.PathPrefixPredicate;
import io.undertow.predicate.PathPrefixPredicate.PathPrefixMatchRecord;
import io.undertow.predicate.Predicate;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.QueryParameterUtils;

Expand Down Expand Up @@ -61,6 +66,13 @@ public void writeAttribute(final HttpServerExchange exchange, final String newVa
exchange.getQueryParameters().putAll(QueryParameterUtils.parseQueryString(newQueryString.substring(1), QueryParameterUtils.getQueryParamEncoding(exchange)));
}

final Map<String, Object> context = exchange.getAttachment(Predicate.PREDICATE_CONTEXT);
if(context != null) {
final PathPrefixPredicate.PathPrefixMatchRecord trans = (PathPrefixMatchRecord) context.get(PathPrefixPredicate.PREFIX_MATCH_RECORD);
if(trans != null) {
trans.overWritten();
}
}
}

@Override
Expand Down
37 changes: 37 additions & 0 deletions core/src/main/java/io/undertow/predicate/PathPrefixPredicate.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
*/
public class PathPrefixPredicate implements Predicate {

public static final String PREFIX_MATCH_RECORD = "PREFIX_MATCH_RECORD";
private final PathMatcher<Boolean> pathMatcher;
private static final boolean traceEnabled;

Expand Down Expand Up @@ -71,6 +72,10 @@ public boolean resolve(final HttpServerExchange value) {
UndertowLogger.PREDICATE_LOGGER.tracef("Storing \"remaining\" string of [%s] for %s.", result.getRemaining(), value);
}
context.put("remaining", result.getRemaining());
final PathPrefixMatchRecord transformer = new PathPrefixMatchRecord();
transformer.setRemaining(result.getRemaining());
transformer.setPrefix(result.getMatched());
context.put(PREFIX_MATCH_RECORD, transformer);
}
return matches;
}
Expand Down Expand Up @@ -112,4 +117,36 @@ public Predicate build(final Map<String, Object> config) {
return new PathPrefixPredicate(path);
}
}

public static class PathPrefixMatchRecord {

private String prefix;
private String remaining;
private boolean overWritten;

public void setPrefix(final String prefix) {
this.prefix = prefix;
}

public void setRemaining(final String remaining) {
this.remaining = remaining;
}

public void overWritten() {
this.overWritten = true;
}

public boolean isOverWritten() {
return this.overWritten;
}

public String getPrefix() {
return prefix;
}

public String getRemaining() {
return remaining;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
package io.undertow.server.handlers.resource;

import io.undertow.UndertowLogger;
import io.undertow.predicate.PathPrefixPredicate;
import io.undertow.predicate.Predicate;
import io.undertow.predicate.PathPrefixPredicate.PathPrefixMatchRecord;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
Expand All @@ -39,6 +42,7 @@
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;

/**
* @author Stuart Douglas
Expand Down Expand Up @@ -89,8 +93,18 @@ public static boolean sendRequestedBlobs(HttpServerExchange exchange) {

return false;
}
/**
*
* @param path
* @param resource
* @return
*/
@Deprecated(forRemoval = true, since = "2.3.6.Final")
public static StringBuilder renderDirectoryListing(String path, final Resource resource) {
return renderDirectoryListing(null, path, resource);
}

public static StringBuilder renderDirectoryListing(String path, Resource resource) {
public static StringBuilder renderDirectoryListing(final HttpServerExchange exchange, String path, final Resource resource) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baranowb can the original method be deprecated with an attribute marking it for removal on 2.4.0.Final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fl4via yes, however, the question is this fix required, given that more or less this can be achieved with some URL rewriting?

if (!path.endsWith("/")){
path += "/";
}
Expand Down Expand Up @@ -123,20 +137,36 @@ public static StringBuilder renderDirectoryListing(String path, Resource resourc
}
}

String relative = null;
if (exchange != null) {
final Map<String, Object> context = exchange.getAttachment(Predicate.PREDICATE_CONTEXT);
if (context != null) {
final PathPrefixPredicate.PathPrefixMatchRecord trans = (PathPrefixMatchRecord) context
.get(PathPrefixPredicate.PREFIX_MATCH_RECORD);
if (trans != null) {
if (trans.isOverWritten()) {
relative = trans.getPrefix();
if (!relative.endsWith("/") && !path.startsWith("/")) {
relative += "/";
}
}
}
}
}

SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss", Locale.US);
int i = 0;
if (parent != null) {
i++;
builder.append("<tr class='odd'><td><a class='icon up' href='").append(parent).append("'>[..]</a></td><td>");
builder.append("<tr class='odd'><td><a class='icon up' href='").append(relative == null ? parent : relative + parent).append(parent.endsWith("/") ? "" : "/").append("'>[..]</a></td><td>");
builder.append(format.format((resource.getLastModified() == null ? new Date(0L) : resource.getLastModified())))
.append("</td><td>--</td></tr>\n");
}

for (Resource entry : resource.list()) {
builder.append("<tr class='").append((++i & 1) == 1 ? "odd" : "even").append("'><td><a class='icon ");
builder.append(entry.isDirectory() ? "dir" : "file");
builder.append("' href='").append(path).append(entry.getName()).append("'>").append(entry.getName()).append("</a></td><td>");
builder.append("' href='").append(relative == null ? path : relative + path).append(entry.getName()).append(entry.isDirectory() ? "/" : "").append("'>").append(entry.getName()).append("</a></td><td>");
builder.append(format.format((entry.getLastModified() == null) ? new Date(0L) : entry.getLastModified()))
.append("</td><td>");
if (entry.isDirectory()) {
Expand All @@ -161,7 +191,7 @@ public static void renderDirectoryListing(HttpServerExchange exchange, Resource
return;
}

StringBuilder builder = renderDirectoryListing(requestPath, resource);
StringBuilder builder = renderDirectoryListing(exchange, requestPath, resource);

try {
ByteBuffer output = ByteBuffer.wrap(builder.toString().getBytes(StandardCharsets.UTF_8));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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 java.util.ArrayList;
import java.util.List;

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.PredicatedHandler;
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 ResourcePredicateHandlerTestCase {

private static final String DIR_PREFIXED = "prefix-resource-dir";
private static final String DIR_PATH = "path-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";

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

PredicatedHandlersParser
.parse("path-prefix(/" + TEST_PREFIX + ")-> { set(attribute=%U,value=${remaining}); resource(location='"
+ pathsRetainer.root.toString() + "',allow-listing=true) }", getClass().getClassLoader()),
new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
}
}));
testURLListing(pathsRetainer, true);
}

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

PredicatedHandlersParser
.parse("path-prefix(/" + TEST_PREFIX + ")-> { resource(location='"
+ pathsRetainer.root.toString() + "',allow-listing=true) }", getClass().getClassLoader()),
new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
}
}));
testURLListing(pathsRetainer, true);
}

@Test
public void testPathMatchNoURIAltering() throws IOException {
final PathsRetainer pathsRetainer = createTestDir(DIR_PATH, true);
//No idea why parsing does not work in one go
final List<PredicatedHandler> lst = new ArrayList<>();
lst.addAll(PredicatedHandlersParser
.parse("path(/" + TEST_PREFIX + ")-> { resource(location='"
+ pathsRetainer.root.toString() + "',allow-listing=true)", getClass().getClassLoader()));
lst.addAll(PredicatedHandlersParser
.parse("path(/" + TEST_PREFIX + "/" +pathsRetainer.sub.getFileName() + ")-> { resource(location='"
+ pathsRetainer.root.toString() + "',allow-listing=true) }", getClass().getClassLoader()));
DefaultServer.setRootHandler(Handlers.predicates(lst,
new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
Assert.assertFalse(true);
}
}));
testURLListing(pathsRetainer, false);
}

private void testURLListing(final PathsRetainer pathsRetainer, boolean testFile) throws IOException {

TestHttpClient client = new TestHttpClient();
HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/" + TEST_PREFIX+"/");
HttpResponse result = client.execute(get);
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
String bodyToTest = EntityUtils.toString(result.getEntity());
//this is not optimal...
Assert.assertTrue(bodyToTest.contains("href='/"+TEST_PREFIX+"/"+pathsRetainer.sub.getFileName()+"/'>"+pathsRetainer.sub.getFileName()+"</a>"));
Assert.assertTrue(bodyToTest.contains("href='/"+TEST_PREFIX+"/"+pathsRetainer.rootFile.getFileName()+"'>"+pathsRetainer.rootFile.getFileName()+"</a>"));
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/" + TEST_PREFIX+ "/" +pathsRetainer.sub.getFileName()+ "/");
result = client.execute(get);
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
bodyToTest = EntityUtils.toString(result.getEntity());
Assert.assertTrue(bodyToTest.contains("href='/"+TEST_PREFIX+"/'>[..]</a>"));
Assert.assertTrue(bodyToTest.contains("href='/"+TEST_PREFIX+"/"+pathsRetainer.sub.getFileName()+"/"+pathsRetainer.subFile.getFileName()+"'>"+pathsRetainer.subFile.getFileName()+"</a>"));
if(testFile) {
get = new HttpGet(DefaultServer.getDefaultServerURL() + "/"+TEST_PREFIX+"/"+pathsRetainer.sub.getFileName()+"/"+pathsRetainer.subFile.getFileName());
result = client.execute(get);
Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
bodyToTest = EntityUtils.toString(result.getEntity());
Assert.assertEquals(GIBBERISH, bodyToTest);
}
}

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 @@ -187,7 +187,7 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res
return;
}
resp.setContentType("text/html");
StringBuilder output = DirectoryUtils.renderDirectoryListing(req.getRequestURI(), resource);
StringBuilder output = DirectoryUtils.renderDirectoryListing(exchange, req.getRequestURI(), resource);
resp.getWriter().write(output.toString());
} else {
resp.sendError(StatusCodes.FORBIDDEN);
Expand Down