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: * */ 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: - *
    - *
  1. stripped from all multiple consecutive occurrences of '/' characters
  2. - *
  3. stripped from trailing '/' character(s)
  4. - *
- * - * @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: - *
    - *
  1. prefixed with '/' character
  2. - *
  3. stripped from all multiple consecutive occurrences of '/' characters
  4. - *
  5. stripped from trailing '/' character(s)
  6. - *
- * - * @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: + *
    + *
  1. stripped from all multiple consecutive occurrences of '/' characters
  2. + *
  3. stripped from trailing '/' character(s)
  4. + *
+ * + * @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: + *
    + *
  1. prefixed with '/' character
  2. + *
  3. stripped from all multiple consecutive occurrences of '/' characters
  4. + *
  5. stripped from trailing '/' character(s)
  6. + *
+ * + * @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: + */ + 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: