346 lines
17 KiB
Diff
346 lines
17 KiB
Diff
|
|
From d2715e3afa13f50deaa19643676816ce391551e9 Mon Sep 17 00:00:00 2001
|
||
|
|
From: Lin Gao <aoingl@gmail.com>
|
||
|
|
Date: Wed, 24 Jul 2019 22:03:01 +0800
|
||
|
|
Subject: [PATCH] [UNDERTOW-1578] 401 Unauthorized should be returned when
|
||
|
|
requesting a protected directory without trailing slash
|
||
|
|
|
||
|
|
---
|
||
|
|
.../servlet/core/DeploymentManagerImpl.java | 2 +
|
||
|
|
.../servlet/handlers/RedirectDirHandler.java | 71 ++++++++
|
||
|
|
.../handlers/ServletInitialHandler.java | 29 +--
|
||
|
|
.../SecurityRedirectTestCase.java | 168 ++++++++++++++++++
|
||
|
|
4 files changed, 244 insertions(+), 26 deletions(-)
|
||
|
|
create mode 100644 servlet/src/main/java/io/undertow/servlet/handlers/RedirectDirHandler.java
|
||
|
|
create mode 100644 servlet/src/test/java/io/undertow/servlet/test/defaultservlet/SecurityRedirectTestCase.java
|
||
|
|
|
||
|
|
diff --git a/servlet/src/main/java/io/undertow/servlet/core/DeploymentManagerImpl.java b/servlet/src/main/java/io/undertow/servlet/core/DeploymentManagerImpl.java
|
||
|
|
index 1906c4eaa0..9c49c4ad53 100644
|
||
|
|
--- a/servlet/src/main/java/io/undertow/servlet/core/DeploymentManagerImpl.java
|
||
|
|
+++ b/servlet/src/main/java/io/undertow/servlet/core/DeploymentManagerImpl.java
|
||
|
|
@@ -71,6 +71,7 @@
|
||
|
|
import io.undertow.servlet.api.ThreadSetupHandler;
|
||
|
|
import io.undertow.servlet.api.WebResourceCollection;
|
||
|
|
import io.undertow.servlet.handlers.CrawlerSessionManagerHandler;
|
||
|
|
+import io.undertow.servlet.handlers.RedirectDirHandler;
|
||
|
|
import io.undertow.servlet.handlers.ServletDispatchingHandler;
|
||
|
|
import io.undertow.servlet.handlers.ServletHandler;
|
||
|
|
import io.undertow.servlet.handlers.ServletInitialHandler;
|
||
|
|
@@ -218,6 +219,7 @@ public Void call(HttpServerExchange exchange, Object ignore) throws Exception {
|
||
|
|
|
||
|
|
HttpHandler wrappedHandlers = ServletDispatchingHandler.INSTANCE;
|
||
|
|
wrappedHandlers = wrapHandlers(wrappedHandlers, deploymentInfo.getInnerHandlerChainWrappers());
|
||
|
|
+ wrappedHandlers = new RedirectDirHandler(wrappedHandlers, deployment.getServletPaths());
|
||
|
|
if(!deploymentInfo.isSecurityDisabled()) {
|
||
|
|
HttpHandler securityHandler = setupSecurityHandlers(wrappedHandlers);
|
||
|
|
wrappedHandlers = new PredicateHandler(DispatcherTypePredicate.REQUEST, securityHandler, wrappedHandlers);
|
||
|
|
diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/RedirectDirHandler.java b/servlet/src/main/java/io/undertow/servlet/handlers/RedirectDirHandler.java
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000000..576eb11a87
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/servlet/src/main/java/io/undertow/servlet/handlers/RedirectDirHandler.java
|
||
|
|
@@ -0,0 +1,71 @@
|
||
|
|
+/*
|
||
|
|
+ * JBoss, Home of Professional Open Source.
|
||
|
|
+ * Copyright 2019 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.servlet.handlers;
|
||
|
|
+
|
||
|
|
+import io.undertow.server.HttpHandler;
|
||
|
|
+import io.undertow.server.HttpServerExchange;
|
||
|
|
+import io.undertow.util.Headers;
|
||
|
|
+import io.undertow.util.Methods;
|
||
|
|
+import io.undertow.util.RedirectBuilder;
|
||
|
|
+import io.undertow.util.StatusCodes;
|
||
|
|
+
|
||
|
|
+/**
|
||
|
|
+ * Handler that redirects the directory requests without trailing slash to the one append trailing slash.
|
||
|
|
+ *
|
||
|
|
+ * @author Lin Gao
|
||
|
|
+ */
|
||
|
|
+public class RedirectDirHandler implements HttpHandler {
|
||
|
|
+
|
||
|
|
+ private static final String HTTP2_UPGRADE_PREFIX = "h2";
|
||
|
|
+
|
||
|
|
+ private final HttpHandler next;
|
||
|
|
+ private final ServletPathMatches paths;
|
||
|
|
+
|
||
|
|
+ public RedirectDirHandler(HttpHandler next, ServletPathMatches paths) {
|
||
|
|
+ this.next = next;
|
||
|
|
+ this.paths = paths;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ @Override
|
||
|
|
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||
|
|
+ final String path = exchange.getRelativePath();
|
||
|
|
+ final ServletPathMatch info = paths.getServletHandlerByPath(path);
|
||
|
|
+ // https://issues.jboss.org/browse/WFLY-3439
|
||
|
|
+ // if the request is an upgrade request then we don't want to redirect
|
||
|
|
+ // as there is a good chance the web socket client won't understand the redirect
|
||
|
|
+ // we make an exception for HTTP2 upgrade requests, as this would have already be handled at
|
||
|
|
+ // the connector level if it was going to be handled.
|
||
|
|
+ String upgradeString = exchange.getRequestHeaders().getFirst(Headers.UPGRADE);
|
||
|
|
+ boolean isUpgradeRequest = upgradeString != null && !upgradeString.startsWith(HTTP2_UPGRADE_PREFIX);
|
||
|
|
+ if (info.getType() == ServletPathMatch.Type.REDIRECT && !isUpgradeRequest) {
|
||
|
|
+ // UNDERTOW-89
|
||
|
|
+ // we redirect on GET requests to the root context to add an / to the end
|
||
|
|
+ if (exchange.getRequestMethod().equals(Methods.GET) || exchange.getRequestMethod().equals(Methods.HEAD)) {
|
||
|
|
+ exchange.setStatusCode(StatusCodes.FOUND);
|
||
|
|
+ } else {
|
||
|
|
+ exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT);
|
||
|
|
+ }
|
||
|
|
+ exchange.getResponseHeaders().put(Headers.LOCATION,
|
||
|
|
+ RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true));
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ next.handleRequest(exchange);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+}
|
||
|
|
diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/ServletInitialHandler.java b/servlet/src/main/java/io/undertow/servlet/handlers/ServletInitialHandler.java
|
||
|
|
index 82a3ad7360..7edc7e493c 100644
|
||
|
|
--- a/servlet/src/main/java/io/undertow/servlet/handlers/ServletInitialHandler.java
|
||
|
|
+++ b/servlet/src/main/java/io/undertow/servlet/handlers/ServletInitialHandler.java
|
||
|
|
@@ -39,11 +39,8 @@
|
||
|
|
import io.undertow.servlet.spec.HttpServletResponseImpl;
|
||
|
|
import io.undertow.servlet.spec.RequestDispatcherImpl;
|
||
|
|
import io.undertow.servlet.spec.ServletContextImpl;
|
||
|
|
-import io.undertow.util.Headers;
|
||
|
|
import io.undertow.util.HttpString;
|
||
|
|
-import io.undertow.util.Methods;
|
||
|
|
import io.undertow.util.Protocols;
|
||
|
|
-import io.undertow.util.RedirectBuilder;
|
||
|
|
import io.undertow.util.StatusCodes;
|
||
|
|
import org.xnio.ChannelListener;
|
||
|
|
import org.xnio.Option;
|
||
|
|
@@ -80,8 +77,6 @@
|
||
|
|
*/
|
||
|
|
public class ServletInitialHandler implements HttpHandler, ServletDispatcher {
|
||
|
|
|
||
|
|
- private static final String HTTP2_UPGRADE_PREFIX = "h2";
|
||
|
|
-
|
||
|
|
private static final RuntimePermission PERMISSION = new RuntimePermission("io.undertow.servlet.CREATE_INITIAL_HANDLER");
|
||
|
|
|
||
|
|
private final HttpHandler next;
|
||
|
|
@@ -149,30 +144,12 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
final ServletPathMatch info = paths.getServletHandlerByPath(path);
|
||
|
|
- //https://issues.jboss.org/browse/WFLY-3439
|
||
|
|
- //if the request is an upgrade request then we don't want to redirect
|
||
|
|
- //as there is a good chance the web socket client won't understand the redirect
|
||
|
|
- //we make an exception for HTTP2 upgrade requests, as this would have already be handled at
|
||
|
|
- //the connector level if it was going to be handled.
|
||
|
|
- String upgradeString = exchange.getRequestHeaders().getFirst(Headers.UPGRADE);
|
||
|
|
- boolean isUpgradeRequest = upgradeString != null && !upgradeString.startsWith(HTTP2_UPGRADE_PREFIX);
|
||
|
|
- if (info.getType() == ServletPathMatch.Type.REDIRECT && !isUpgradeRequest) {
|
||
|
|
- //UNDERTOW-89
|
||
|
|
- //we redirect on GET requests to the root context to add an / to the end
|
||
|
|
- if(exchange.getRequestMethod().equals(Methods.GET) || exchange.getRequestMethod().equals(Methods.HEAD)) {
|
||
|
|
- exchange.setStatusCode(StatusCodes.FOUND);
|
||
|
|
- } else {
|
||
|
|
- exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT);
|
||
|
|
- }
|
||
|
|
- exchange.getResponseHeaders().put(Headers.LOCATION, RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true));
|
||
|
|
- return;
|
||
|
|
- } else if (info.getType() == ServletPathMatch.Type.REWRITE) {
|
||
|
|
- //this can only happen if the path ends with a /
|
||
|
|
- //otherwise there would be a redirect instead
|
||
|
|
+ if (info.getType() == ServletPathMatch.Type.REWRITE) {
|
||
|
|
+ // this can only happen if the path ends with a /
|
||
|
|
+ // otherwise there would be a redirect instead
|
||
|
|
exchange.setRelativePath(info.getRewriteLocation());
|
||
|
|
exchange.setRequestPath(exchange.getResolvedPath() + info.getRewriteLocation());
|
||
|
|
}
|
||
|
|
-
|
||
|
|
final HttpServletResponseImpl response = new HttpServletResponseImpl(exchange, servletContext);
|
||
|
|
final HttpServletRequestImpl request = new HttpServletRequestImpl(exchange, servletContext);
|
||
|
|
final ServletRequestContext servletRequestContext = new ServletRequestContext(servletContext.getDeployment(), request, response, info);
|
||
|
|
diff --git a/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/SecurityRedirectTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/SecurityRedirectTestCase.java
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000000..8fa9323177
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/servlet/src/test/java/io/undertow/servlet/test/defaultservlet/SecurityRedirectTestCase.java
|
||
|
|
@@ -0,0 +1,168 @@
|
||
|
|
+/*
|
||
|
|
+ * JBoss, Home of Professional Open Source.
|
||
|
|
+ * Copyright 2019 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.servlet.test.defaultservlet;
|
||
|
|
+
|
||
|
|
+import static io.undertow.util.Headers.AUTHORIZATION;
|
||
|
|
+import static io.undertow.util.Headers.BASIC;
|
||
|
|
+import static io.undertow.util.Headers.LOCATION;
|
||
|
|
+import static io.undertow.util.Headers.WWW_AUTHENTICATE;
|
||
|
|
+import static org.junit.Assert.assertEquals;
|
||
|
|
+
|
||
|
|
+import java.io.IOException;
|
||
|
|
+
|
||
|
|
+import javax.servlet.ServletException;
|
||
|
|
+
|
||
|
|
+import org.apache.http.Header;
|
||
|
|
+import org.apache.http.HttpResponse;
|
||
|
|
+import org.apache.http.client.HttpClient;
|
||
|
|
+import org.apache.http.client.methods.HttpGet;
|
||
|
|
+import org.apache.http.impl.client.HttpClientBuilder;
|
||
|
|
+import org.junit.Assert;
|
||
|
|
+import org.junit.BeforeClass;
|
||
|
|
+import org.junit.Test;
|
||
|
|
+import org.junit.runner.RunWith;
|
||
|
|
+
|
||
|
|
+import io.undertow.server.handlers.PathHandler;
|
||
|
|
+import io.undertow.servlet.api.DeploymentInfo;
|
||
|
|
+import io.undertow.servlet.api.DeploymentManager;
|
||
|
|
+import io.undertow.servlet.api.LoginConfig;
|
||
|
|
+import io.undertow.servlet.api.SecurityConstraint;
|
||
|
|
+import io.undertow.servlet.api.ServletContainer;
|
||
|
|
+import io.undertow.servlet.api.WebResourceCollection;
|
||
|
|
+import io.undertow.servlet.test.path.ServletPathMappingTestCase;
|
||
|
|
+import io.undertow.servlet.test.security.constraint.ServletIdentityManager;
|
||
|
|
+import io.undertow.servlet.test.util.TestClassIntrospector;
|
||
|
|
+import io.undertow.servlet.test.util.TestResourceLoader;
|
||
|
|
+import io.undertow.testutils.DefaultServer;
|
||
|
|
+import io.undertow.testutils.HttpClientUtils;
|
||
|
|
+import io.undertow.util.FlexBase64;
|
||
|
|
+import io.undertow.util.StatusCodes;
|
||
|
|
+
|
||
|
|
+/**
|
||
|
|
+ * TestCase on redirect with or without trailing slash when requesting protected path.
|
||
|
|
+ *
|
||
|
|
+ * @author Lin Gao
|
||
|
|
+ */
|
||
|
|
+@RunWith(DefaultServer.class)
|
||
|
|
+public class SecurityRedirectTestCase {
|
||
|
|
+
|
||
|
|
+ @BeforeClass
|
||
|
|
+ public static void setup() throws ServletException {
|
||
|
|
+
|
||
|
|
+ final PathHandler root = new PathHandler();
|
||
|
|
+ final ServletContainer container = ServletContainer.Factory.newInstance();
|
||
|
|
+
|
||
|
|
+ ServletIdentityManager identityManager = new ServletIdentityManager();
|
||
|
|
+ identityManager.addUser("user1", "password1", "role1");
|
||
|
|
+
|
||
|
|
+ DeploymentInfo builder = new DeploymentInfo()
|
||
|
|
+ .setClassIntrospecter(TestClassIntrospector.INSTANCE)
|
||
|
|
+ .setClassLoader(ServletPathMappingTestCase.class.getClassLoader())
|
||
|
|
+ .setContextPath("/servletContext")
|
||
|
|
+ .setDeploymentName("servletContext.war")
|
||
|
|
+ .setResourceManager(new TestResourceLoader(SecurityRedirectTestCase.class))
|
||
|
|
+ .addWelcomePages("index.html")
|
||
|
|
+ .setIdentityManager(identityManager)
|
||
|
|
+ .setLoginConfig(new LoginConfig("BASIC", "Test Realm"))
|
||
|
|
+ .addSecurityConstraint(new SecurityConstraint()
|
||
|
|
+ .addRoleAllowed("role1")
|
||
|
|
+ .addWebResourceCollection(new WebResourceCollection()
|
||
|
|
+ .addUrlPatterns("/index.html", "/filterpath/*")));
|
||
|
|
+
|
||
|
|
+ DeploymentManager manager = container.addDeployment(builder);
|
||
|
|
+ manager.deploy();
|
||
|
|
+ root.addPrefixPath(builder.getContextPath(), manager.start());
|
||
|
|
+ DefaultServer.setRootHandler(root);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ @SuppressWarnings("deprecation")
|
||
|
|
+ @Test
|
||
|
|
+ public void testSecurityWithWelcomeFileRedirect() throws IOException {
|
||
|
|
+ // disable following redirect
|
||
|
|
+ HttpClient client = HttpClientBuilder.create().disableRedirectHandling().build();
|
||
|
|
+ try {
|
||
|
|
+ HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext");
|
||
|
|
+ HttpResponse result = client.execute(get);
|
||
|
|
+ Assert.assertEquals(StatusCodes.FOUND, result.getStatusLine().getStatusCode());
|
||
|
|
+ Header[] values = result.getHeaders(LOCATION.toString());
|
||
|
|
+ assertEquals(1, values.length);
|
||
|
|
+ assertEquals(DefaultServer.getDefaultServerURL() + "/servletContext/", values[0].getValue());
|
||
|
|
+ HttpClientUtils.readResponse(result);
|
||
|
|
+
|
||
|
|
+ get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/");
|
||
|
|
+ result = client.execute(get);
|
||
|
|
+ Assert.assertEquals(StatusCodes.UNAUTHORIZED, result.getStatusLine().getStatusCode());
|
||
|
|
+
|
||
|
|
+ values = result.getHeaders(WWW_AUTHENTICATE.toString());
|
||
|
|
+ assertEquals(1, values.length);
|
||
|
|
+ assertEquals(BASIC + " realm=\"Test Realm\"", values[0].getValue());
|
||
|
|
+ HttpClientUtils.readResponse(result);
|
||
|
|
+
|
||
|
|
+ get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/");
|
||
|
|
+ get.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("user1:password1".getBytes(), false));
|
||
|
|
+ result = client.execute(get);
|
||
|
|
+ String response = HttpClientUtils.readResponse(result);
|
||
|
|
+ Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
|
||
|
|
+ Assert.assertTrue(response.contains("Redirected home page"));
|
||
|
|
+ } finally {
|
||
|
|
+ client.getConnectionManager().shutdown();
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ @SuppressWarnings("deprecation")
|
||
|
|
+ @Test
|
||
|
|
+ public void testSecurityWithoutWelcomeFileRedirect() throws IOException {
|
||
|
|
+ // disable following redirect
|
||
|
|
+ HttpClient client = HttpClientBuilder.create().disableRedirectHandling().build();
|
||
|
|
+ try {
|
||
|
|
+ HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/filterpath");
|
||
|
|
+ HttpResponse result = client.execute(get);
|
||
|
|
+ Assert.assertEquals(StatusCodes.UNAUTHORIZED, result.getStatusLine().getStatusCode());
|
||
|
|
+ HttpClientUtils.readResponse(result);
|
||
|
|
+
|
||
|
|
+ get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/filterpath/");
|
||
|
|
+ result = client.execute(get);
|
||
|
|
+ Assert.assertEquals(StatusCodes.UNAUTHORIZED, result.getStatusLine().getStatusCode());
|
||
|
|
+
|
||
|
|
+ Header[] values = result.getHeaders(WWW_AUTHENTICATE.toString());
|
||
|
|
+ assertEquals(1, values.length);
|
||
|
|
+ assertEquals(BASIC + " realm=\"Test Realm\"", values[0].getValue());
|
||
|
|
+ HttpClientUtils.readResponse(result);
|
||
|
|
+
|
||
|
|
+ get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/filterpath");
|
||
|
|
+ get.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("user1:password1".getBytes(), false));
|
||
|
|
+ result = client.execute(get);
|
||
|
|
+ Assert.assertEquals(StatusCodes.FOUND, result.getStatusLine().getStatusCode());
|
||
|
|
+ values = result.getHeaders(LOCATION.toString());
|
||
|
|
+ assertEquals(1, values.length);
|
||
|
|
+ assertEquals(DefaultServer.getDefaultServerURL() + "/servletContext/filterpath/", values[0].getValue());
|
||
|
|
+ HttpClientUtils.readResponse(result);
|
||
|
|
+
|
||
|
|
+ get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/filterpath/filtered.txt");
|
||
|
|
+ get.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("user1:password1".getBytes(), false));
|
||
|
|
+ result = client.execute(get);
|
||
|
|
+ String response = HttpClientUtils.readResponse(result);
|
||
|
|
+ Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
|
||
|
|
+ Assert.assertTrue(response.equals("Stuart"));
|
||
|
|
+ } finally {
|
||
|
|
+ client.getConnectionManager().shutdown();
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+}
|