From 961ebe197d739221c6471a7087d1e7a12c004529 Mon Sep 17 00:00:00 2001
From: Grzegorz Grzybek
Date: Wed, 24 Apr 2024 09:13:36 +0200
Subject: [PATCH] Url handling issue 3382 (#3405)
* chore(urls): Unify WAR URL handling in unauthenticated scenario (#3382)
Signed-off-by: Grzegorz Grzybek
* chore(urls): Unify WAR URL handling in authenticated scenario (#3382)
* chore(urls): Unify SpringBoot URL handling (#3382)
* chore(urls): Unify WAR/SpringBoot/Quarkus URL handling (fixes #3382)
* chore(urls): Apply code review discussion (#3382)
Signed-off-by: Grzegorz Grzybek
* chore(urls): Fix test failures (#3382)
Signed-off-by: Grzegorz Grzybek
---------
Signed-off-by: Grzegorz Grzybek
---
.../src/main/webapp/WEB-INF/web.xml | 2 +-
.../java/io/hawt/system/Authenticator.java | 1 +
.../src/main/java/io/hawt/util/Strings.java | 56 +----
.../main/java/io/hawt/web/ServletHelpers.java | 88 +++++++
.../io/hawt/web/auth/AuthSessionHelpers.java | 11 -
.../web/auth/AuthenticationConfiguration.java | 83 +++++-
.../hawt/web/auth/AuthenticationFilter.java | 14 +-
.../web/auth/ClientRouteRedirectFilter.java | 237 ++++++++++++++++++
.../io/hawt/web/auth/LoginRedirectFilter.java | 104 --------
.../java/io/hawt/web/auth/LogoutServlet.java | 2 +-
.../io/hawt/web/auth/RelativeRequestUri.java | 7 +-
.../io/hawt/web/auth/SessionExpiryFilter.java | 8 +-
.../java/io/hawt/web/auth/UserServlet.java | 2 +-
.../test/java/io/hawt/util/StringsTest.java | 116 ---------
.../java/io/hawt/web/ServletHelpersTest.java | 141 +++++++++++
...ava => ClientRouteRedirectFilterTest.java} | 36 +--
.../src/main/webapp/WEB-INF/web.xml | 6 +-
hawtio-war/src/main/webapp/WEB-INF/web.xml | 11 +-
.../quarkus/deployment/HawtioProcessor.java | 23 +-
...wtioQuarkusClientRouteRedirectFilter.java} | 4 +-
.../filters/HawtioQuarkusPathFilter.java | 72 ------
.../HawtioSpringBootEndpointIT.java | 2 +-
.../HawtioSpringBootTestSupport.java | 33 ++-
.../hawt/springboot/EndpointPathResolver.java | 11 +-
.../io/hawt/springboot/HawtioEndpoint.java | 23 +-
.../HawtioManagementConfiguration.java | 64 ++++-
.../hawt/springboot/TrailingSlashFilter.java | 12 +-
27 files changed, 706 insertions(+), 463 deletions(-)
create mode 100644 hawtio-system/src/main/java/io/hawt/web/auth/ClientRouteRedirectFilter.java
delete mode 100644 hawtio-system/src/main/java/io/hawt/web/auth/LoginRedirectFilter.java
rename hawtio-system/src/test/java/io/hawt/web/auth/{LoginRedirectFilterTest.java => ClientRouteRedirectFilterTest.java} (51%)
rename platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/{HawtioQuarkusLoginRedirectFilter.java => HawtioQuarkusClientRouteRedirectFilter.java} (86%)
delete mode 100644 platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusPathFilter.java
diff --git a/deploy/hawtio-default/src/main/webapp/WEB-INF/web.xml b/deploy/hawtio-default/src/main/webapp/WEB-INF/web.xml
index 7e710e99c4..c4e9496d1e 100644
--- a/deploy/hawtio-default/src/main/webapp/WEB-INF/web.xml
+++ b/deploy/hawtio-default/src/main/webapp/WEB-INF/web.xml
@@ -108,7 +108,7 @@
LoginRedirectFilter
- io.hawt.web.auth.LoginRedirectFilter
+ io.hawt.web.auth.ClientRouteRedirectFilter
LoginRedirectFilter
diff --git a/hawtio-system/src/main/java/io/hawt/system/Authenticator.java b/hawtio-system/src/main/java/io/hawt/system/Authenticator.java
index 9ff4a0db17..d1a85b0a42 100644
--- a/hawtio-system/src/main/java/io/hawt/system/Authenticator.java
+++ b/hawtio-system/src/main/java/io/hawt/system/Authenticator.java
@@ -34,6 +34,7 @@
* Authenticator supports the following authentication methods:
*
* - a set of user name and password
+ * - oidc (bearer) access token
* - client certificates
*
*/
diff --git a/hawtio-system/src/main/java/io/hawt/util/Strings.java b/hawtio-system/src/main/java/io/hawt/util/Strings.java
index de9d98dde8..250a92bb3c 100644
--- a/hawtio-system/src/main/java/io/hawt/util/Strings.java
+++ b/hawtio-system/src/main/java/io/hawt/util/Strings.java
@@ -27,66 +27,12 @@ public static List split(String text, String delimiter) {
.filter(Strings::isNotBlank).collect(Collectors.toList());
}
- /**
- * Normalizes a path. If the path contains a single '/' character it is returned
- * unchanged, otherwise the path is:
- *
- * - stripped from all multiple consecutive occurrences of '/' characters
- * - stripped from trailing '/' character(s)
- *
- *
- * @param path
- * path to normalize
- * @return normalized path
- */
- public static String cleanPath(final String path) {
- final String result = path.replaceAll("//+", "/");
- return result.length() == 1 && result.charAt(0) == '/' ? result
- : result.replaceAll("/+$", "");
- }
-
- /**
- * Creates a web context path from components. Concatenates all path components
- * using '/' character as delimiter and the result is then:
- *
- * - prefixed with '/' character
- * - stripped from all multiple consecutive occurrences of '/' characters
- * - stripped from trailing '/' character(s)
- *
- *
- * @return empty string or string which starts with a "/" character but does not
- * end with a "/" character
- */
- public static String webContextPath(final String first, final String... more) {
- if (more.length == 0 && (first == null || first.isEmpty())) {
- return "";
- }
-
- final StringBuilder b = new StringBuilder();
- if (first != null) {
- if (!first.startsWith("/")) {
- b.append('/');
- }
- b.append(first);
- }
-
- for (final String s : more) {
- if (s != null && !s.isEmpty()) {
- b.append('/');
- b.append(s);
- }
- }
-
- final String cleanedPath = cleanPath(b.toString());
- return cleanedPath.length() == 1 ? "" : cleanedPath;
- }
-
public static String resolvePlaceholders(String value) {
return resolvePlaceholders(value, System.getProperties());
}
/**
- * Simple, recursively-safe property placeholder resolver. Only system properties are used (for now). De-facto
+ * Simple, recursively-safe property placeholder resolver. De-facto
* standard {@code ${...}} syntax is used. Unresolvable properties are not replaced and separators pass to
* resulting value.
*
diff --git a/hawtio-system/src/main/java/io/hawt/web/ServletHelpers.java b/hawtio-system/src/main/java/io/hawt/web/ServletHelpers.java
index 252fcb04ed..93609c565c 100644
--- a/hawtio-system/src/main/java/io/hawt/web/ServletHelpers.java
+++ b/hawtio-system/src/main/java/io/hawt/web/ServletHelpers.java
@@ -7,6 +7,8 @@
import java.net.URL;
import javax.management.AttributeNotFoundException;
+import io.hawt.web.auth.SessionExpiryFilter;
+import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletResponse;
import io.hawt.system.Authenticator;
@@ -144,4 +146,90 @@ public static String sanitizeHeader(String header) {
}
return header.replaceAll("[\\r\\n]", "");
}
+
+ /**
+ * Normalizes a path. If the path contains a single '/' character it is returned
+ * unchanged, otherwise the path is:
+ *
+ * - stripped from all multiple consecutive occurrences of '/' characters
+ * - stripped from trailing '/' character(s)
+ *
+ *
+ * @param path
+ * path to normalize
+ * @return normalized path
+ */
+ public static String cleanPath(final String path) {
+ final String result = path.replaceAll("//+", "/");
+ return result.length() == 1 && result.charAt(0) == '/' ? result
+ : result.replaceAll("/+$", "");
+ }
+
+ /**
+ * Creates a web context path from components. Concatenates all path components
+ * using '/' character as delimiter and the result is then:
+ *
+ * - prefixed with '/' character
+ * - stripped from all multiple consecutive occurrences of '/' characters
+ * - stripped from trailing '/' character(s)
+ *
+ *
+ * @return empty string or string which starts with a "/" character but does not
+ * end with a "/" character
+ */
+ public static String webContextPath(final String first, final String... more) {
+ if (more.length == 0 && (first == null || first.isEmpty())) {
+ return "";
+ }
+
+ final StringBuilder b = new StringBuilder();
+ if (first != null) {
+ if (!first.startsWith("/")) {
+ b.append('/');
+ }
+ b.append(first);
+ }
+
+ for (final String s : more) {
+ if (s != null && !s.isEmpty()) {
+ b.append('/');
+ b.append(s);
+ }
+ }
+
+ final String cleanedPath = cleanPath(b.toString());
+ return cleanedPath.length() == 1 ? "" : cleanedPath;
+ }
+
+ /**
+ * Return a number of web path segments that need to be skipped to reach hawtio path. In JakartaEE
+ * environment (WAR) everything after context path is "hawtio path", so {@code 0} is returned. In Spring Boot
+ * we may have to skip some segments (like {@code /actuator/hawtio}).
+ *
+ * @param servletContext
+ * @return
+ */
+ public static int hawtioPathIndex(ServletContext servletContext) {
+ String servletPath = (String) servletContext.getAttribute(SessionExpiryFilter.SERVLET_PATH);
+ if (servletPath == null) {
+ // this attribute is set only in non JakartaEE environments, so here we are in standard WAR
+ // deployment. Just return "0", which means full path without initial context path
+ return 0;
+ } else {
+ // when SessionExpiryFilter.SERVLET_PATH is set, it contains prefix which should be skipped and which
+ // is not standard JakartaEE path components (context path, servlet path, path info).
+ // for Spring Boot we have to skip dispatcher servlet path, management endpoints base ("/actuator")
+ // and management endpoint mapping
+ String cleanPath = webContextPath(servletPath);
+ int pathIndex = 0;
+ // for "/actuator/hawtio", we have to return "2", so count slashes
+ for (char c : cleanPath.toCharArray()) {
+ if (c == '/') {
+ pathIndex++;
+ }
+ }
+ return pathIndex;
+ }
+ }
+
}
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/AuthSessionHelpers.java b/hawtio-system/src/main/java/io/hawt/web/auth/AuthSessionHelpers.java
index 1c2b9a2ea5..202007bb48 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/AuthSessionHelpers.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/AuthSessionHelpers.java
@@ -97,15 +97,4 @@ public static boolean isAuthenticated(HttpSession session) {
return session != null && session.getAttribute("subject") != null;
}
- public static boolean isSpringSecurityEnabled() {
- try {
- Class.forName("org.springframework.security.core.SpringSecurityCoreVersion");
- LOG.debug("Spring Security enabled");
- return true;
- } catch (ClassNotFoundException e) {
- LOG.debug("Spring Security not found");
- return false;
- }
- }
-
}
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationConfiguration.java b/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationConfiguration.java
index 330f1ea5ff..e5c152808b 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationConfiguration.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationConfiguration.java
@@ -2,7 +2,6 @@
import java.io.IOException;
import java.io.InputStream;
-import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -25,12 +24,69 @@ public class AuthenticationConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationConfiguration.class);
public static final String LOGIN_URL = "/login";
- public static final String[] UNSECURED_PATHS = {
- "/login", "/auth/login", "/auth/logout", "/auth/config", "/auth/config/session-timeout",
- "/css", "/fonts", "/img", "/js", "/static", "/hawtconfig.json",
- "/jolokia", "/user", "/keycloak", "/plugin", "/favicon.ico"
+
+ // these paths shouldn't be redirected to /login, because either they're for static resources we don't have to
+ // protect (like site building resources - css, js, html) or they're related to authentication itself
+ // finally, /jolokia and /proxy should not be redirected, because these are accessed via xhr/fetch and should
+ // simply return 403 if needed
+ // all these paths are passed to request.getServletPath().startsWith(), so it depends on servlet mapping
+ // (relative to context path, so ignoring "/hawtio" for WAR or value from "management.server.base-path" property):
+ // - for prefix mapping, like "/jolokia/*", for request like "/jolokia/read/xxx", servlet path is "/jolokia"
+ // - for extension mapping, like "*.info", for request like "/x/y/z.info", servlet path is ... "/x/y/z.info"
+ // - for default mapping, "/", for request like "/css/defaults.css", servlet path is "/css/defaults.css"
+
+ /**
+ * Static resources paths, which should be always reachable.
+ */
+ public static final String[] UNSECURED_RESOURCE_PATHS = {
+ "/index.html", "/favicon.ico", "/hawtconfig.json",
+ "/robots.txt", "/json.worker.js", "/editor.worker.js",
+ "/css", "/fonts", "/img", "/js", "/static"
};
+ /**
+ * Paths related to authentication process.
+ * {@code /login} path is actually a client-side router path, but Hawtio sometimes redirects (thus forcing
+ * server request) to this path for unified authentication experience.
+ * {@code /auth/*}, {@code user}, {@code /keycloak} paths are actual servlet mappings.
+ */
+ public static final String[] UNSECURED_AUTHENTICATION_PATHS = {
+ "/login",
+ "/auth/login", "/auth/logout", "/auth/config",
+ "/user", "/keycloak"
+ };
+
+ /**
+ * Paths for configuration of the client (@hawtio/react) part.
+ */
+ public static final String[] UNSECURED_META_PATHS = {
+ "/plugin"
+ };
+
+ /**
+ * API paths. These may be confusing:
+ * - should NOT be redirected to /login, but
+ * - should be protected otherwise (e.g., AuthenticationFilter)
+ *
+ */
+ public static final String[] UNSECURED_SERVLET_PATHS = {
+ "/jolokia", "/proxy"
+ };
+
+ /**
+ * Paths that shouldn't be redirected to {@code /login} when user is not authenticated.
+ */
+ public static final String[] UNSECURED_PATHS;
+
+ static {
+ ArrayList l = new ArrayList<>();
+ l.addAll(Arrays.asList(UNSECURED_RESOURCE_PATHS));
+ l.addAll(Arrays.asList(UNSECURED_AUTHENTICATION_PATHS));
+ l.addAll(Arrays.asList(UNSECURED_META_PATHS));
+ l.addAll(Arrays.asList(UNSECURED_SERVLET_PATHS));
+ UNSECURED_PATHS = l.toArray(String[]::new);
+ }
+
// =========================================================================
// Configuration properties
// =========================================================================
@@ -205,7 +261,7 @@ private static List getDiscoveries(String auth
AuthenticationContainerDiscovery discovery = clazz.getDeclaredConstructor().newInstance();
discoveries.add(discovery);
} catch (Exception e) {
- LOG.warn("Couldn't instantiate discovery " + discoveryClass, e);
+ LOG.warn("Couldn't instantiate discovery {}", discoveryClass, e);
}
}
return discoveries;
@@ -255,6 +311,21 @@ public boolean isOidcEnabled() {
return oidcConfiguration != null && oidcConfiguration.isEnabled();
}
+ public static boolean isSpringSecurityEnabled() {
+ try {
+ Class.forName("org.springframework.security.core.SpringSecurityCoreVersion");
+ LOG.trace("Spring Security enabled");
+ return true;
+ } catch (ClassNotFoundException e) {
+ LOG.trace("Spring Security not found");
+ return false;
+ }
+ }
+
+ public boolean isExternalAuthenticationEnabled() {
+ return isKeycloakEnabled() || isOidcEnabled() || isSpringSecurityEnabled();
+ }
+
/**
* Initialize OIDC configuration, so it is available both in {@link AuthConfigurationServlet} and
* {@link io.hawt.web.filters.ContentSecurityPolicyFilter}.
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationFilter.java b/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationFilter.java
index d9758956d3..70d2802e26 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationFilter.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/AuthenticationFilter.java
@@ -34,10 +34,13 @@ public class AuthenticationFilter implements Filter {
protected int timeout;
protected AuthenticationConfiguration authConfiguration;
+ private int pathIndex;
+
@Override
public void init(FilterConfig filterConfig) throws ServletException {
authConfiguration = AuthenticationConfiguration.getConfiguration(filterConfig.getServletContext());
timeout = AuthSessionHelpers.getSessionTimeout(filterConfig.getServletContext());
+ this.pathIndex = ServletHelpers.hawtioPathIndex(filterConfig.getServletContext());
}
@Override
@@ -62,12 +65,21 @@ public void doFilter(final ServletRequest request, final ServletResponse respons
return;
}
+ boolean proxyMode = false;
+ RelativeRequestUri uri = new RelativeRequestUri(httpRequest, pathIndex);
+ if (uri.getComponents().length > 0 && "proxy".equals(uri.getComponents()[0])) {
+ // https://github.com/hawtio/hawtio/issues/3178
+ // /proxy/* requests are now authenticated by this filter, but we have to do it differently, because
+ // "Authorization" header carries credentials for target Jolokia agent
+ proxyMode = !uri.getUri().equals("proxy/enabled");
+ }
+
HttpSession session = httpRequest.getSession(false);
if (session != null) {
Subject subject = (Subject) session.getAttribute("subject");
// For Spring Security
- if (AuthSessionHelpers.isSpringSecurityEnabled()) {
+ if (AuthenticationConfiguration.isSpringSecurityEnabled()) {
if (subject == null && httpRequest.getRemoteUser() != null) {
AuthSessionHelpers.setup(
session, new Subject(), httpRequest.getRemoteUser(), timeout);
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/ClientRouteRedirectFilter.java b/hawtio-system/src/main/java/io/hawt/web/auth/ClientRouteRedirectFilter.java
new file mode 100644
index 0000000000..f73c9d1c02
--- /dev/null
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/ClientRouteRedirectFilter.java
@@ -0,0 +1,237 @@
+package io.hawt.web.auth;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import io.hawt.web.ServletHelpers;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+import io.hawt.system.AuthHelpers;
+import io.hawt.system.AuthenticateResult;
+import io.hawt.system.Authenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static io.hawt.web.filters.BaseTagHrefFilter.PARAM_APPLICATION_CONTEXT_PATH;
+
+/**
+ * A filter that handles client-side routing URLs and redirects to login page depending on authentication state.
+ * There are two kinds of URLs handled:
+ * - URLs that correspond to Hawtio resources and servlets
+ * - URLs that correspond to Hawtio-React client routes (which would give HTTP/404 when handled)
+ *
+ *
+ * Special client route is {@code /login}, which should be handled carefuly and in some cases user may get
+ * redirected to this URL for smoother client experience (no React app blinking).
+ *
+ * This filter should be called after {@link AuthenticationFilter}, but for requests not handled by that
+ * filter, we may perform pre-emptive authentication.
+ *
+ * This filter used to be called {@code LoginRedirectFilter}, but it's doing a bit more now to provide unified
+ * experience between WAR, Spring Boot and Quarkus deployments.
+ *
+ * Even in Spring Boot, this filter is called before {@code DispatcherServlet}, so it's invoked before
+ * any {@code @RequestMapping} methods.
+ */
+public class ClientRouteRedirectFilter implements Filter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClientRouteRedirectFilter.class);
+
+ public static final String ATTRIBUTE_UNSECURED_PATHS = "unsecuredPaths";
+
+ private int timeout;
+ private AuthenticationConfiguration authConfiguration;
+
+ /**
+ * Paths which are either unsecured or are secured in different way (like AuthenticationFilter)
+ */
+ private String[] unsecuredPaths;
+
+ /**
+ * Base path for hawtio. Should be "/" for hawtio.war
+ * and e.g., "/actuator/hawtio" for Spring Boot (configurable)
+ */
+ private final String basePath;
+
+ /**
+ * Base path including context path, which is "/hawtio" for hawtio.war (or "/console" for console.war)
+ * and may be anything on Spring Boot with "server.servlet.context-path" or
+ * "management.server.base-path" properties
+ */
+ private String baseFullPath;
+
+ /**
+ * Only the context path - /hawtio for hawtio.war and value from
+ * server.servlet.context-path or management.server.base-path properties on Spring Boot.
+ */
+ private String contextPath;
+
+ private Redirector redirector = new Redirector();
+
+ public ClientRouteRedirectFilter() {
+ this(AuthenticationConfiguration.UNSECURED_PATHS, "/");
+ }
+
+ public ClientRouteRedirectFilter(String[] unsecuredPaths, String hawtioBase) {
+ this.unsecuredPaths = unsecuredPaths;
+ this.basePath = ServletHelpers.cleanPath(hawtioBase);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ authConfiguration = AuthenticationConfiguration.getConfiguration(filterConfig.getServletContext());
+ timeout = AuthSessionHelpers.getSessionTimeout(filterConfig.getServletContext());
+ LOG.info("Hawtio ClientRouteRedirectFilter is using {} sec. HttpSession timeout", timeout);
+
+ Object unsecured = filterConfig.getServletContext().getAttribute(ATTRIBUTE_UNSECURED_PATHS);
+ if (unsecured != null) {
+ unsecuredPaths = (String[]) unsecured;
+ }
+ contextPath = filterConfig.getServletContext().getContextPath();
+ baseFullPath = ServletHelpers.webContextPath(contextPath, basePath);
+ String appContextPath = filterConfig.getInitParameter(PARAM_APPLICATION_CONTEXT_PATH);
+ if (appContextPath != null && !appContextPath.isEmpty()) {
+ // Quarkus doesn't have any context path, but we still need to have /hawtio base
+ baseFullPath = ServletHelpers.cleanPath(appContextPath);
+ }
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ LOG.trace("Applying {}", getClass().getSimpleName());
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ HttpSession session = httpRequest.getSession(false);
+
+ // TOCHECK: we may consider using this filter only for GET requests
+
+ // this is full path, which includes context path and path info
+ String requestURI = ServletHelpers.cleanPath(httpRequest.getRequestURI());
+ // this is a path without context path and should be everything after "/hawtio" (or "/actuator/hawtio")
+ String hawtioPath = requestURI.length() < baseFullPath.length() ? ""
+ : requestURI.substring(baseFullPath.length());
+ // this is a path after context path:
+ // - everything after /hawtio for hawtio.war
+ // - everything after /actuator/hawtio for default Spring Boot config. but also handles cases with custom
+ // management base and/or mapping and if same port is used for main and management server, then dispatcher
+ // servlet's base path is also handled (and this is the case why we CAN'T use req.getServletPath())
+ String path = requestURI.length() < contextPath.length() ? ""
+ : requestURI.substring(contextPath.length());
+
+ boolean loginPage = hawtioPath.startsWith("/login");
+
+ if (baseFullPath.equals(requestURI)) {
+ // explicitly change to "/" for Tomcat, so it's redirected if not authenticated
+ // for Spring Boot, "/actuator/hawtio" is changed to "/actuator/hawtio/", so we know it's top-level
+ path = "/".equals(basePath) ? "/" : basePath + "/";
+ }
+
+ LOG.debug("Check if path [{}] requires redirect", path);
+
+ // 0) whatever the configuration, accessing css, index.html, fonts, ..., should be handled normally
+ if (!loginPage && !isSecuredPath(path)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ // "/login" is similar to URLs like "/connect/remote" or "/jmx", because it's client-side router URL
+ // but it should be handled differently
+ // "/" (and even "") are also treated as client-side routes, because there's no path among
+ // io.hawt.web.auth.AuthenticationConfiguration.UNSECURED_PATHS that "/" startsWith()
+
+ // 1) no authentication or external authentication
+ if (!authConfiguration.isEnabled() || authConfiguration.isExternalAuthenticationEnabled()) {
+ if (loginPage) {
+ // /login should be redirected to "/", so user doesn't see login page blinking
+ redirector.doRedirect(httpRequest, httpResponse, "/");
+ } else {
+ // other client-side router URLs should be forwarded to /index.html
+ // and it should be done here, not with:
+ // 1) 404 + /index.html hack for WAR
+ // 2) io.hawt.springboot.HawtioEndpoint.forwardHawtioRequestToIndexHtml() for SpringBoot (RegExp)
+ // 3) io.hawt.quarkus.filters.HawtioQuarkusPathFilter.doFilter() for Quarkus (RegExp)
+ redirector.doForward(httpRequest, httpResponse, "/index.html");
+ }
+ return;
+ }
+
+ // 2) when already authenticated - the same as #1, but please leave for clarity
+ if (AuthSessionHelpers.isAuthenticated(session)) {
+ if (loginPage) {
+ // /login should be redirected to "/", so authenticated user doesn't see login page blinking
+ redirector.doRedirect(httpRequest, httpResponse, "/");
+ } else {
+ // other client-side router URLs should be forwarded to /index.html
+ redirector.doForward(httpRequest, httpResponse, "/index.html");
+ }
+ return;
+ }
+
+ // 3) not authenticated access to /login - forward to /index.html to get the login page
+ if (loginPage) {
+ redirector.doForward(httpRequest, httpResponse, "/index.html");
+ return;
+ }
+
+ // try pre-emptive authentication, so when user sees index.html page (Hawtio client), jolokia requests
+ // will already be authenticated.
+ // TOCHECK: be careful with Jolokia/Proxy requests
+ boolean preemptiveAuth = tryAuthenticateRequest(httpRequest, session);
+
+ // 4) at this stage we have to redirect to /login if authentication failed
+ if (!preemptiveAuth) {
+ // redirect to login page - no other option. Actually later we're forwarded to /index.html
+ // but user sees /login in URL, so it's good
+ redirector.doRedirect(httpRequest, httpResponse, AuthenticationConfiguration.LOGIN_URL);
+ return;
+ }
+
+ // 5) we're authenticated pre-emptively - so the same situation as #2.b
+ redirector.doForward(httpRequest, httpResponse, "/index.html");
+ }
+
+ /**
+ * Preemptive authentication with side effects (storing subject and used within forcibly created session)
+ *
+ * @param request
+ * @param session
+ * @return
+ */
+ boolean tryAuthenticateRequest(HttpServletRequest request, HttpSession session) {
+ AuthenticateResult result = new Authenticator(request, authConfiguration).authenticate(
+ subject -> {
+ String username = AuthHelpers.getUsername(subject);
+ LOG.info("Logging in user: {}", username);
+ AuthSessionHelpers.setup(session != null ? session :
+ request.getSession(true), subject, username, timeout);
+ });
+
+ return result.is(AuthenticateResult.Type.AUTHORIZED);
+ }
+
+ /**
+ * "Secured path" means it's not one of resources paths (css, js, images, fonts, index.html itself, ...) but
+ * it's also not a "specially protected path" (/jolokia, /proxy, /auth, ...). It means that all
+ * "client-side routes" are "secured" (like /jmx, /connect/remote, ...).
+ * Mind that "/login" is one of such client-side secured routes/paths, but it's handled in special way.
+ *
+ * @param path
+ * @return
+ */
+ boolean isSecuredPath(String path) {
+ return Arrays.stream(unsecuredPaths).noneMatch(path::startsWith);
+ }
+
+ public void setRedirector(Redirector redirector) {
+ this.redirector = redirector;
+ }
+}
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/LoginRedirectFilter.java b/hawtio-system/src/main/java/io/hawt/web/auth/LoginRedirectFilter.java
deleted file mode 100644
index f5f2359c60..0000000000
--- a/hawtio-system/src/main/java/io/hawt/web/auth/LoginRedirectFilter.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package io.hawt.web.auth;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import jakarta.servlet.Filter;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.FilterConfig;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpSession;
-
-import io.hawt.system.AuthHelpers;
-import io.hawt.system.AuthenticateResult;
-import io.hawt.system.Authenticator;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Redirect to login page when authentication is enabled.
- */
-public class LoginRedirectFilter implements Filter {
-
- private static final Logger LOG = LoggerFactory.getLogger(LoginRedirectFilter.class);
-
- public final String ATTRIBUTE_UNSECURED_PATHS = "unsecuredPaths";
-
- private int timeout;
- private AuthenticationConfiguration authConfiguration;
-
- private String[] unsecuredPaths;
-
- private Redirector redirector = new Redirector();
-
- public LoginRedirectFilter() {
- this(AuthenticationConfiguration.UNSECURED_PATHS);
- }
-
- public LoginRedirectFilter(String[] unsecuredPaths) {
- this.unsecuredPaths = unsecuredPaths;
- }
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- authConfiguration = AuthenticationConfiguration.getConfiguration(filterConfig.getServletContext());
- timeout = AuthSessionHelpers.getSessionTimeout(filterConfig.getServletContext());
- LOG.info("Hawtio loginRedirectFilter is using {} sec. HttpSession timeout", timeout);
-
- Object unsecured = filterConfig.getServletContext().getAttribute(ATTRIBUTE_UNSECURED_PATHS);
- if (unsecured != null) {
- unsecuredPaths = (String[]) unsecured;
- }
- }
-
- @Override
- public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
- LOG.trace("Applying {}", getClass().getSimpleName());
-
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
- HttpSession session = httpRequest.getSession(false);
- String path = httpRequest.getServletPath();
-
- if (isRedirectRequired(session, path, httpRequest)) {
- redirector.doRedirect(httpRequest, httpResponse, AuthenticationConfiguration.LOGIN_URL);
- } else {
- chain.doFilter(request, response);
- }
- }
-
- private boolean isRedirectRequired(HttpSession session, String path, HttpServletRequest request) {
- LOG.debug("Check if path [{}] requires redirect", path);
- return authConfiguration.isEnabled()
- && !authConfiguration.isKeycloakEnabled()
- && !authConfiguration.isOidcEnabled()
- && !AuthSessionHelpers.isSpringSecurityEnabled()
- && !AuthSessionHelpers.isAuthenticated(session)
- && isSecuredPath(path)
- && !tryAuthenticateRequest(request, session);
- }
-
- boolean tryAuthenticateRequest(HttpServletRequest request, HttpSession session) {
- AuthenticateResult result = new Authenticator(request, authConfiguration).authenticate(
- subject -> {
- String username = AuthHelpers.getUsername(subject);
- LOG.info("Logging in user: {}", username);
- AuthSessionHelpers.setup(session != null ? session :
- request.getSession(true), subject, username, timeout);
- });
-
- return result.is(AuthenticateResult.Type.AUTHORIZED);
- }
-
- boolean isSecuredPath(String path) {
- return Arrays.stream(unsecuredPaths).noneMatch(path::startsWith);
- }
-
- public void setRedirector(Redirector redirector) {
- this.redirector = redirector;
- }
-}
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/LogoutServlet.java b/hawtio-system/src/main/java/io/hawt/web/auth/LogoutServlet.java
index 9813589695..3f19c6fbfa 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/LogoutServlet.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/LogoutServlet.java
@@ -36,7 +36,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
addHeaders(response);
request.logout();
- if (AuthSessionHelpers.isSpringSecurityEnabled()) {
+ if (AuthenticationConfiguration.isSpringSecurityEnabled()) {
AuthSessionHelpers.clear(request, authConfiguration, false);
redirector.doRedirect(request, response, "/");
} else {
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/RelativeRequestUri.java b/hawtio-system/src/main/java/io/hawt/web/auth/RelativeRequestUri.java
index e8a59fd7a3..4d2d16230d 100755
--- a/hawtio-system/src/main/java/io/hawt/web/auth/RelativeRequestUri.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/RelativeRequestUri.java
@@ -2,10 +2,9 @@
import java.util.regex.Pattern;
+import io.hawt.web.ServletHelpers;
import jakarta.servlet.http.HttpServletRequest;
-import io.hawt.util.Strings;
-
/**
* URI path relative to a given index. An index represents the number of path
* components (portions of request uri separated by '/' character) preceding the
@@ -62,11 +61,13 @@ public RelativeRequestUri(final HttpServletRequest request,
throw new IllegalArgumentException("pathIndex is negative");
}
- final String requestUri = Strings.webContextPath(request.getRequestURI());
+ final String requestUri = ServletHelpers.webContextPath(request.getRequestURI());
int start = request.getContextPath().length();
if (start < requestUri.length() && requestUri.charAt(start) == '/') {
start++;
}
+ // when context path is "/hawtio" and full URI is "/hawtio/jolokia/version",
+ // "start" now points to "j"
if (pathIndex != 0) {
int c = 0;
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/SessionExpiryFilter.java b/hawtio-system/src/main/java/io/hawt/web/auth/SessionExpiryFilter.java
index 5f2616d44a..60f3132b2d 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/SessionExpiryFilter.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/SessionExpiryFilter.java
@@ -14,7 +14,6 @@
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
-import io.hawt.util.Strings;
import io.hawt.web.ServletHelpers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,12 +48,7 @@ public class SessionExpiryFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
authConfiguration = AuthenticationConfiguration.getConfiguration(filterConfig.getServletContext());
- String servletPath = (String) filterConfig.getServletContext().getAttribute(SERVLET_PATH);
- if (servletPath == null) {
- this.pathIndex = 0; // assume hawtio is served from root
- } else {
- this.pathIndex = Strings.webContextPath(servletPath).replaceAll("[^/]+", "").length();
- }
+ this.pathIndex = ServletHelpers.hawtioPathIndex(filterConfig.getServletContext());
}
@Override
diff --git a/hawtio-system/src/main/java/io/hawt/web/auth/UserServlet.java b/hawtio-system/src/main/java/io/hawt/web/auth/UserServlet.java
index d9b785151b..faf27d4a56 100644
--- a/hawtio-system/src/main/java/io/hawt/web/auth/UserServlet.java
+++ b/hawtio-system/src/main/java/io/hawt/web/auth/UserServlet.java
@@ -51,7 +51,7 @@ protected String getUsername(HttpServletRequest request, HttpServletResponse res
}
// For Spring Security
- if (AuthSessionHelpers.isSpringSecurityEnabled()) {
+ if (AuthenticationConfiguration.isSpringSecurityEnabled()) {
return request.getRemoteUser();
}
diff --git a/hawtio-system/src/test/java/io/hawt/util/StringsTest.java b/hawtio-system/src/test/java/io/hawt/util/StringsTest.java
index ccdf366039..67b99ab99d 100644
--- a/hawtio-system/src/test/java/io/hawt/util/StringsTest.java
+++ b/hawtio-system/src/test/java/io/hawt/util/StringsTest.java
@@ -3,20 +3,14 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Properties;
-import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
-import static org.junit.jupiter.params.provider.Arguments.arguments;
public class StringsTest {
@@ -115,114 +109,4 @@ public void customProperties() {
}
}
- public static class CleanPathTest {
-
- public static Stream params() {
- return Stream.of(
- arguments("", ""),
- arguments(" ", " "),
- arguments("/", "/"),
- arguments("a", "a"),
- arguments("/a", "/a"),
- arguments("a/", "a"),
- arguments("/a/", "/a"),
- arguments("//a/", "/a"),
- arguments("/a//", "/a"),
- arguments("//a//", "/a"),
- arguments("/a/b/", "/a/b"),
- arguments("/a///b/", "/a/b")
- );
- }
-
- @ParameterizedTest
- @MethodSource("params")
- public void test(String input, String expected) {
- assertThat(Strings.cleanPath(input), equalTo(expected));
- }
- }
-
-
- public static class WebContextPathFromSingleComponentTest {
-
- public static Stream params() {
- return Stream.of(
- arguments(null, ""),
- arguments("", ""),
- arguments(" ", "/ "),
- arguments("/", ""),
- arguments("a", "/a"),
- arguments("/a", "/a"),
- arguments("a/", "/a"),
- arguments("/a/", "/a"),
- arguments("//a/", "/a"),
- arguments("/a//", "/a"),
- arguments("//a//", "/a"),
- arguments("/a/b/", "/a/b"),
- arguments("/a///b/", "/a/b")
- );
- }
-
- @ParameterizedTest
- @MethodSource("params")
- public void test(String input, String expected) {
- assertThat(Strings.webContextPath((input)), equalTo(expected));
- }
- }
-
-
- public static class WebContextPathFromMultipleComponentsTest {
-
- public static class Parameters {
- private final String input;
- private final String expected;
- private final String more;
-
- private Parameters(String input, String more, String expected) {
- this.input = input;
- this.expected = expected;
- this.more = more;
- }
-
- public Parameters(String input, String expected) {
- this.input = input;
- this.expected = expected;
- this.more = null;
- }
- }
-
- public static Stream params() {
- return Stream.of(
- new Parameters(null, ""),
- new Parameters("", ""),
- new Parameters(" ", "/ "),
- new Parameters("/", ""),
- new Parameters("a", "/a"),
- new Parameters("/a", "/a"),
- new Parameters("a/", "/a"),
- new Parameters("/a/", "/a"),
- new Parameters("//a/", "/a"),
- new Parameters("/a//", "/a"),
- new Parameters("//a//", "/a"),
- new Parameters(null, null, ""),
- new Parameters(null, "a", "/a"),
- new Parameters("a", null, "/a"),
- new Parameters("a", "b", "/a/b"),
- new Parameters("/a", "b", "/a/b"),
- new Parameters("a", "/b", "/a/b"),
- new Parameters("/a", "/b", "/a/b"),
- new Parameters("/a/", "b", "/a/b"),
- new Parameters("/a/", "/b", "/a/b"),
- new Parameters("/a/", "/b/", "/a/b"),
- new Parameters("/a//", "/b//", "/a/b")
- );
- }
-
-
- @ParameterizedTest
- @MethodSource("params")
- public void test(Parameters args) {
- assertThat(Strings.webContextPath(args.input, args.more), equalTo(args.expected));
- }
- }
-
}
diff --git a/hawtio-system/src/test/java/io/hawt/web/ServletHelpersTest.java b/hawtio-system/src/test/java/io/hawt/web/ServletHelpersTest.java
index caa9329a1d..e21ec7058c 100644
--- a/hawtio-system/src/test/java/io/hawt/web/ServletHelpersTest.java
+++ b/hawtio-system/src/test/java/io/hawt/web/ServletHelpersTest.java
@@ -8,17 +8,25 @@
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Map;
+import java.util.stream.Stream;
+import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import static io.hawt.web.ServletHelpers.HEADER_HAWTIO_FORBIDDEN_REASON;
+import static io.hawt.web.auth.SessionExpiryFilter.SERVLET_PATH;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class ServletHelpersTest {
@@ -71,4 +79,137 @@ public void testDoForbidden() throws IOException {
verify(httpResponse).setHeader(HEADER_HAWTIO_FORBIDDEN_REASON, ForbiddenReason.NONE.name());
verify(httpResponse).flushBuffer();
}
+
+ public static class CleanPathTest {
+
+ public static Stream params() {
+ return Stream.of(
+ arguments("", ""),
+ arguments(" ", " "),
+ arguments("/", "/"),
+ arguments("a", "a"),
+ arguments("/a", "/a"),
+ arguments("a/", "a"),
+ arguments("/a/", "/a"),
+ arguments("//a/", "/a"),
+ arguments("/a//", "/a"),
+ arguments("//a//", "/a"),
+ arguments("/a/b/", "/a/b"),
+ arguments("/a///b/", "/a/b")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ public void test(String input, String expected) {
+ assertThat(ServletHelpers.cleanPath(input), equalTo(expected));
+ }
+ }
+
+ public static class WebContextPathFromSingleComponentTest {
+
+ public static Stream params() {
+ return Stream.of(
+ arguments(null, ""),
+ arguments("", ""),
+ arguments(" ", "/ "),
+ arguments("/", ""),
+ arguments("a", "/a"),
+ arguments("/a", "/a"),
+ arguments("a/", "/a"),
+ arguments("/a/", "/a"),
+ arguments("//a/", "/a"),
+ arguments("/a//", "/a"),
+ arguments("//a//", "/a"),
+ arguments("/a/b/", "/a/b"),
+ arguments("/a///b/", "/a/b")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ public void test(String input, String expected) {
+ assertThat(ServletHelpers.webContextPath((input)), equalTo(expected));
+ }
+ }
+
+ public static class WebContextPathFromMultipleComponentsTest {
+
+ public static class Parameters {
+ private final String input;
+ private final String expected;
+ private final String more;
+
+ private Parameters(String input, String more, String expected) {
+ this.input = input;
+ this.expected = expected;
+ this.more = more;
+ }
+
+ public Parameters(String input, String expected) {
+ this.input = input;
+ this.expected = expected;
+ this.more = null;
+ }
+ }
+
+ public static Stream params() {
+ return Stream.of(
+ new Parameters(null, ""),
+ new Parameters("", ""),
+ new Parameters(" ", "/ "),
+ new Parameters("/", ""),
+ new Parameters("a", "/a"),
+ new Parameters("/a", "/a"),
+ new Parameters("a/", "/a"),
+ new Parameters("/a/", "/a"),
+ new Parameters("//a/", "/a"),
+ new Parameters("/a//", "/a"),
+ new Parameters("//a//", "/a"),
+ new Parameters(null, null, ""),
+ new Parameters(null, "a", "/a"),
+ new Parameters("a", null, "/a"),
+ new Parameters("a", "b", "/a/b"),
+ new Parameters("/a", "b", "/a/b"),
+ new Parameters("a", "/b", "/a/b"),
+ new Parameters("/a", "/b", "/a/b"),
+ new Parameters("/a/", "b", "/a/b"),
+ new Parameters("/a/", "/b", "/a/b"),
+ new Parameters("/a/", "/b/", "/a/b"),
+ new Parameters("/a//", "/b//", "/a/b")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ public void test(Parameters args) {
+ assertThat(ServletHelpers.webContextPath(args.input, args.more), equalTo(args.expected));
+ }
+ }
+
+ public static class HawtioPathIndexTest {
+
+ public static Stream params() {
+ return Stream.of(
+ // [ SERVLET_PATH attribute, full request URI, expected Hawtio path position ]
+ arguments(null, "/jolokia", 0),
+ arguments(null, "/jolokia/version", 0),
+ arguments("", "/jolokia", 0),
+ arguments("", "/jolokia/version", 0),
+ arguments("/x", "/x/jolokia", 1),
+ arguments("/mgmt/actuator/hawtio", "/mgmt/actuator/hawtio/jolokia", 3)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ public void test(String servletPathAttribute, String uri, int pathIndex) {
+ ServletContext ctx = mock(ServletContext.class);
+ if (servletPathAttribute != null) {
+ when(ctx.getAttribute(SERVLET_PATH)).thenReturn(servletPathAttribute);
+ }
+ assertThat(ServletHelpers.hawtioPathIndex(ctx), equalTo(pathIndex));
+ }
+ }
+
}
diff --git a/hawtio-system/src/test/java/io/hawt/web/auth/LoginRedirectFilterTest.java b/hawtio-system/src/test/java/io/hawt/web/auth/ClientRouteRedirectFilterTest.java
similarity index 51%
rename from hawtio-system/src/test/java/io/hawt/web/auth/LoginRedirectFilterTest.java
rename to hawtio-system/src/test/java/io/hawt/web/auth/ClientRouteRedirectFilterTest.java
index 9d1f955572..73d6793809 100644
--- a/hawtio-system/src/test/java/io/hawt/web/auth/LoginRedirectFilterTest.java
+++ b/hawtio-system/src/test/java/io/hawt/web/auth/ClientRouteRedirectFilterTest.java
@@ -13,9 +13,9 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class LoginRedirectFilterTest {
+public class ClientRouteRedirectFilterTest {
- private LoginRedirectFilter loginRedirectFilter;
+ private ClientRouteRedirectFilter clientRouteRedirectFilter;
private FilterConfig filterConfig;
private ServletContext servletContext;
@@ -27,32 +27,32 @@ public void setUp() {
@Test
public void shouldTestSecuredPaths() throws Exception {
- loginRedirectFilter = new LoginRedirectFilter();
+ clientRouteRedirectFilter = new ClientRouteRedirectFilter();
when(filterConfig.getServletContext()).thenReturn(servletContext);
when(servletContext.getAttribute(ConfigManager.CONFIG_MANAGER)).thenReturn(new ConfigManager());
- loginRedirectFilter.init(filterConfig);
- assertTrue(loginRedirectFilter.isSecuredPath("/d"));
- assertTrue(loginRedirectFilter.isSecuredPath("/e/f"));
- assertTrue(loginRedirectFilter.isSecuredPath("/auth"));
+ clientRouteRedirectFilter.init(filterConfig);
+ assertTrue(clientRouteRedirectFilter.isSecuredPath("/d"));
+ assertTrue(clientRouteRedirectFilter.isSecuredPath("/e/f"));
+ assertTrue(clientRouteRedirectFilter.isSecuredPath("/auth"));
// these paths are not "secured" from the PoV of LoginRedirectFilter - however these are protected by
// AuthenticationFilter
- assertFalse(loginRedirectFilter.isSecuredPath("/jolokia"));
- assertFalse(loginRedirectFilter.isSecuredPath("/jolokia/read/java.lang:type=Runtime/Name"));
- assertFalse(loginRedirectFilter.isSecuredPath("/favicon.ico"));
- assertFalse(loginRedirectFilter.isSecuredPath("/auth/login"));
- assertFalse(loginRedirectFilter.isSecuredPath("/auth/logout"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/jolokia"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/jolokia/read/java.lang:type=Runtime/Name"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/favicon.ico"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/auth/login"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/auth/logout"));
}
@Test
public void customizedUnsecuredPaths() throws Exception {
String[] unsecuredPaths = { "/hawtio/auth", "/hawtio/secret/content" };
- loginRedirectFilter = new LoginRedirectFilter(unsecuredPaths);
+ clientRouteRedirectFilter = new ClientRouteRedirectFilter(unsecuredPaths, "/");
when(filterConfig.getServletContext()).thenReturn(servletContext);
when(servletContext.getAttribute(ConfigManager.CONFIG_MANAGER)).thenReturn(new ConfigManager());
- loginRedirectFilter.init(filterConfig);
- assertTrue(loginRedirectFilter.isSecuredPath("/d"));
- assertTrue(loginRedirectFilter.isSecuredPath("/e/f"));
- assertFalse(loginRedirectFilter.isSecuredPath("/hawtio/auth/login"));
- assertFalse(loginRedirectFilter.isSecuredPath("/hawtio/secret/content/secure"));
+ clientRouteRedirectFilter.init(filterConfig);
+ assertTrue(clientRouteRedirectFilter.isSecuredPath("/d"));
+ assertTrue(clientRouteRedirectFilter.isSecuredPath("/e/f"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/hawtio/auth/login"));
+ assertFalse(clientRouteRedirectFilter.isSecuredPath("/hawtio/secret/content/secure"));
}
}
diff --git a/hawtio-war-minimal/src/main/webapp/WEB-INF/web.xml b/hawtio-war-minimal/src/main/webapp/WEB-INF/web.xml
index 83413653c3..f5a952dd07 100644
--- a/hawtio-war-minimal/src/main/webapp/WEB-INF/web.xml
+++ b/hawtio-war-minimal/src/main/webapp/WEB-INF/web.xml
@@ -107,11 +107,11 @@
- LoginRedirectFilter
- io.hawt.web.auth.LoginRedirectFilter
+ ClientRouteRedirectFilter
+ io.hawt.web.auth.ClientRouteRedirectFilter
- LoginRedirectFilter
+ ClientRouteRedirectFilter
/*
diff --git a/hawtio-war/src/main/webapp/WEB-INF/web.xml b/hawtio-war/src/main/webapp/WEB-INF/web.xml
index 83413653c3..9c20bd03fb 100644
--- a/hawtio-war/src/main/webapp/WEB-INF/web.xml
+++ b/hawtio-war/src/main/webapp/WEB-INF/web.xml
@@ -107,11 +107,11 @@
- LoginRedirectFilter
- io.hawt.web.auth.LoginRedirectFilter
+ ClientRouteRedirectFilter
+ io.hawt.web.auth.ClientRouteRedirectFilter
- LoginRedirectFilter
+ ClientRouteRedirectFilter
/*
@@ -245,11 +245,6 @@
io.hawt.HawtioContextListener
-
- 404
- /index.html
-
-
index.html
diff --git a/platforms/quarkus/deployment/src/main/java/io/hawt/quarkus/deployment/HawtioProcessor.java b/platforms/quarkus/deployment/src/main/java/io/hawt/quarkus/deployment/HawtioProcessor.java
index 8e74c0adb0..04432b4b96 100644
--- a/platforms/quarkus/deployment/src/main/java/io/hawt/quarkus/deployment/HawtioProcessor.java
+++ b/platforms/quarkus/deployment/src/main/java/io/hawt/quarkus/deployment/HawtioProcessor.java
@@ -13,12 +13,11 @@
import io.hawt.quarkus.HawtioRecorder;
import io.hawt.quarkus.auth.HawtioQuarkusAuthenticator;
import io.hawt.quarkus.filters.HawtioQuarkusAuthenticationFilter;
-import io.hawt.quarkus.filters.HawtioQuarkusLoginRedirectFilter;
-import io.hawt.quarkus.filters.HawtioQuarkusPathFilter;
+import io.hawt.quarkus.filters.HawtioQuarkusClientRouteRedirectFilter;
import io.hawt.quarkus.servlets.HawtioQuakusLoginServlet;
import io.hawt.quarkus.servlets.HawtioQuakusLogoutServlet;
import io.hawt.web.auth.AuthenticationFilter;
-import io.hawt.web.auth.LoginRedirectFilter;
+import io.hawt.web.auth.ClientRouteRedirectFilter;
import io.hawt.web.auth.LoginServlet;
import io.hawt.web.auth.LogoutServlet;
import io.hawt.web.filters.BaseTagHrefFilter;
@@ -82,7 +81,7 @@ public class HawtioProcessor {
private static final Map WEB_XML_OVERRIDES = Map.of(
LoginServlet.class.getName(), HawtioQuakusLoginServlet.class.getName(),
LogoutServlet.class.getName(), HawtioQuakusLogoutServlet.class.getName(),
- LoginRedirectFilter.class.getName(), HawtioQuarkusLoginRedirectFilter.class.getName(),
+ ClientRouteRedirectFilter.class.getName(), HawtioQuarkusClientRouteRedirectFilter.class.getName(),
AuthenticationFilter.class.getName(), HawtioQuarkusAuthenticationFilter.class.getName()
);
@@ -149,12 +148,14 @@ private void registerFilters(WebMetaData webMetaData, BuildProducer listener) {
diff --git a/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusLoginRedirectFilter.java b/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusClientRouteRedirectFilter.java
similarity index 86%
rename from platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusLoginRedirectFilter.java
rename to platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusClientRouteRedirectFilter.java
index e0ef14a3f5..1f0f492dc0 100644
--- a/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusLoginRedirectFilter.java
+++ b/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusClientRouteRedirectFilter.java
@@ -7,11 +7,11 @@
import io.hawt.quarkus.HawtioConfig;
import io.hawt.web.auth.AuthenticationConfiguration;
-import io.hawt.web.auth.LoginRedirectFilter;
+import io.hawt.web.auth.ClientRouteRedirectFilter;
import io.hawt.web.auth.Redirector;
import io.quarkus.arc.Arc;
-public class HawtioQuarkusLoginRedirectFilter extends LoginRedirectFilter {
+public class HawtioQuarkusClientRouteRedirectFilter extends ClientRouteRedirectFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
diff --git a/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusPathFilter.java b/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusPathFilter.java
deleted file mode 100644
index ae550b18aa..0000000000
--- a/platforms/quarkus/runtime/src/main/java/io/hawt/quarkus/filters/HawtioQuarkusPathFilter.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package io.hawt.quarkus.filters;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.Objects;
-
-import jakarta.servlet.Filter;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-import io.hawt.quarkus.HawtioConfig;
-import io.hawt.util.IOHelper;
-import io.hawt.web.ServletHelpers;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Forwards all React router route URLs to index.html.
- *
- * Ignores jolokia paths and other Hawtio resources.
- */
-public class HawtioQuarkusPathFilter implements Filter {
-
- private static final Logger LOG = LoggerFactory.getLogger(HawtioQuarkusPathFilter.class);
-
- private static final String FILTERED_PATH_PATTERN = "^/(?:(?!\\bjolokia\\b|auth|proxy|keycloak|css|fonts|img|js|user|static|\\.).)*";
-
- private static final String FILTERED_PATH_HAWTCONFIG = "/hawtconfig.json";
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
- LOG.trace("Applying {}", getClass().getSimpleName());
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- String path = httpRequest.getRequestURI().substring(HawtioConfig.DEFAULT_CONTEXT_PATH.length());
-
- // TODO: Is there a better way to handle hawtconfig.json from classpath in Quarkus?
- if (path.equals(FILTERED_PATH_HAWTCONFIG)) {
- LOG.debug("path = {} -- reading from classpath", path);
- String content = loadHawtconfigFromHawtioStatic();
- if (content != null) {
- ServletHelpers.sendJSONResponse((HttpServletResponse) response, content);
- return;
- }
- } else if (path.matches(FILTERED_PATH_PATTERN)) {
- LOG.debug("path = {} -- matched", path);
- httpRequest.getRequestDispatcher(HawtioConfig.DEFAULT_CONTEXT_PATH + "/index.html").forward(request, response);
- return;
- }
-
- LOG.debug("path = {} -- not matched", path);
- chain.doFilter(request, response);
- }
-
- // TODO: We might not need to load hawtconfig.json from hawtio-static. For Quarkus, static resources can be loaded from META-INF/resources.
- private static String loadHawtconfigFromHawtioStatic() {
- String hawtioStaticPath = String.format("classpath:/hawtio-static%s", FILTERED_PATH_HAWTCONFIG);
- try (InputStream is = ServletHelpers.loadFile(hawtioStaticPath);
- BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) {
- LOG.debug("path = {} -- classpath resource found", hawtioStaticPath);
- return IOHelper.readFully(reader);
- } catch (Exception e) {
- LOG.debug("path = {} -- classpath resource not found: {}", hawtioStaticPath, e.getMessage());
- }
- return null;
- }
-}
diff --git a/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootEndpointIT.java b/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootEndpointIT.java
index fdea30f4a1..f3f4e42697 100644
--- a/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootEndpointIT.java
+++ b/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootEndpointIT.java
@@ -62,7 +62,7 @@ public void testHawtioAuthenticationEnabled() {
client.get().uri(properties.getJolokiaPath(false)).exchange()
.expectStatus().isForbidden();
- client.get().uri(properties.getHawtioPath(false)).exchange()
+ client.get().uri(properties.getHawtioPath(false) + "/").exchange()
.expectStatus().isFound()
.expectHeader().valueMatches("Location", loginRedirectUrlRegex);
diff --git a/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootTestSupport.java b/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootTestSupport.java
index 14af5ded9c..cda2dd751d 100644
--- a/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootTestSupport.java
+++ b/platforms/springboot/src/integration-test/java/io/hawt/springboot/HawtioSpringBootTestSupport.java
@@ -5,7 +5,7 @@
import java.util.List;
import java.util.Objects;
-import io.hawt.util.Strings;
+import io.hawt.web.ServletHelpers;
import org.assertj.core.api.Assertions;
import org.jolokia.support.spring.actuator.JolokiaEndpointAutoConfiguration;
import org.junit.jupiter.api.BeforeEach;
@@ -21,8 +21,10 @@
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.test.util.TestSocketUtils;
import org.springframework.test.web.reactive.server.WebTestClient;
+import reactor.netty.http.client.HttpClient;
public class HawtioSpringBootTestSupport {
@@ -69,7 +71,7 @@ protected void assertHawtioEndpointPaths(AssertableWebApplicationContext context
}
public void testHawtioEndpoint(AssertableWebApplicationContext context, TestProperties properties, boolean isCustomManagementPortConfigured) {
- getTestClient(context)
+ getTestClient(context, true)
.get().uri(properties.getHawtioPath(isCustomManagementPortConfigured)).exchange()
.expectStatus().isOk()
.expectBody()
@@ -105,11 +107,20 @@ public void testHawtioPluginRequest(AssertableWebApplicationContext context, Tes
}
protected WebTestClient getTestClient(AssertableWebApplicationContext context) {
+ return getTestClient(context, false);
+ }
+
+ protected WebTestClient getTestClient(AssertableWebApplicationContext context, boolean followRedirects) {
Integer port = context.getEnvironment().getProperty("management.server.port", Integer.class);
if (port == null) {
port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class).getWebServer().getPort();
}
- return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build();
+ WebTestClient.Builder builder = WebTestClient.bindToServer().baseUrl("http://localhost:" + port);
+ if (followRedirects) {
+ builder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().followRedirect(true)));
+ }
+
+ return builder.build();
}
protected static class TestProperties {
@@ -175,11 +186,11 @@ private TestProperties(final String contextPath,
if (jolokiaPath != null) {
- this.jolokiaPath = Strings.webContextPath(jolokiaPath);
+ this.jolokiaPath = ServletHelpers.webContextPath(jolokiaPath);
}
if (hawtioPath != null) {
- this.hawtioPath = Strings.webContextPath(hawtioPath);
+ this.hawtioPath = ServletHelpers.webContextPath(hawtioPath);
}
}
@@ -188,20 +199,20 @@ public static TestPropertiesBuilder builder() {
}
public String getHawtioPath(boolean isManagementPortConfigured) {
- return Strings.webContextPath(getBasePath(isManagementPortConfigured), hawtioPath);
+ return ServletHelpers.webContextPath(getBasePath(isManagementPortConfigured), hawtioPath);
}
public String getJolokiaPath(boolean isManagementPortConfigured) {
- return Strings.webContextPath(getBasePath(isManagementPortConfigured), jolokiaPath);
+ return ServletHelpers.webContextPath(getBasePath(isManagementPortConfigured), jolokiaPath);
}
public String getHawtioJolokiaPath(boolean isManagementPortConfigured) {
- return Strings.webContextPath(getHawtioPath(isManagementPortConfigured), "jolokia");
+ return ServletHelpers.webContextPath(getHawtioPath(isManagementPortConfigured), "jolokia");
}
public String getHawtioPluginPath(boolean isManagementPortConfigured) {
- return Strings.webContextPath(getHawtioPath(isManagementPortConfigured), "plugin");
+ return ServletHelpers.webContextPath(getHawtioPath(isManagementPortConfigured), "plugin");
}
public String[] getProperties() {
@@ -210,9 +221,9 @@ public String[] getProperties() {
private String getBasePath(boolean customPortConfigured) {
if (customPortConfigured) {
- return Strings.webContextPath(managementBasePath, managementWebBasePath);
+ return ServletHelpers.webContextPath(managementBasePath, managementWebBasePath);
}
- return Strings.webContextPath(contextPath, servletPath, managementWebBasePath);
+ return ServletHelpers.webContextPath(contextPath, servletPath, managementWebBasePath);
}
private void addProperty(String name, String value) {
diff --git a/platforms/springboot/src/main/java/io/hawt/springboot/EndpointPathResolver.java b/platforms/springboot/src/main/java/io/hawt/springboot/EndpointPathResolver.java
index 32cfd8b9c4..f21b60e9f0 100644
--- a/platforms/springboot/src/main/java/io/hawt/springboot/EndpointPathResolver.java
+++ b/platforms/springboot/src/main/java/io/hawt/springboot/EndpointPathResolver.java
@@ -1,9 +1,8 @@
package io.hawt.springboot;
-import io.hawt.util.Strings;
-
import java.util.Map;
+import io.hawt.web.ServletHelpers;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
@@ -32,7 +31,7 @@ public EndpointPathResolver(final WebEndpointProperties webEndpointProperties,
* an absolute path (starting with {@code /}) within Spring Boot context path. Spring Boot configuration
* is taken into account ({@code spring.mvc.servlet.path} and {@code management.endpoints.web.base-path}).
* Context path (configured with {@code server.servlet.context-path} or {@code management.server.base-path}) is
- * not part of returned path, as all resolved paths are relative to the context.
+ * not a part of the returned path, as all resolved paths are relative to the context.
*
* Spring Boot may run two separate web containers:
* - Main server (with port configured using {@code server.port} property)
@@ -72,7 +71,7 @@ public String resolve(final String endpointName) {
endpointPathMapping = endpointName;
}
- final String webContextPath = Strings.webContextPath(servletPath, basePath, endpointPathMapping);
+ final String webContextPath = ServletHelpers.webContextPath(servletPath, basePath, endpointPathMapping);
return webContextPath.isEmpty() ? "/" : webContextPath;
}
@@ -84,7 +83,7 @@ public String resolveUrlMapping(String endpointName, String... mappings) {
endpointPath = endpointPath.replace(servletPath, "");
}
- return Strings.webContextPath(endpointPath, mappings);
+ return ServletHelpers.webContextPath(endpointPath, mappings);
}
public String resolveContextPath() {
@@ -98,6 +97,6 @@ public String resolveContextPath() {
contextPath = managementServerProperties.getBasePath();
}
- return Strings.webContextPath(contextPath);
+ return ServletHelpers.webContextPath(contextPath);
}
}
diff --git a/platforms/springboot/src/main/java/io/hawt/springboot/HawtioEndpoint.java b/platforms/springboot/src/main/java/io/hawt/springboot/HawtioEndpoint.java
index 9b05fa7c96..eca5dd1893 100644
--- a/platforms/springboot/src/main/java/io/hawt/springboot/HawtioEndpoint.java
+++ b/platforms/springboot/src/main/java/io/hawt/springboot/HawtioEndpoint.java
@@ -2,8 +2,8 @@
import java.util.List;
-import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -14,7 +14,13 @@
import org.springframework.web.util.UriComponents;
/**
- * Spring Boot endpoint to expose Hawtio.
+ * Spring Boot endpoint to expose Hawtio. It is more tightly integrated with Spring MVC than
+ * {@link org.springframework.boot.actuate.endpoint.annotation.Endpoint} and methods annotated with
+ * {@link RequestMapping} are invoked by {@link org.springframework.web.servlet.DispatcherServlet} through
+ * {@link org.springframework.web.servlet.HandlerAdapter}.
+ *
+ * The implication is that {@link RequestMapping} methods are called after DispatcherServlet and after
+ * all mapped Hawtio filters.
*/
@ControllerEndpoint(id = "hawtio")
public class HawtioEndpoint implements WebMvcConfigurer {
@@ -30,6 +36,10 @@ public void setPlugins(final List plugins) {
this.plugins = plugins;
}
+ // forwardHawtioRequestToIndexHtml() with "{path:^(?:(?!\bjolokia\b|auth|css|fonts|img|js|user|static|\.).)*$}/**"
+ // mapping is no longer needed - everything is handled by ClientRouteRedirectFilter
+ // but let's keep it
+
/**
* Forwards all React router route URLs to index.html.
* Ignores jolokia paths and paths for other Hawtio resources.
@@ -37,8 +47,8 @@ public void setPlugins(final List plugins) {
* @return The Spring Web forward directive for the Hawtio index.html resource.
*/
@RequestMapping(
- value = {"", "{path:^(?:(?!\\bjolokia\\b|auth|css|fonts|img|js|user|static|\\.).)*$}/**"},
- produces = MediaType.TEXT_HTML_VALUE)
+ value = {"", "{path:^(?:(?!\\bjolokia\\b|auth|css|fonts|img|js|user|static|\\.).)*$}/**"},
+ produces = MediaType.TEXT_HTML_VALUE)
public String forwardHawtioRequestToIndexHtml(HttpServletRequest request) {
final String path = endpointPath.resolve("hawtio");
@@ -51,8 +61,8 @@ public String forwardHawtioRequestToIndexHtml(HttpServletRequest request) {
}
final UriComponents uriComponents = ServletUriComponentsBuilder.fromPath(path)
- .path("/index.html")
- .build();
+ .path("/index.html")
+ .build();
return "forward:" + uriComponents.getPath();
}
@@ -82,4 +92,3 @@ public void addResourceHandlers(final ResourceHandlerRegistry registry) {
// @formatter:on
}
}
-
diff --git a/platforms/springboot/src/main/java/io/hawt/springboot/HawtioManagementConfiguration.java b/platforms/springboot/src/main/java/io/hawt/springboot/HawtioManagementConfiguration.java
index 786465cba7..cc1d079abe 100644
--- a/platforms/springboot/src/main/java/io/hawt/springboot/HawtioManagementConfiguration.java
+++ b/platforms/springboot/src/main/java/io/hawt/springboot/HawtioManagementConfiguration.java
@@ -14,7 +14,7 @@
import io.hawt.system.ConfigManager;
import io.hawt.web.auth.AuthenticationConfiguration;
import io.hawt.web.auth.AuthenticationFilter;
-import io.hawt.web.auth.LoginRedirectFilter;
+import io.hawt.web.auth.ClientRouteRedirectFilter;
import io.hawt.web.auth.LoginServlet;
import io.hawt.web.auth.LogoutServlet;
import io.hawt.web.auth.Redirector;
@@ -33,6 +33,7 @@
import io.hawt.web.filters.XFrameOptionsFilter;
import io.hawt.web.filters.XXSSProtectionFilter;
import io.hawt.web.proxy.ProxyServlet;
+import jakarta.servlet.http.HttpServletResponse;
import org.jolokia.support.spring.actuator.JolokiaEndpoint;
import org.jolokia.support.spring.actuator.JolokiaEndpointAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
@@ -45,7 +46,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.mvc.AbstractUrlViewController;
import static io.hawt.web.filters.BaseTagHrefFilter.PARAM_APPLICATION_CONTEXT_PATH;
@@ -57,7 +61,7 @@ public class HawtioManagementConfiguration {
// a path within Spring server or management server that's the "base" of hawtio actuator.
// By default it should be "/actuator/hawtio", but may be affected by application.properties settings
- // (for example management.endpoints.web.base-path which defaults to "/actuator")
+ // (for example management.endpoints.web.base-path which defaults to "/actuator", but can be customized)
private final String hawtioPath;
public HawtioManagementConfiguration(final EndpointPathResolver pathResolver) {
@@ -74,6 +78,19 @@ public ConfigManager hawtioConfigManager(final HawtioProperties hawtioProperties
return new ConfigManager(hawtioProperties.get()::get);
}
+ @Bean
+ public SimpleUrlHandlerMapping hawtioWelcomeFiles(final EndpointPathResolver pathResolver) {
+ AbstractController abstractController = new AbstractController() {
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ return new ModelAndView("forward:" + pathResolver.resolve("hawtio") + "/index.html");
+ }
+ };
+ // https://github.com/hawtio/hawtio/issues/3382
+ // order=10 to handle "/actuator/hawtio/" URL as forward to "/actuator/hawtio/index.html" for consistency
+ return new SimpleUrlHandlerMapping(Map.of(pathResolver.resolve("hawtio") + "/", abstractController), 10);
+ }
+
@Bean
@ConditionalOnBean(JolokiaEndpoint.class)
@ConditionalOnExposedEndpoint(name = "jolokia")
@@ -113,20 +130,26 @@ public Redirector redirector() {
// Filters
// -------------------------------------------------------------------------
+ // use @Order annotation to ensure filter mapping as in web.xml. method invocation order _may_ be JVM dependent
+
/**
* Since Spring Boot 3.0, paths with trailing slash are not automatically processed
* and need to be explicitly configured for handling them. This Spring Boot
- * specific filter redirects requests for hawtio/ to hawtio/index.html.
+ * specific filter used to redirect {@code /actuator/hawtio/} requests to {@code /actuator/hawtio/index.html},
+ * but now it used for consistency with WAR deployments - to redirect {@code /actuator/hawtio} to
+ * {@code /actuator/hawtio/}. Then the request is being processed by {@link ClientRouteRedirectFilter}.
*/
@Bean
+ @Order(0)
public FilterRegistrationBean trailingSlashFilter(final Redirector redirector) {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new TrailingSlashFilter(redirector));
- filter.addUrlPatterns(hawtioPath + "/");
+ filter.addUrlPatterns(hawtioPath);
return filter;
}
@Bean
+ @Order(1)
public FilterRegistrationBean sessionExpiryFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new SessionExpiryFilter());
@@ -135,6 +158,7 @@ public FilterRegistrationBean sessionExpiryFilter() {
}
@Bean
+ @Order(2)
public FilterRegistrationBean cacheFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new CacheHeadersFilter());
@@ -143,6 +167,7 @@ public FilterRegistrationBean cacheFilter() {
}
@Bean
+ @Order(3)
public FilterRegistrationBean hawtioCorsFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new CORSFilter());
@@ -151,6 +176,7 @@ public FilterRegistrationBean hawtioCorsFilter() {
}
@Bean
+ @Order(4)
public FilterRegistrationBean xframeOptionsFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new XFrameOptionsFilter());
@@ -159,6 +185,7 @@ public FilterRegistrationBean xframeOptionsFilter() {
}
@Bean
+ @Order(5)
public FilterRegistrationBean xxssProtectionFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new XXSSProtectionFilter());
@@ -167,6 +194,7 @@ public FilterRegistrationBean xxssProtectionFilter() {
}
@Bean
+ @Order(6)
public FilterRegistrationBean xContentTypeOptionsFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new XContentTypeOptionsFilter());
@@ -175,6 +203,7 @@ public FilterRegistrationBean xContentTypeOptionsFilt
}
@Bean
+ @Order(7)
public FilterRegistrationBean contentSecurityPolicyFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new ContentSecurityPolicyFilter());
@@ -183,6 +212,7 @@ public FilterRegistrationBean contentSecurityPolicy
}
@Bean
+ @Order(8)
public FilterRegistrationBean strictTransportSecurityFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new StrictTransportSecurityFilter());
@@ -191,6 +221,7 @@ public FilterRegistrationBean strictTransportSecu
}
@Bean
+ @Order(9)
public FilterRegistrationBean publicKeyPinningFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new PublicKeyPinningFilter());
@@ -199,6 +230,7 @@ public FilterRegistrationBean publicKeyPinningFilter() {
}
@Bean
+ @Order(10)
public FilterRegistrationBean referrerPolicyFilter() {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new ReferrerPolicyFilter());
@@ -207,6 +239,7 @@ public FilterRegistrationBean referrerPolicyFilter() {
}
@Bean
+ @Order(11)
@ConditionalOnBean(JolokiaEndpoint.class)
@ConditionalOnExposedEndpoint(name = "jolokia")
public FilterRegistrationBean authenticationFilter(final EndpointPathResolver pathResolver) {
@@ -217,18 +250,30 @@ public FilterRegistrationBean authenticationFilter(final E
return filter;
}
+ /**
+ * This filter was called {@code LoginRedirectFilter}, but now it also handles redirection/forwarding for
+ * client-side routes (React Router), so we no longer need this special RegExp mapped
+ * {@link org.springframework.web.bind.annotation.RequestMapping} annotated method in {@link HawtioEndpoint}.
+ *
+ * @param redirector
+ * @param pathResolver
+ * @return
+ */
@Bean
- public FilterRegistrationBean loginRedirectFilter(final Redirector redirector) {
+ @Order(12)
+ public FilterRegistrationBean clientRouteRedirectFilter(final Redirector redirector,
+ EndpointPathResolver pathResolver) {
final String[] unsecuredPaths = prependContextPath(AuthenticationConfiguration.UNSECURED_PATHS);
- final FilterRegistrationBean filter = new FilterRegistrationBean<>();
- final LoginRedirectFilter loginRedirectFilter = new LoginRedirectFilter(unsecuredPaths);
- loginRedirectFilter.setRedirector(redirector);
- filter.setFilter(loginRedirectFilter);
+ final FilterRegistrationBean filter = new FilterRegistrationBean<>();
+ final ClientRouteRedirectFilter clientRouteRedirectFilter = new ClientRouteRedirectFilter(unsecuredPaths, pathResolver.resolve("hawtio"));
+ clientRouteRedirectFilter.setRedirector(redirector);
+ filter.setFilter(clientRouteRedirectFilter);
filter.addUrlPatterns(hawtioPath + "/*");
return filter;
}
@Bean
+ @Order(13)
public FilterRegistrationBean baseTagHrefFilter(final EndpointPathResolver pathResolver) {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
final BaseTagHrefFilter baseTagHrefFilter = new BaseTagHrefFilter();
@@ -241,6 +286,7 @@ public FilterRegistrationBean baseTagHrefFilter(final Endpoin
}
@Bean
+ @Order(14)
public FilterRegistrationBean flightRecorderDownloadFacade(final EndpointPathResolver pathResolver) {
final FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setFilter(new FlightRecordingDownloadFacade());
diff --git a/platforms/springboot/src/main/java/io/hawt/springboot/TrailingSlashFilter.java b/platforms/springboot/src/main/java/io/hawt/springboot/TrailingSlashFilter.java
index ded3870196..bc6c96193c 100644
--- a/platforms/springboot/src/main/java/io/hawt/springboot/TrailingSlashFilter.java
+++ b/platforms/springboot/src/main/java/io/hawt/springboot/TrailingSlashFilter.java
@@ -14,6 +14,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Filter that simulates Root context access from WAR deployments. I.e., {@code /actuator/hawtio}
+ * is redirected to {@code /actuator/hawtio/}.
+ */
public class TrailingSlashFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(TrailingSlashFilter.class);
@@ -30,16 +34,12 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
- if (!httpRequest.getRequestURI().endsWith("/")) {
- filterChain.doFilter(request, response);
- return;
- }
String query = httpRequest.getQueryString();
if (query != null && !query.isEmpty()) {
- redirector.doRedirect(httpRequest, httpResponse, "/index.html?" + query);
+ redirector.doRedirect(httpRequest, httpResponse, "/?" + query);
} else {
- redirector.doRedirect(httpRequest, httpResponse, "/index.html");
+ redirector.doRedirect(httpRequest, httpResponse, "/");
}
}
}