entry : config.getGatewayPortMappings().entrySet()) {
+ if (entry.getValue().equals(config.getGatewayPort())) {
+ redirectContext = "/" + config.getGatewayPath() + "/" + entry.getKey();
break;
}
}
}
if (defaultTopologyName != null) {
- defaultTopologyRedirectContext = config.getDefaultAppRedirectPath();
- if (defaultTopologyRedirectContext != null
- && defaultTopologyRedirectContext.trim().isEmpty()) {
- defaultTopologyRedirectContext = null;
+ redirectContext = config.getDefaultAppRedirectPath();
+ if (redirectContext != null && redirectContext.trim().isEmpty()) {
+ redirectContext = null;
}
}
- if (defaultTopologyRedirectContext != null) {
- LOG.defaultTopologySetup(defaultTopologyName, defaultTopologyRedirectContext);
+ if (redirectContext != null) {
+ LOG.defaultTopologySetup(defaultTopologyName, redirectContext);
}
- return defaultTopologyRedirectContext;
+ return redirectContext;
}
@Override
- public void handle(final String target, final Request baseRequest,
- final HttpServletRequest request, final HttpServletResponse response)
- throws IOException, ServletException {
- final String baseURI = baseRequest.getRequestURI();
- final int port = baseRequest.getLocalPort();
+ public boolean handle(final Request request, final Response response, final Callback callback)
+ throws Exception {
+ final String requestPath = request.getHttpURI().getPath();
+ final int port = Request.getLocalPort(request);
if (config.isGatewayPortMappingEnabled()
- && config.getGatewayPortMappings().containsValue(port)) {
+ && config.getGatewayPortMappings().containsValue(port)) {
// If Port Mapping feature enabled
- handlePortMapping(target, baseRequest, request, response, port);
- } else if (defaultTopologyRedirectContext != null &&
- !baseURI.startsWith("/" + config.getGatewayPath())) {
+ return handlePortMapping(request, response, callback, port);
+ } else if (defaultTopologyRedirectContext != null
+ && !requestPath.startsWith("/" + config.getGatewayPath())) {
//Backwards compatibility for default topology feature
- handleDefaultTopologyMapping(target, baseRequest, request, response);
+ return handleDefaultTopologyMapping(request, response, callback);
} else {
// case where topology port mapping is not enabled (or improperly configured)
// and no default topology is configured
- super.handle(target, baseRequest, request, response);
+ return super.handle(request, response, callback);
}
}
- private void handlePortMapping(final String target, final Request baseRequest,
- final HttpServletRequest request,
- final HttpServletResponse response, final int port)
- throws IOException, ServletException {
+ private boolean handlePortMapping(final Request request, final Response response,
+ final Callback callback, final int port) throws Exception {
final String topologyName = config.getGatewayPortMappings().entrySet()
.stream()
.filter(e -> e.getValue().equals(port))
@@ -119,37 +114,46 @@ private void handlePortMapping(final String target, final Request baseRequest,
.findFirst()
.orElse(null);
final String gatewayTopologyContext = "/" + config.getGatewayPath() + "/" + topologyName;
- String newTarget = target;
+ final String requestPath = request.getHttpURI().getPath();
- if(!target.contains(gatewayTopologyContext)) {
- newTarget = gatewayTopologyContext + target;
+ // If the request URI does not already contain /{gatewayPath}/{topologyName},
+ // wrap the request to prepend it.
+ if (!requestPath.contains(gatewayTopologyContext)) {
+ final String newPath = gatewayTopologyContext + requestPath;
+ LOG.topologyPortMappingUpdateRequest(requestPath, newPath);
+
+ Request rewritten = rewritePath(request, newPath);
+ return super.handle(rewritten, response, callback);
}
- // if the request does not contain /{gatewayName}/{topologyName}
- if(!baseRequest.getRequestURI().contains(gatewayTopologyContext)) {
- RequestUpdateHandler.ForwardedRequest newRequest = new RequestUpdateHandler.ForwardedRequest(
- request, gatewayTopologyContext);
+ return super.handle(request, response, callback);
+ }
- baseRequest.setPathInfo(gatewayTopologyContext + baseRequest.getPathInfo());
- baseRequest.setURIPathQuery(gatewayTopologyContext + baseRequest.getRequestURI());
+ private boolean handleDefaultTopologyMapping(final Request request, final Response response,
+ final Callback callback) throws Exception {
+ final String requestPath = request.getHttpURI().getPath();
+ final String newPath = defaultTopologyRedirectContext + requestPath;
+ LOG.defaultTopologyForward(requestPath, newPath);
- LOG.topologyPortMappingUpdateRequest(target, newTarget);
- super.handle(newTarget, baseRequest, newRequest, response);
- } else {
- super.handle(newTarget, baseRequest, request, response);
- }
+ Request rewritten = rewritePath(request, newPath);
+ rewritten.setAttribute(DEFAULT_TOPOLOGY_FORWARD_ATTRIBUTE_NAME, "true");
+ return super.handle(rewritten, response, callback);
}
- private void handleDefaultTopologyMapping(final String target, final Request baseRequest,
- final HttpServletRequest request,
- final HttpServletResponse response)
- throws IOException, ServletException {
- RequestUpdateHandler.ForwardedRequest newRequest = new RequestUpdateHandler.ForwardedRequest(
- request, defaultTopologyRedirectContext);
-
- final String newTarget = defaultTopologyRedirectContext + target;
- LOG.defaultTopologyForward(target, newTarget);
- request.setAttribute(DEFAULT_TOPOLOGY_FORWARD_ATTRIBUTE_NAME, "true");
- super.handle(newTarget, baseRequest, newRequest, response);
+ /**
+ * Wraps the given request so that its {@link HttpURI} reports the supplied
+ * path instead of the original one. All other URI components (scheme,
+ * authority, query, fragment) are preserved.
+ */
+ private static Request rewritePath(final Request request, final String newPath) {
+ final HttpURI original = request.getHttpURI();
+ final HttpURI rewritten = HttpURI.build(original).path(newPath).asImmutable();
+
+ return new Request.Wrapper(request) {
+ @Override
+ public HttpURI getHttpURI() {
+ return rewritten;
+ }
+ };
}
-}
+}
\ No newline at end of file
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
index 9d10202047..c8f5e24aab 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
@@ -16,112 +16,56 @@
*/
package org.apache.knox.gateway.filter;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.knox.gateway.GatewayMessages;
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.apache.knox.gateway.services.GatewayServices;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.handler.ScopedHandler;
-
-import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
import java.util.Locale;
/**
- * This handler will be ONLY registered with a specific connector listening on a
- * port that is configured through a property gateway.port.mapping.{topologyName}
- * in gateway-site.xml
- *
- * The function of this connector is to append the right context path and
- * forward the request to the default port.
+ * Container for the {@link ForwardedRequest} wrapper used by the port-mapping /
+ * default-topology request rewriting logic (see KNOX-928).
*
- * See KNOX-928
- *
+ * The former Jetty {@code ScopedHandler} implementation was removed as part of
+ * the Jetty 12 migration because {@code ScopedHandler} no longer exists and the
+ * rewriting logic now lives directly inside {@link PortMappingHelperHandler}.
+ * Only the request wrapper is retained here for backwards compatibility with
+ * existing call sites and tests.
*/
-public class RequestUpdateHandler extends ScopedHandler {
- private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
-
- private String redirectContext;
-
- public RequestUpdateHandler(final GatewayConfig config,
- final String topologyName, final GatewayServices services) {
- super();
-
- if (config == null) {
- throw new IllegalArgumentException("config==null");
- }
- if (services == null) {
- throw new IllegalArgumentException("services==null");
- }
- if (topologyName == null) {
- throw new IllegalArgumentException("topologyName==null");
- }
+public final class RequestUpdateHandler {
- redirectContext = "/" + config.getGatewayPath() + "/" + topologyName;
- }
-
- @Override
- public void doScope(final String target, final Request baseRequest,
- final HttpServletRequest request, final HttpServletResponse response)
- throws IOException, ServletException {
- nextScope(target, baseRequest, request, response);
- }
-
- @Override
- public void doHandle(final String target, final Request baseRequest,
- final HttpServletRequest request, final HttpServletResponse response)
- throws IOException, ServletException {
-
- RequestUpdateHandler.ForwardedRequest newRequest = new RequestUpdateHandler.ForwardedRequest(
- request, redirectContext);
-
- // if the request already has the /{gatewaypath}/{topology} part then skip
- if (!StringUtils.startsWithIgnoreCase(target, redirectContext)) {
- baseRequest.setPathInfo(redirectContext + baseRequest.getPathInfo());
- baseRequest.setURIPathQuery(redirectContext + baseRequest.getRequestURI());
-
- final String newTarget = redirectContext + target;
- LOG.topologyPortMappingUpdateRequest(target, newTarget);
- nextHandle(newTarget, baseRequest, newRequest, response);
- } else {
- nextHandle(target, baseRequest, newRequest, response);
- }
+ private RequestUpdateHandler() {
+ // utility holder for the ForwardedRequest wrapper
}
/**
- * A request wrapper class that wraps a request and adds the context path if
- * needed.
+ * A request wrapper class that wraps a request and prepends a context path
+ * to the request URI.
*/
- static class ForwardedRequest extends HttpServletRequestWrapper {
+ public static class ForwardedRequest extends HttpServletRequestWrapper {
private final String contextPath;
private final String requestURL;
- ForwardedRequest(final HttpServletRequest request, final String contextPath) {
+ public ForwardedRequest(final HttpServletRequest request, final String contextPath) {
super(request);
this.contextPath = contextPath;
this.requestURL = generateRequestURL();
}
/**
- * Handle the case where getServerPort returns -1
+ * Handle the case where getServerPort returns -1.
* @return requestURL
*/
private String generateRequestURL() {
if (getRequest().getServerPort() != -1) {
return String.format(Locale.ROOT, "%s://%s:%s%s",
- getRequest().getScheme(),
- getRequest().getServerName(),
- getRequest().getServerPort(),
- getRequestURI());
+ getRequest().getScheme(),
+ getRequest().getServerName(),
+ getRequest().getServerPort(),
+ getRequestURI());
} else {
return String.format(Locale.ROOT, "%s://%s%s",
- getRequest().getScheme(),
- getRequest().getServerName(),
- getRequestURI());
+ getRequest().getScheme(),
+ getRequest().getServerName(),
+ getRequestURI());
}
}
@@ -140,4 +84,4 @@ public String getContextPath() {
return this.contextPath;
}
}
-}
+}
\ No newline at end of file
From 2bdd7ff34452cfc90d6dd0b5a4b6c121f67a50bb Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Tue, 21 Apr 2026 19:14:12 +0200
Subject: [PATCH 08/43] KNOX-3238: refactor createWebAppContext
---
.../org/apache/knox/gateway/GatewayServer.java | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 5a237e7604..82f443331c 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -75,8 +75,8 @@
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
-import org.eclipse.jetty.webapp.Configuration;
-import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.ee10.webapp.Configuration;
+import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.spec.EnterpriseArchive;
@@ -834,8 +834,10 @@ private WebAppContext createWebAppContext( Topology topology, File warFile, Stri
String contextPath;
contextPath = "/" + Urls.trimLeadingAndTrailingSlashJoin( config.getGatewayPath(), topoName, warPath );
context.setContextPath( contextPath );
- SessionCookieConfig sessionCookieConfig = context.getServletContext().getSessionCookieConfig();
- sessionCookieConfig.setName(KNOXSESSIONCOOKIENAME);
+ // In Jetty 12 the servlet ServletContext.getSessionCookieConfig() is not
+ // reachable until the context is started. The session cookie name is
+ // configured on the SessionHandler directly.
+ context.getSessionHandler().setSessionCookie(KNOXSESSIONCOOKIENAME);
context.setWar( warFile.getAbsolutePath() );
context.setAttribute( GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE, topoName );
context.setAttribute( "org.apache.knox.gateway.frontend.uri", getFrontendUri( context, config ) );
@@ -850,9 +852,11 @@ private WebAppContext createWebAppContext( Topology topology, File warFile, Stri
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
context.setClassLoader(jspClassLoader);
- context.setMaxFormContentSize(config.getJettyMaxFormContentSize());
+ // NOTE: In Jetty 12 the max form content size and max form keys are
+ // configured server-wide via FormFields.MAX_LENGTH_ATTRIBUTE and
+ // FormFields.MAX_FIELDS_ATTRIBUTE (see createJetty()). The per-context
+ // setters were removed; server-level settings apply to all WebApps.
log.setMaxFormContentSize(config.getJettyMaxFormContentSize());
- context.setMaxFormKeys(config.getJettyMaxFormKeys());
log.setMaxFormKeys(config.getJettyMaxFormKeys());
return context;
}
From 1f5e6f0b1ead0772bd17e9cd4a53ff6b8f2e3507 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Tue, 21 Apr 2026 19:19:38 +0200
Subject: [PATCH 09/43] KNOX-3238: refactor createConnector
---
.../java/org/apache/knox/gateway/GatewayServer.java | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 82f443331c..302ba1f5ee 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -68,6 +68,7 @@
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
@@ -441,9 +442,16 @@ private List createConnector(final Server server,
httpsConfig.setSecurePort( connectorPort );
httpsConfig.addCustomizer( new SecureRequestCustomizer() );
SSLService ssl = services.getService(ServiceType.SSL_SERVICE);
- SslContextFactory sslContextFactory = (SslContextFactory)ssl.buildSslContextFactory( config );
+ // In Jetty 12 the ServerConnector(Server, SslContextFactory, ConnectionFactory)
+ // convenience constructor was removed. Build an SslConnectionFactory explicitly
+ // and prepend it to the HttpConnectionFactory so that the SSL layer wraps HTTP/1.1.
+ SslContextFactory.Server sslContextFactory =
+ (SslContextFactory.Server) ssl.buildSslContextFactory( config );
ssl.excludeTopologyFromClientAuth(sslContextFactory, config, topologyName);
- connector = new ServerConnector( server, sslContextFactory, new HttpConnectionFactory( httpsConfig ) );
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory( httpsConfig );
+ SslConnectionFactory sslConnectionFactory =
+ new SslConnectionFactory( sslContextFactory, httpConnectionFactory.getProtocol() );
+ connector = new ServerConnector( server, sslConnectionFactory, httpConnectionFactory );
} else {
connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
}
From 0aabda28683afc13ba2c01cf676161eb713e230b Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 22 Apr 2026 13:21:39 +0200
Subject: [PATCH 10/43] KNOX-3238: refactor Http404ErrorHandler
---
.../java/org/apache/knox/gateway/GatewayServer.java | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 302ba1f5ee..1a0128c744 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -65,6 +65,7 @@
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -74,6 +75,7 @@
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.ee10.webapp.Configuration;
@@ -1242,15 +1244,15 @@ private static class Http404ErrorHandler extends ErrorHandler {
}
@Override
- public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
final int gatewayPrefixLength = ("/" + gatewayPath + "/").length();
- String pathInfo = baseRequest.getPathInfo();
+ String pathInfo = Request.getPathInContext(request);
String topologyName = pathInfo.substring(gatewayPrefixLength, pathInfo.indexOf('/', gatewayPrefixLength));
if (gs.isInactiveTopology(topologyName) && (response.getStatus() == HttpServletResponse.SC_NOT_FOUND)) {
- request.setAttribute("javax.servlet.error.message", "Service Unavailable"); // The default ErrorHandler references this attribute
+ request.setAttribute(ErrorHandler.ERROR_MESSAGE, "Service Unavailable"); // The default ErrorHandler references this attribute
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
- super.doError(target, baseRequest, request, response);
+ super.handle(request, response, callback);
}
}
From 8f3daf8301bcddd3efca536bf952f9c591c63d79 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 22 Apr 2026 15:04:23 +0200
Subject: [PATCH 11/43] KNOX-3238: refactor createWebAppContext pt2
---
.../src/main/java/org/apache/knox/gateway/GatewayServer.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 1a0128c744..564b488761 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -70,7 +70,6 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
-import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
@@ -90,7 +89,6 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
-import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
From d2b7309aa3d424d8f550adee5b69c950397abb7e Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Wed, 22 Apr 2026 17:08:47 +0200
Subject: [PATCH 12/43] KNOX-3238: configure jsp handling
---
.../java/org/apache/knox/gateway/GatewayServer.java | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 564b488761..d96eb61370 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -77,7 +77,6 @@
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
-import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
@@ -631,12 +630,6 @@ private synchronized void start() throws Exception {
// Create Jetty.
createJetty();
- // Add Annotations processing into the Jetty server to support JSPs
- Configuration.ClassList classlist = Configuration.ClassList.setServerDefault( jetty );
- classlist.addBefore(
- "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
- "org.eclipse.jetty.annotations.AnnotationConfiguration" );
-
// Load the current topologies.
// Redeploy autodeploy topologies.
File topologiesDir = calculateAbsoluteTopologiesDir();
@@ -853,11 +846,11 @@ private WebAppContext createWebAppContext( Topology topology, File warFile, Stri
context.setAttribute( GatewayServices.GATEWAY_NAME, config.getGatewayPath());
// Add support for JSPs.
context.setAttribute(
- "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
- ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );
+ "org.eclipse.jetty.ee10.webapp.ContainerIncludeJarPattern",
+ ".*/jakarta\\.servlet\\.jsp\\.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$");
context.setTempDirectory( FileUtils.getFile( warFile, "META-INF", "temp" ) );
context.setErrorHandler( createErrorHandler() );
- context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
+ context.setInitParameter("org.eclipse.jetty.ee10.servlet.Default.dirAllowed", "false");
ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
context.setClassLoader(jspClassLoader);
// NOTE: In Jetty 12 the max form content size and max form keys are
From 88cb0e4d9d64a577dc957a25619cf0078a949ec5 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 21:12:03 +0200
Subject: [PATCH 13/43] KNOX-3238: fix Jetty request log.
---
.../main/java/org/apache/knox/gateway/GatewayServer.java | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index d96eb61370..fb61b12300 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -73,7 +73,6 @@
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
-import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -485,9 +484,6 @@ private static HandlerCollection createHandlers(
}
HandlerCollection handlers = new HandlerCollection();
- RequestLogHandler logHandler = new RequestLogHandler();
-
- logHandler.setRequestLog( new AccessHandler() );
TraceHandler traceHandler = new TraceHandler();
traceHandler.setHandler( contexts );
@@ -532,8 +528,6 @@ private static HandlerCollection createHandlers(
});
}
- handlers.addHandler(logHandler);
-
if(config.isStrictTransportEnabled()) {
final String strictTransportOption = config.getStrictTransportOption();
handlers.addHandler(new HSTSHandler(strictTransportOption));
@@ -696,6 +690,7 @@ private synchronized void start() throws Exception {
}
jetty.setHandler(handlers);
+ jetty.setRequestLog(new AccessHandler());
jetty.addLifeCycleListener(new GatewayServerLifecycleListener(config));
// Start Jetty.
From 112b47c63026d0fbb6c4d756527fff2c1d0ae23b Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 21:24:54 +0200
Subject: [PATCH 14/43] KNOX-3238: fix Jetty lifecycle listener
---
.../src/main/java/org/apache/knox/gateway/GatewayServer.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index fb61b12300..7e4efabe76 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -691,7 +691,7 @@ private synchronized void start() throws Exception {
jetty.setHandler(handlers);
jetty.setRequestLog(new AccessHandler());
- jetty.addLifeCycleListener(new GatewayServerLifecycleListener(config));
+ jetty.addEventListener(new GatewayServerLifecycleListener(config));
// Start Jetty.
try {
From 4030aab8d13a52f2c247cdcb883ae6c7c3e99a4f Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 21:34:12 +0200
Subject: [PATCH 15/43] KNOX-3238: fix contextToHandlerMap initialization
---
.../src/main/java/org/apache/knox/gateway/GatewayServer.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 7e4efabe76..bc212bf4da 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -477,7 +477,7 @@ private static HandlerCollection createHandlers(
final Map contextToHandlerMap = new HashMap<>();
if(contexts.getHandlers() != null) {
- Arrays.asList(contexts.getHandlers()).stream()
+ contexts.getHandlers().stream()
.filter(h -> h instanceof WebAppContext)
.forEach(h -> contextToHandlerMap
.put(((WebAppContext) h).getContextPath(), h));
From a1172956bfefe56c845e2b2d30af9c7237a05ac7 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 21:40:07 +0200
Subject: [PATCH 16/43] KNOX-3238: fix setVirtualHosts initialization
---
.../src/main/java/org/apache/knox/gateway/GatewayServer.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index bc212bf4da..a25d5ddfba 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -520,7 +520,7 @@ private static HandlerCollection createHandlers(
if(context != null) {
((WebAppContext) context).setVirtualHosts(
- new String[] { "@" + entry.getKey().toLowerCase(Locale.ROOT) });
+ List.of("@" + entry.getKey().toLowerCase(Locale.ROOT)));
} else {
// no topology found for mapping entry.getKey()
log.noMappedTopologyFound(entry.getKey());
From 6ea6c1164ad4d955f1d13c50e1072e782f6e7897 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 22:53:00 +0200
Subject: [PATCH 17/43] KNOX-3238: fix Jetty handler chain.
---
.../apache/knox/gateway/GatewayServer.java | 29 +++++++++----------
1 file changed, 13 insertions(+), 16 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index a25d5ddfba..2233400623 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -72,7 +72,6 @@
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -469,7 +468,7 @@ private List createConnector(final Server server,
return connectors;
}
- private static HandlerCollection createHandlers(
+ private static Handler createHandlers(
final GatewayConfig config,
final GatewayServices services,
final ContextHandlerCollection contexts,
@@ -483,8 +482,6 @@ private static HandlerCollection createHandlers(
.put(((WebAppContext) h).getContextPath(), h));
}
- HandlerCollection handlers = new HandlerCollection();
-
TraceHandler traceHandler = new TraceHandler();
traceHandler.setHandler( contexts );
traceHandler.setTracedBodyFilter( System.getProperty( "org.apache.knox.gateway.trace.body.status.filter" ) );
@@ -528,24 +525,24 @@ private static HandlerCollection createHandlers(
});
}
- if(config.isStrictTransportEnabled()) {
- final String strictTransportOption = config.getStrictTransportOption();
- handlers.addHandler(new HSTSHandler(strictTransportOption));
- log.strictTransportHeaderEnabled(strictTransportOption);
- }
-
+ Handler rootHandler = portMappingHandler;
if (config.isWebsocketEnabled()) {
final GatewayWebsocketHandler websocketHandler = new GatewayWebsocketHandler(
config, services);
websocketHandler.setHandler(portMappingHandler);
+ rootHandler = websocketHandler;
+ }
- handlers.addHandler(websocketHandler);
-
- } else {
- handlers.addHandler(portMappingHandler);
+ if(config.isStrictTransportEnabled()) {
+ final String strictTransportOption = config.getStrictTransportOption();
+ HSTSHandler hstsHandler = new HSTSHandler(strictTransportOption);
+ hstsHandler.setHandler(rootHandler);
+ rootHandler = hstsHandler;
+ log.strictTransportHeaderEnabled(strictTransportOption);
}
- return handlers;
+
+ return rootHandler;
}
/**
@@ -660,7 +657,7 @@ private synchronized void start() throws Exception {
// log WARN message and continue
checkMappedTopologiesExist(topologyPortMap, deployedTopologyList);
- final HandlerCollection handlers = createHandlers( config, services, contexts, topologyPortMap);
+ final Handler handlers = createHandlers( config, services, contexts, topologyPortMap);
// Check whether a topology wants dedicated port,
// if yes then we create a connector that listens on the provided port.
From e836629ee3819e710798cbe063a9abedd469647c Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 23:20:18 +0200
Subject: [PATCH 18/43] KNOX-3238: rebase and migrate
gateway-service-restcatalog to use jakarta.servlet API.
---
gateway-service-restcatalog/pom.xml | 4 ++--
.../service/restcatalog/RestCatalogDispatch.java | 6 +++---
.../service/restcatalog/RestCatalogHaDispatch.java | 6 +++---
.../restcatalog/TokenMetadataHeaderHandler.java | 8 ++++----
.../restcatalog/RestCatalogDispatchTestUtils.java | 8 ++++----
.../service/restcatalog/RestCatalogHaDispatchTest.java | 10 +++++-----
6 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/gateway-service-restcatalog/pom.xml b/gateway-service-restcatalog/pom.xml
index 4f67ee25be..0a6a42a91f 100644
--- a/gateway-service-restcatalog/pom.xml
+++ b/gateway-service-restcatalog/pom.xml
@@ -34,8 +34,8 @@
compile
- javax.servlet
- javax.servlet-api
+ jakarta.servlet
+ jakarta.servlet-api
compile
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java
index ca9951d8ef..d800ac5208 100644
--- a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java
@@ -20,9 +20,9 @@
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.knox.gateway.dispatch.ConfigurableDispatch;
-import javax.servlet.FilterConfig;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java
index eb0290a292..dc00a74f1e 100644
--- a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java
@@ -20,9 +20,9 @@
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.knox.gateway.ha.dispatch.ConfigurableHADispatch;
-import javax.servlet.FilterConfig;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RestCatalogHaDispatch extends ConfigurableHADispatch {
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java
index 3511efe9c9..cc65a9e56d 100644
--- a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java
@@ -29,10 +29,10 @@
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
diff --git a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java
index 80180ee96e..2280ca85a9 100644
--- a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java
+++ b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java
@@ -28,10 +28,10 @@
import org.easymock.EasyMock;
import org.eclipse.jetty.http.HttpMethod;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import static org.easymock.EasyMock.anyString;
diff --git a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java
index 93f69f3906..3791a44140 100644
--- a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java
+++ b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java
@@ -32,11 +32,11 @@
import org.junit.Assert;
import org.junit.Test;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletOutputStream;
-import javax.servlet.WriteListener;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
From dff2c64aa14c3a3ca15e8a935828f71501c7d250 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 24 Apr 2026 03:49:04 +0200
Subject: [PATCH 19/43] KNOX-3238: correct GatewayWebsocketHandler,
ProxyWebSocketAdapter, WebshellWebSocketAdapter and JWTValidatorFactory.
---
gateway-server/pom.xml | 5 +
.../apache/knox/gateway/GatewayServer.java | 2 +-
.../webshell/WebshellWebSocketAdapter.java | 35 ++-
.../websockets/GatewayWebsocketHandler.java | 214 +++++++++-----
.../websockets/JWTValidatorFactory.java | 12 +-
.../websockets/ProxyWebSocketAdapter.java | 262 +++++++++---------
6 files changed, 310 insertions(+), 220 deletions(-)
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index c7068aae65..b1c1b44e3c 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -339,6 +339,11 @@
org.eclipse.jetty.websocket
jetty-websocket-jetty-api
+
+ org.eclipse.jetty.websocket
+ jetty-websocket-jetty-server
+
+
org.eclipse.jetty.ee10.websocket
jetty-ee10-websocket-jetty-server
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index 2233400623..d4567e6155 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -1235,7 +1235,7 @@ public boolean handle(Request request, Response response, Callback callback) thr
request.setAttribute(ErrorHandler.ERROR_MESSAGE, "Service Unavailable"); // The default ErrorHandler references this attribute
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
- super.handle(request, response, callback);
+ return super.handle(request, response, callback);
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java b/gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java
index 44fb8d1a6b..7ec9e4ac95 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/webshell/WebshellWebSocketAdapter.java
@@ -18,6 +18,7 @@
package org.apache.knox.gateway.webshell;
import java.io.IOException;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
@@ -35,7 +36,9 @@
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.websockets.JWTValidator;
import org.apache.knox.gateway.websockets.ProxyWebSocketAdapter;
+import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.StatusCode;
public class WebshellWebSocketAdapter extends ProxyWebSocketAdapter {
private Session session;
@@ -61,7 +64,7 @@ public WebshellWebSocketAdapter(ExecutorService pool, GatewayConfig config, JWTV
@SuppressWarnings("PMD.DoNotUseThreads")
@Override
- public void onWebSocketConnect(final Session session) {
+ public void onWebSocketOpen(final Session session) {
this.session = session;
connectionInfo.connect();
pool.execute(this::blockingReadFromHost);
@@ -111,18 +114,28 @@ private void transToHost (String userInput){
}
private void transToClient(String message){
- try {
- session.getRemote().sendString(message);
- } catch (IOException e){
- LOG.onError("Error sending message to client");
+ if (session == null) {
+ LOG.onError("Cannot send message to client; session is null");
cleanup();
+ return;
}
+ // In Jetty 12 the send is asynchronous: report send failures via the Callback,
+ // preserving the old IOException-based error handling semantics.
+ session.sendText(message, Callback.from(
+ () -> { /* success: nothing to do */ },
+ t -> {
+ LOG.onError("Error sending message to client");
+ cleanup();
+ }));
}
@Override
- public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
- throw new UnsupportedOperationException(
- "Websocket for binary messages is not supported at this time.");
+ public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
+ // Binary is not supported for webshell sessions. Complete the callback so the
+ // Jetty 12 demand loop can surface the error and close the connection cleanly
+ // rather than throwing from the listener (which would leave demand stalled).
+ callback.fail(new UnsupportedOperationException(
+ "Websocket for binary messages is not supported at this time."));
}
@Override
@@ -166,10 +179,10 @@ private void audit(String userInput){
}
private void cleanup() {
- if(session != null && session.isOpen()) {
- session.close();
+ if (session != null && session.isOpen()) {
+ session.close(StatusCode.NORMAL, null, Callback.NOOP);
session = null;
}
connectionInfo.disconnect();
}
-}
+}
\ No newline at end of file
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
index a52f425511..3e3d0ce854 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
@@ -28,18 +28,30 @@
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+
+// Jetty 12 Core & WebSocket Imports
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.WebSocketCreator;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
+
import jakarta.websocket.ClientEndpointConfig;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyStore;
-import java.util.Arrays;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -54,8 +66,7 @@
*
* @since 0.10
*/
-public class GatewayWebsocketHandler extends WebSocketHandler
- implements WebSocketCreator {
+public class GatewayWebsocketHandler extends Handler.Wrapper {
private static final WebsocketLogMessages LOG = MessagesFactory
.get(WebsocketLogMessages.class);
@@ -82,7 +93,7 @@ public class GatewayWebsocketHandler extends WebSocketHandler
final GatewayConfig config;
final GatewayServices services;
-
+ private WebSocketUpgradeHandler wsHandler;
public GatewayWebsocketHandler(final GatewayConfig config,
final GatewayServices services) {
@@ -91,27 +102,108 @@ public GatewayWebsocketHandler(final GatewayConfig config,
this.services = services;
pool = Executors.newFixedThreadPool(POOL_SIZE);
this.concurrentWebshells = new AtomicInteger(0);
+ // Set the internal handler as the one we are wrapping
+ setHandler(wsHandler);
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ Server server = getServer();
+ if (server == null) {
+ throw new IllegalStateException("GatewayWebsocketHandler must be attached to a Server before starting");
+ }
+
+ // 1. Get or create the global ServerWebSocketContainer (no ContextHandler needed)
+ ServerWebSocketContainer container = ServerWebSocketContainer.ensure(server, null);
+ configureServerWebSocketContainer(container);
+
+ // 3. Create the UpgradeHandler.
+ this.wsHandler = new WebSocketUpgradeHandler(container);
+
+ // 4. PRESERVE THE CHAIN: This handler currently wraps PortMappingHelperHandler.
+ // We must ensure the internal wsHandler also wraps it, so HTTP traffic flows downwards.
+ this.wsHandler.setHandler(getHandler());
+
+ // Start the internal handler
+ this.wsHandler.start();
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ if (this.wsHandler != null) {
+ this.wsHandler.stop();
+ }
+ super.doStop();
}
@Override
- public void configure(final WebSocketServletFactory factory) {
- factory.setCreator(this);
- factory.getPolicy()
- .setMaxTextMessageSize(config.getWebsocketMaxTextMessageSize());
- factory.getPolicy()
- .setMaxBinaryMessageSize(config.getWebsocketMaxBinaryMessageSize());
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ // Delegate to the WebSocketUpgradeHandler.
+ // If it detects a WebSocket upgrade, it triggers KnoxWebSocketCreator and returns true.
+ // If it's standard HTTP traffic, it delegates to its wrapped handler (PortMappingHelperHandler).
+ return wsHandler.handle(request, response, callback);
+ }
- factory.getPolicy().setMaxBinaryMessageBufferSize(
- config.getWebsocketMaxBinaryMessageBufferSize());
- factory.getPolicy().setMaxTextMessageBufferSize(
- config.getWebsocketMaxTextMessageBufferSize());
+ private class KnoxWebSocketCreator implements WebSocketCreator {
+ @Override
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ try {
+ // 1. Get the raw HTTP URI from the Jetty 12 Request
+ HttpURI httpURI = req.getHttpURI();
- factory.getPolicy()
- .setInputBufferSize(config.getWebsocketInputBufferSize());
+ // 2. Translate the scheme to match Jetty 9's behavior (http -> ws, https -> wss)
+ String wsScheme = "https".equalsIgnoreCase(httpURI.getScheme()) ? "wss" : "ws";
+
+ // 3. Reconstruct the java.net.URI for Knox's internal routing methods
+ final URI requestURI = HttpURI.build(httpURI).scheme(wsScheme).toURI();
+
+ // Now Knox's regex will work perfectly!
+ if (isWebshellRequest(requestURI)) {
+ return handleWebshellRequest(req); // Note: Update handleWebshellRequest to accept ServerUpgradeRequest
+ }
- factory.getPolicy()
- .setAsyncWriteTimeout(config.getWebsocketAsyncWriteTimeout());
- factory.getPolicy().setIdleTimeout(config.getWebsocketIdleTimeout());
+ final String backendURL = getMatchedBackendURL(requestURI);
+ LOG.debugLog("Generated backend URL for websocket connection: " + backendURL);
+
+ final ClientEndpointConfig clientConfig = getClientEndpointConfig(req, backendURL);
+ clientConfig.getUserProperties().put("org.apache.knox.gateway.websockets.truststore", getTruststore());
+
+ return new ProxyWebSocketAdapter(URI.create(backendURL), pool, clientConfig, config);
+
+ } catch (final Exception e) {
+ LOG.failedCreatingWebSocket(e);
+ // In Jetty 12, completing the callback with failure tells the server to reject the upgrade
+ callback.failed(e);
+ return null;
+ }
+ }
+ }
+
+ public void configureServerWebSocketContainer(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(config.getWebsocketMaxTextMessageSize());
+ container.setMaxBinaryMessageSize(config.getWebsocketMaxBinaryMessageSize());
+ container.setInputBufferSize(config.getWebsocketInputBufferSize());
+
+ container.setIdleTimeout(Duration.ofMillis(config.getWebsocketIdleTimeout()));
+
+ // 2. Map ALL incoming requests to our custom Knox routing creator
+ // "regex|^/.*" acts as a catch-all interceptor.
+ container.addMapping("regex|^/.*", new KnoxWebSocketCreator());
+
+ //removed in Jetty 12 container.setMaxBinaryMessageBufferSize(config.getWebsocketMaxBinaryMessageBufferSize());
+ //removed in Jetty 12 container.setMaxTextMessageBufferSize(config.getWebsocketMaxTextMessageBufferSize());
+
+ //removed in Jetty 12 container.setAsyncWriteTimeout(config.getWebsocketAsyncWriteTimeout());
+ // handled by the core HTTP connection idle timeouts and
+ // one can apply it directly to the asynchronous write execution
+ // (e.g., using CompletableFuture.orTimeout(duration, TimeUnit) when we call session.sendText(...)).
+
+ // removed, idle timeout is used or specified in send() methods:
+ // container.setAsyncSendTimeout(config.getWebsocketAsyncWriteTimeout());
+
+ //same as setIdleTimeout: container.setDefaultMaxSessionIdleTimeout(config.getWebsocketIdleTimeout());
}
@@ -119,7 +211,7 @@ private Boolean isWebshellRequest(URI requestURI){
return requestURI.toString().matches(REGEX_WEBSHELL_REQUEST_PATH);
}
- private WebshellWebSocketAdapter handleWebshellRequest(ServletUpgradeRequest req){
+ private WebshellWebSocketAdapter handleWebshellRequest(ServerUpgradeRequest req){
if (config.isWebShellEnabled()){
if (concurrentWebshells.get() >= config.getMaximumConcurrentWebshells()){
throw new RuntimeException("Number of allowed concurrent Web Shell sessions exceeded");
@@ -133,31 +225,6 @@ private WebshellWebSocketAdapter handleWebshellRequest(ServletUpgradeRequest req
throw new RuntimeException("Web Shell not enabled");
}
-
- @Override
- public Object createWebSocket(ServletUpgradeRequest req,
- ServletUpgradeResponse resp) {
- try {
- final URI requestURI = req.getRequestURI();
-
- if (isWebshellRequest(requestURI)) {
- return handleWebshellRequest(req);
- }
-
- // URL used to connect to websocket backend
- final String backendURL = getMatchedBackendURL(requestURI);
- LOG.debugLog("Generated backend URL for websocket connection: " + backendURL);
-
- // Upgrade happens here
- final ClientEndpointConfig clientConfig = getClientEndpointConfig(req);
- clientConfig.getUserProperties().put("org.apache.knox.gateway.websockets.truststore", getTruststore());
- return new ProxyWebSocketAdapter(URI.create(backendURL), pool, clientConfig, config);
- } catch (final Exception e) {
- LOG.failedCreatingWebSocket(e);
- throw new RuntimeException(e);
- }
- }
-
private KeyStore getTruststore() throws KeystoreServiceException {
final KeystoreService ks = this.services
.getService(ServiceType.KEYSTORE_SERVICE);
@@ -175,26 +242,37 @@ private KeyStore getTruststore() throws KeystoreServiceException {
* to be passed to the backend.
* @since 0.14.0
*/
- private ClientEndpointConfig getClientEndpointConfig(final ServletUpgradeRequest req) {
+ private ClientEndpointConfig getClientEndpointConfig(final ServerUpgradeRequest req, final String backendURL) {
return ClientEndpointConfig.Builder.create()
- .configurator(new ClientEndpointConfig.Configurator() {
-
- @Override
- public void beforeRequest(final Map> headers) {
-
- /* Add request headers */
- req.getHeaders().forEach(headers::putIfAbsent);
- try {
- final URI backendURL = new URI(getMatchedBackendURL(req.getRequestURI()));
- headers.put("Host", Arrays.asList(backendURL.getHost() + ":" + backendURL.getPort()));
- } catch (final URISyntaxException e) {
- LOG.onError(String.format(Locale.ROOT,
- "Error getting backend url, this could cause 'Host does not match SNI' exception. Cause: ",
- e.toString()));
- }
- }
- }).build();
+ .configurator(new ClientEndpointConfig.Configurator() {
+
+ @Override
+ public void beforeRequest(final Map> headers) {
+
+ // 1. Safely iterate over Jetty 12 HttpFields and copy them to the Jakarta map
+ for (HttpField field : req.getHeaders()) {
+ headers.computeIfAbsent(field.getName(), k -> new ArrayList<>())
+ .add(field.getValue());
+ }
+
+ // 2. Properly construct and override the Host header
+ try {
+ final URI backendURI = new URI(backendURL);
+
+ // Handle implicit ports (where getPort() returns -1) to prevent "Host: example.com:-1"
+ int port = backendURI.getPort();
+ String hostValue = backendURI.getHost() + (port != -1 ? ":" + port : "");
+
+ headers.put("Host", Collections.singletonList(hostValue));
+
+ } catch (final URISyntaxException e) {
+ LOG.onError(String.format(Locale.ROOT,
+ "Error getting backend url, this could cause 'Host does not match SNI' exception. Cause: %s",
+ e.toString()));
+ }
+ }
+ }).build();
}
/**
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java
index bf97408821..658a2ae918 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java
@@ -30,9 +30,11 @@
import org.apache.knox.gateway.topology.Service;
import org.apache.knox.gateway.topology.Topology;
import org.apache.knox.gateway.util.CertificateUtils;
-import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
import jakarta.servlet.ServletException;
-import java.net.HttpCookie;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.LinkedHashMap;
@@ -47,7 +49,7 @@ public class JWTValidatorFactory {
public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
private static final JWTMessages jwtMessagesLog = MessagesFactory.get(JWTMessages.class);
- public static JWTValidator create(JettyServerUpgradeRequest req, GatewayServices gatewayServices,
+ public static JWTValidator create(ServerUpgradeRequest req, GatewayServices gatewayServices,
GatewayConfig gatewayConfig){
Map params = getParams(gatewayServices);
String cookieName = params.containsKey(KNOXSSO_COOKIE_NAME)? params.get(KNOXSSO_COOKIE_NAME):DEFAULT_SSO_COOKIE_NAME;
@@ -97,8 +99,8 @@ private static Map getParams(GatewayServices gatewayServices){
return params;
}
- private static JWT extractToken(JettyServerUpgradeRequest req, String cookieName){
- List ssoCookies = req.getCookies();
+ private static JWT extractToken(ServerUpgradeRequest req, String cookieName){
+ List ssoCookies = Request.getCookies(req);
if (ssoCookies != null){
for (HttpCookie ssoCookie : ssoCookies) {
if (cookieName.equals(ssoCookie.getName())) {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyWebSocketAdapter.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyWebSocketAdapter.java
index 9cc9847c96..039d454fc2 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyWebSocketAdapter.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyWebSocketAdapter.java
@@ -18,54 +18,68 @@
package org.apache.knox.gateway.websockets;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
-import java.util.List;
+import java.nio.ByteBuffer;
+import java.security.KeyStore;
import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
-import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.WebSocketContainer;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.config.GatewayConfig;
-import org.eclipse.jetty.io.RuntimeIOException;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.ee10.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
+import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
-import org.eclipse.jetty.websocket.api.BatchMode;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
-import java.security.KeyStore;
+
/**
* Handles outbound/inbound Websocket connections and sessions.
*
+ * The frontend (browser <-> Knox) side uses the native Jetty 12
+ * WebSocket API via {@link Session.Listener.AbstractAutoDemanding}. The
+ * backend (Knox <-> upstream) side stays on JSR-356 ({@code jakarta.websocket}).
+ *
* @since 0.10
*/
-public class ProxyWebSocketAdapter extends WebSocketAdapter {
+public class ProxyWebSocketAdapter extends Session.Listener.AbstractAutoDemanding {
protected static final WebsocketLogMessages LOG = MessagesFactory.get(WebsocketLogMessages.class);
- /* URI for the backend */
+ private static final String TRUSTSTORE_USER_PROPERTY =
+ "org.apache.knox.gateway.websockets.truststore";
+
+ /** URI for the backend */
private final URI backend;
- /* Session between the frontend (browser) and Knox */
+ /** Session between the frontend (browser) and Knox */
private Session frontendSession;
- /* Session between the backend (outbound) and Knox */
+ /** Session between the backend (outbound) and Knox */
private jakarta.websocket.Session backendSession;
+ /** JSR-356 client container used to connect to the backend */
private WebSocketContainer container;
protected ExecutorService pool;
- /* Message buffer for holding data frames temporarily in memory till connection is setup.
- Keeping the max size of the buffer as 100 messages for now. */
- private List messageBuffer = new ArrayList<>();
- private Lock remoteLock = new ReentrantLock();
+ /**
+ * Buffer for messages arriving from the backend before the frontend session
+ * is ready. Capped by {@code config.getWebsocketMaxWaitBufferCount()}.
+ */
+ private final List messageBuffer = new ArrayList<>();
+
+ /** Guards the buffer-vs-send transition at open time. */
+ private final Lock remoteLock = new ReentrantLock();
protected final GatewayConfig config;
@@ -73,14 +87,16 @@ public class ProxyWebSocketAdapter extends WebSocketAdapter {
* Used to transmit headers from browser to backend server.
* @since 0.14
*/
- private ClientEndpointConfig clientConfig;
+ private final ClientEndpointConfig clientConfig;
public ProxyWebSocketAdapter(final URI backend, final ExecutorService pool, GatewayConfig config) {
this(backend, pool, null, config);
}
- public ProxyWebSocketAdapter(final URI backend, final ExecutorService pool, final ClientEndpointConfig clientConfig,
- GatewayConfig config) {
+ public ProxyWebSocketAdapter(final URI backend,
+ final ExecutorService pool,
+ final ClientEndpointConfig clientConfig,
+ final GatewayConfig config) {
super();
this.backend = backend;
this.pool = pool;
@@ -89,90 +105,80 @@ public ProxyWebSocketAdapter(final URI backend, final ExecutorService pool, fina
}
@Override
- public void onWebSocketConnect(final Session frontEndSession) {
+ public void onWebSocketOpen(final Session frontEndSession) {
/*
- * Let's connect to the backend, this is where the Backend-to-frontend
- * plumbing takes place
+ * Let's connect to the backend. This is where the Backend-to-Frontend
+ * plumbing takes place.
*/
- container = ContainerProvider.getWebSocketContainer();
- container.setDefaultMaxTextMessageBufferSize(frontEndSession.getPolicy().getMaxTextMessageBufferSize());
- container.setDefaultMaxBinaryMessageBufferSize(frontEndSession.getPolicy().getMaxBinaryMessageBufferSize());
- container.setAsyncSendTimeout(frontEndSession.getPolicy().getAsyncWriteTimeout());
- container.setDefaultMaxSessionIdleTimeout(frontEndSession.getPolicy().getIdleTimeout());
-
- KeyStore ks = null;
- if(clientConfig != null) {
- ks = (KeyStore) clientConfig.getUserProperties().get("org.apache.knox.gateway.websockets.truststore");
+ KeyStore truststore = null;
+ if (clientConfig != null) {
+ truststore = (KeyStore) clientConfig.getUserProperties().get(TRUSTSTORE_USER_PROPERTY);
}
+ container = buildBackendContainer(truststore);
+
/*
- Currently javax.websocket API has no provisions to configure SSL
- https://github.com/eclipse-ee4j/websocket-api/issues/210
- Until that gets fixed we'll have to resort to this.
- */
- if(container instanceof org.eclipse.jetty.websocket.jsr356.ClientContainer &&
- ((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient() != null &&
- ((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient().getSslContextFactory() != null ) {
- ((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient().getHttpClient().getSslContextFactory().setTrustStore(ks);
- LOG.logMessage("Truststore for websocket setup");
- }
+ * Seed sensible defaults on the outbound (JSR-356) container from the
+ * inbound session. In Jetty 12 the payload size / idle timeout knobs live
+ * on the server container (not the session) and are propagated here so
+ * that the backend connection honors the same limits.
+ */
+ container.setDefaultMaxTextMessageBufferSize(config.getWebsocketMaxTextMessageBufferSize());
+ container.setDefaultMaxBinaryMessageBufferSize(config.getWebsocketMaxBinaryMessageBufferSize());
+ container.setAsyncSendTimeout(config.getWebsocketAsyncWriteTimeout());
+ container.setDefaultMaxSessionIdleTimeout(config.getWebsocketIdleTimeout());
final ProxyInboundClient backendSocket = new ProxyInboundClient(getMessageCallback());
- /* build the configuration */
-
/* Attempt Connect */
try {
backendSession = container.connectToServer(backendSocket, clientConfig, backend);
-
LOG.onConnectionOpen(backend.toString());
-
} catch (DeploymentException e) {
LOG.connectionFailed(e);
throw new RuntimeException(e);
} catch (IOException e) {
LOG.connectionFailed(e);
- throw new RuntimeIOException(e);
+ throw new UncheckedIOException(e);
}
remoteLock.lock();
- super.onWebSocketConnect(frontEndSession);
- this.frontendSession = frontEndSession;
-
- final RemoteEndpoint remote = frontEndSession.getRemote();
try {
+ this.frontendSession = frontEndSession;
if (!messageBuffer.isEmpty()) {
- flushBufferedMessages(remote);
-
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
+ flushBufferedMessages();
} else {
LOG.debugLog("Message buffer is empty");
}
- } catch (IOException e) {
- LOG.connectionFailed(e);
- throw new RuntimeIOException(e);
- }
- finally
- {
+ } finally {
remoteLock.unlock();
}
}
- @Override
- public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
- if (isNotConnected()) {
- return;
+ /**
+ * Build the JSR-356 client container and, if a truststore was supplied,
+ * back it with an {@link HttpClient} whose SSL context trusts those certs.
+ *
+ * In Jetty 12 the old {@code org.eclipse.jetty.websocket.jsr356.ClientContainer}
+ * cast is no longer available; the supported way to configure SSL for the
+ * JSR-356 client is to hand {@link JakartaWebSocketClientContainerProvider}
+ * a pre-configured {@link HttpClient}.
+ */
+ private WebSocketContainer buildBackendContainer(final KeyStore truststore) {
+ if (truststore == null) {
+ return JakartaWebSocketClientContainerProvider.getContainer(null);
}
-
- throw new UnsupportedOperationException(
- "Websocket support for binary messages is not supported at this time.");
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ sslContextFactory.setTrustStore(truststore);
+ HttpClient httpClient = new HttpClient();
+ httpClient.setSslContextFactory(sslContextFactory);
+ LOG.logMessage("Truststore for websocket setup");
+ return JakartaWebSocketClientContainerProvider.getContainer(httpClient);
}
@Override
public void onWebSocketText(final String message) {
- if (isNotConnected()) {
+ if (frontendSession == null || !frontendSession.isOpen()) {
return;
}
@@ -181,12 +187,21 @@ public void onWebSocketText(final String message) {
/* Proxy message to backend */
try {
backendSession.getBasicRemote().sendText(message);
-
} catch (IOException e) {
LOG.connectionFailed(e);
}
}
+ @Override
+ public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
+ if (frontendSession == null || !frontendSession.isOpen()) {
+ callback.succeed();
+ return;
+ }
+ callback.fail(new UnsupportedOperationException(
+ "Websocket support for binary messages is not supported at this time."));
+ }
+
@Override
public void onWebSocketClose(int statusCode, String reason) {
super.onWebSocketClose(statusCode, reason);
@@ -200,20 +215,17 @@ public void onWebSocketError(final Throwable t) {
}
/**
- * Cleanup sessions
+ * Cleanup sessions on error.
*/
private void cleanupOnError(final Throwable t) {
-
LOG.onError(t.toString());
if (t.toString().contains("exceeds maximum size")) {
- if(frontendSession != null && frontendSession.isOpen()) {
- frontendSession.close(StatusCode.MESSAGE_TOO_LARGE, t.getMessage());
+ if (frontendSession != null && frontendSession.isOpen()) {
+ frontendSession.close(StatusCode.MESSAGE_TOO_LARGE, t.getMessage(), Callback.NOOP);
}
- }
-
- else {
- if(frontendSession != null && frontendSession.isOpen()) {
- frontendSession.close(StatusCode.SERVER_ERROR, t.getMessage());
+ } else {
+ if (frontendSession != null && frontendSession.isOpen()) {
+ frontendSession.close(StatusCode.SERVER_ERROR, t.getMessage(), Callback.NOOP);
}
cleanup();
}
@@ -235,8 +247,10 @@ public void onConnectionOpen(Object session) {
@Override
public void onConnectionClose(final CloseReason reason) {
try {
- frontendSession.close(reason.getCloseCode().getCode(),
- reason.getReasonPhrase());
+ if (frontendSession != null && frontendSession.isOpen()) {
+ frontendSession.close(reason.getCloseCode().getCode(),
+ reason.getReasonPhrase(), Callback.NOOP);
+ }
} finally {
cleanup();
}
@@ -251,12 +265,12 @@ public void onError(Throwable cause) {
public void onMessageText(String message, Object session) {
LOG.logMessage("[From Backend <---]" + message);
remoteLock.lock();
- final RemoteEndpoint remote = getRemote();
try {
- if (remote == null) {
- LOG.debugLog("Remote endpoint is null");
+ if (frontendSession == null) {
+ LOG.debugLog("Frontend session is null");
if (messageBuffer.size() >= config.getWebsocketMaxWaitBufferCount()) {
- throw new RuntimeIOException("Remote is null and message buffer is full. Cannot buffer anymore ");
+ throw new UncheckedIOException(new IOException(
+ "Frontend session is not ready and message buffer is full."));
}
LOG.debugLog("Buffering message: " + message);
messageBuffer.add(message);
@@ -264,78 +278,52 @@ public void onMessageText(String message, Object session) {
}
/* Proxy message to frontend */
- flushBufferedMessages(remote);
+ flushBufferedMessages();
LOG.debugLog("Sending current message [From Backend <---]: " + message);
- remote.sendString(message);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException e) {
- LOG.connectionFailed(e);
- throw new RuntimeIOException(e);
- }
- finally
- {
+ frontendSession.sendText(message, Callback.NOOP);
+ } finally {
remoteLock.unlock();
}
}
@Override
- public void onMessageBinary(byte[] message, boolean last,
- Object session) {
+ public void onMessageBinary(byte[] message, boolean last, Object session) {
throw new UnsupportedOperationException(
- "Websocket support for binary messages is not supported at this time.");
-
+ "Websocket support for binary messages is not supported at this time.");
}
@Override
public void onMessagePong(jakarta.websocket.PongMessage message, Object session) {
- LOG.logMessage("[From Backend <---]: PING");
+ LOG.logMessage("[From Backend <---]: PONG");
remoteLock.lock();
- final RemoteEndpoint remote = getRemote();
try {
- if (remote == null) {
- LOG.debugLog("Remote endpoint is null");
+ if (frontendSession == null) {
+ LOG.debugLog("Frontend session is null");
return;
}
- /* Proxy Ping message to frontend */
- flushBufferedMessages(remote);
+ /* Proxy Pong message to frontend */
+ flushBufferedMessages();
LOG.logMessage("Sending current PING [From Backend <---]: ");
- remote.sendPing(message.getApplicationData());
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException e) {
- LOG.connectionFailed(e);
- throw new RuntimeIOException(e);
- }
- finally
- {
+ frontendSession.sendPing(message.getApplicationData(), Callback.NOOP);
+ } finally {
remoteLock.unlock();
}
}
-
};
-
}
@SuppressWarnings("PMD.DoNotUseThreads")
private void cleanup() {
- /* do the cleaning business in separate thread so we don't block */
- pool.execute(new Runnable() {
- @Override
- public void run() {
- closeQuietly();
- }
- });
+ /* do the cleaning business in a separate thread so we don't block */
+ pool.execute(this::closeQuietly);
}
private void closeQuietly() {
try {
- if(backendSession != null && !backendSession.isOpen()) {
+ if (backendSession != null && backendSession.isOpen()) {
backendSession.close();
}
} catch (IOException e) {
@@ -350,20 +338,24 @@ private void closeQuietly() {
}
}
- if(frontendSession != null && !frontendSession.isOpen()) {
- frontendSession.close();
+ if (frontendSession != null && frontendSession.isOpen()) {
+ frontendSession.close(StatusCode.NORMAL, null, Callback.NOOP);
}
}
- /*
- * Function to flush buffered messages. Should be called with remoteLock held
+ /**
+ * Flush buffered messages to the frontend session. Must be called while
+ * holding {@link #remoteLock}.
*/
- private void flushBufferedMessages(final RemoteEndpoint remote) throws IOException {
+ private void flushBufferedMessages() {
+ if (messageBuffer.isEmpty()) {
+ return;
+ }
LOG.debugLog("Flushing old buffered messages");
- for(String obj:messageBuffer) {
- LOG.debugLog("Sending old buffered message [From Backend <---]: " + obj);
- remote.sendString(obj);
+ for (String buffered : messageBuffer) {
+ LOG.debugLog("Sending old buffered message [From Backend <---]: " + buffered);
+ frontendSession.sendText(buffered, Callback.NOOP);
}
messageBuffer.clear();
}
-}
+}
\ No newline at end of file
From 4530b74db35e13f181b5d19df0f6b446995af751 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 24 Apr 2026 04:28:16 +0200
Subject: [PATCH 20/43] KNOX-3238: correct JWTValidatorFactory,
GatewayPortMappingConfigTest, KnoxCacheManager and KnoxLdapRealm.
---
.../gateway/shirorealm/KnoxCacheManager.java | 2 +-
.../gateway/shirorealm/KnoxLdapRealm.java | 2 +-
.../gateway/GatewayPortMappingConfigTest.java | 4 +--
.../gateway/websockets/JWTValidatorTest.java | 35 ++++++++++---------
4 files changed, 23 insertions(+), 20 deletions(-)
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxCacheManager.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxCacheManager.java
index 0d39fdf70c..00ca71f1d9 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxCacheManager.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxCacheManager.java
@@ -151,7 +151,7 @@ private static boolean containsOverlappingFileLockException(Throwable throwable)
* @param xmlConfiguration the XML configuration of the cache manager
*/
private void resolveLockConflict(XmlConfiguration xmlConfiguration) {
- Optional> serviceConfig = xmlConfiguration.getServiceCreationConfigurations().stream()
+ Optional> serviceConfig = xmlConfiguration.getServiceCreationConfigurations().stream()
.filter(service -> service instanceof CacheManagerPersistenceConfiguration).findFirst();
if (serviceConfig.isPresent()) {
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
index c4562ae167..8b9f78d1a5 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
@@ -40,12 +40,12 @@
import org.apache.shiro.crypto.hash.Hash;
import org.apache.shiro.crypto.hash.HashRequest;
import org.apache.shiro.crypto.hash.HashService;
-import org.apache.shiro.lang.util.StringUtils;
import org.apache.shiro.realm.ldap.DefaultLdapRealm;
import org.apache.shiro.realm.ldap.LdapContextFactory;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.apache.shiro.subject.MutablePrincipalCollection;
import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.util.StringUtils;
import javax.naming.AuthenticationException;
import javax.naming.Context;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayPortMappingConfigTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayPortMappingConfigTest.java
index 94f5fa6bb8..678a2f91ed 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayPortMappingConfigTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayPortMappingConfigTest.java
@@ -21,7 +21,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -185,7 +185,7 @@ private static void startGatewayServer() throws Exception {
gatewayServer.addConnector(connector);
// workaround so we can add our handler later at runtime
- HandlerCollection handlers = new HandlerCollection(true);
+ ContextHandlerCollection handlers = new ContextHandlerCollection(true);
// add some initial handlers
ContextHandler context = new ContextHandler();
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java
index e79bea6d7c..9353b4290c 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java
@@ -35,7 +35,9 @@
import org.apache.knox.gateway.topology.Topology;
import org.apache.knox.gateway.util.X509CertificateUtil;
import org.easymock.EasyMock;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
import org.junit.After;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -44,7 +46,6 @@
import org.junit.rules.ExpectedException;
import java.lang.reflect.Field;
-import java.net.HttpCookie;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
@@ -112,11 +113,13 @@ public void tearDown() {
}
}
- private void setTokenOnRequest(ServletUpgradeRequest request, SignedJWT jwt){
- HttpCookie cookie1 = new HttpCookie("hadoop-jwt", "garbage");
- HttpCookie cookie2 = new HttpCookie("hadoop-jwt", "ljm" + jwt.serialize());// garbled jwt
- HttpCookie cookie3 = new HttpCookie("hadoop-jwt", jwt.serialize());
- EasyMock.expect(request.getCookies()).andReturn(Arrays.asList(cookie1, cookie2, cookie3)).anyTimes();
+ private void setTokenOnRequest(ServerUpgradeRequest request, SignedJWT jwt){
+ HttpFields mockedHeaders = HttpFields.build()
+ .add(HttpHeader.COOKIE, "hadoop-jwt=garbage")
+ .add(HttpHeader.COOKIE, "hadoop-jwt=ljm" + jwt.serialize())
+ .add(HttpHeader.COOKIE, "hadoop-jwt=" + jwt.serialize())
+ .asImmutable();
+ EasyMock.expect(request.getHeaders()).andReturn(mockedHeaders).anyTimes();
}
private static SignedJWT getJWT(final String issuer,
@@ -190,7 +193,7 @@ public void testValidToken() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, validJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, validJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -211,8 +214,8 @@ public void testMissingToken() throws Exception {
thrown.expect(RuntimeException.class);
thrown.expectMessage("No Valid JWT found");
setUpParams(new HashMap<>());
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
- EasyMock.expect(request.getCookies()).andReturn(null).anyTimes();
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
+ EasyMock.expect(request.getHeaders()).andReturn(HttpFields.EMPTY).anyTimes();
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
EasyMock.expect(authorityService.verifyToken(jwtValidator.getToken(), publicKey)).andReturn(true).anyTimes();
@@ -233,7 +236,7 @@ public void testUnexpectedTokenIssuer() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, unexpectedIssuerJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, unexpectedIssuerJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -255,7 +258,7 @@ public void testExpiredJWT() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, expiredJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, expiredJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -277,7 +280,7 @@ public void testInvalidNBFJWT() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, expiredJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, expiredJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -299,7 +302,7 @@ public void testUnexpectedSigAlg() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, JWSAlgorithm.RS512.getName() );
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, unexpectedSigAlgJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -320,7 +323,7 @@ public void testNoPublicKey() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, validJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, validJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
@@ -342,7 +345,7 @@ public void testFailToVerifyToken() throws Exception{
params.put(JWT_EXPECTED_ISSUER, JWT_TEST_ISSUER);
params.put(JWT_EXPECTED_SIGALG, parsableJWT.getHeader().getAlgorithm().getName());
setUpParams(params);
- ServletUpgradeRequest request = EasyMock.createNiceMock(ServletUpgradeRequest.class);
+ ServerUpgradeRequest request = EasyMock.createNiceMock(ServerUpgradeRequest.class);
setTokenOnRequest(request, parsableJWT);
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
From 377bda143e5c066ba2b03e7b0a86f7dcea1a0615 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:18:26 +0200
Subject: [PATCH 21/43] KNOX-3238: fix websocket tests
---
.../websockets/GatewayWebsocketHandler.java | 2 +-
.../WebshellWebsocketAdapterTest.java | 17 ++-
.../websockets/AbstractWebSocketHandler.java | 100 ++++++++++++++++
.../gateway/websockets/BadBackendTest.java | 6 +-
.../knox/gateway/websockets/BadSocket.java | 38 +++---
.../knox/gateway/websockets/BadUrlTest.java | 4 +-
.../websockets/BigEchoSocketHandler.java | 36 +++---
.../websockets/ConnectionDroppedTest.java | 6 +-
.../knox/gateway/websockets/EchoSocket.java | 64 +++++-----
.../GatewayWebsocketHandlerTest.java | 113 +++++-------------
.../websockets/MessageFailureTest.java | 8 +-
.../websockets/WebsocketEchoHandler.java | 32 +++--
.../websockets/WebsocketEchoTestBase.java | 8 +-
.../WebsocketMultipleConnectionTest.java | 4 +-
.../WebsocketServerInitiatedMessageTest.java | 51 +++-----
.../WebsocketServerInitiatedPingTest.java | 53 ++++----
16 files changed, 283 insertions(+), 259 deletions(-)
create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/websockets/AbstractWebSocketHandler.java
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
index 3e3d0ce854..2c9a3630b8 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
@@ -159,7 +159,7 @@ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse re
// 3. Reconstruct the java.net.URI for Knox's internal routing methods
final URI requestURI = HttpURI.build(httpURI).scheme(wsScheme).toURI();
- // Now Knox's regex will work perfectly!
+ // Now Knox's regex will work
if (isWebshellRequest(requestURI)) {
return handleWebshellRequest(req); // Note: Update handleWebshellRequest to accept ServerUpgradeRequest
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java
index 61e4446ff8..84fbe04346 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/webshell/WebshellWebsocketAdapterTest.java
@@ -27,9 +27,10 @@
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.websockets.JWTValidator;
import org.apache.knox.gateway.websockets.WebsocketLogMessages;
+import org.easymock.Capture;
import org.easymock.EasyMock;
import org.easymock.EasyMockSupport;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
import org.junit.Rule;
import org.junit.Test;
@@ -99,14 +100,18 @@ public void testOnWebSocketConnect() throws Exception{
EasyMock.expect(connectionInfo.getInputStream()).andReturn(inputStreamEmpty).times(1);
Session session = EasyMock.createNiceMock(Session.class);
- RemoteEndpoint remote = EasyMock.createNiceMock(RemoteEndpoint.class);
+ Capture callbackCapture = EasyMock.newCapture();
+ session.sendText(EasyMock.eq(bashOuput), EasyMock.capture(callbackCapture));
+ EasyMock.expectLastCall().andAnswer(() -> {
+ callbackCapture.getValue().succeed();
+ return null;
+ }).anyTimes();
// send to client
- EasyMock.expect(session.getRemote()).andReturn(remote).anyTimes();
- remote.sendString(bashOuput);
+
// clean up
EasyMock.expect(session.isOpen()).andReturn(true).anyTimes();
session.close();
- EasyMock.replay(session, remote);
+ EasyMock.replay(session);
connectionInfo.disconnect();
PowerMock.replay(connectionInfo, ConnectionInfo.class);
@@ -114,7 +119,7 @@ public void testOnWebSocketConnect() throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(10);
AtomicInteger concurrentWebshells = new AtomicInteger(0);
WebshellWebSocketAdapter webshellWebSocketAdapter = new WebshellWebSocketAdapter(pool, gatewayConfig, jwtValidator, concurrentWebshells);
- webshellWebSocketAdapter.onWebSocketConnect(session);
+ webshellWebSocketAdapter.onWebSocketOpen(session);
verifyAll();
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/AbstractWebSocketHandler.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/AbstractWebSocketHandler.java
new file mode 100644
index 0000000000..7a14dde296
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/AbstractWebSocketHandler.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.websockets;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
+import org.eclipse.jetty.websocket.server.WebSocketCreator;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
+
+/**
+ * A base class for Jetty 12 Native WebSocket handlers that abstracts away
+ * the container initialization and upgrade handler lifecycle.
+ */
+public abstract class AbstractWebSocketHandler extends Handler.Wrapper implements WebSocketCreator {
+
+ private WebSocketUpgradeHandler wsHandler;
+
+ @Override
+ protected void doStart() throws Exception {
+ Server server = getServer();
+ if (server == null) {
+ throw new IllegalStateException("WebSocketHandler must be attached to a Server before starting");
+ }
+
+ // Ensure the container exists
+ ServerWebSocketContainer container = ServerWebSocketContainer.ensure(server, null);
+
+ // Let the subclass apply policies (max sizes, timeouts, etc.)
+ configure(container);
+
+ // Use the exposed mapping spec
+ container.addMapping(getMappingSpec(), this);
+
+ // Create, wire, and start the internal UpgradeHandler
+ this.wsHandler = new WebSocketUpgradeHandler(container);
+ this.wsHandler.setHandler(getHandler());
+ this.wsHandler.start();
+
+ super.doStart();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ if (this.wsHandler != null) {
+ this.wsHandler.stop();
+ }
+ super.doStop();
+ }
+
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ return wsHandler.handle(request, response, callback);
+ }
+
+ /**
+ * Defines the URL mapping for this WebSocket handler.
+ * Defaults to catching all requests. Subclasses can override this to specify exact paths.
+ * * @return The Jetty mapping spec (e.g., "/ws/*", "regex|^/.*", etc.)
+ */
+ protected String getMappingSpec() {
+ return "regex|^/.*";
+ }
+
+ /**
+ * Configure the ServerWebSocketContainer policies.
+ * @param container The Jetty 12 WebSocket container
+ */
+ protected abstract void configure(ServerWebSocketContainer container);
+
+ /**
+ * Return the Jetty 12 Session.Listener (the WebSocket) for the given request.
+ * @param req The upgrade request
+ * @param resp The upgrade response
+ * @param callback The callback to signal success/failure of the upgrade
+ * @return The WebSocket instance
+ */
+ @Override
+ public abstract Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback);
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadBackendTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadBackendTest.java
index 5f9058cfae..33784b5b1b 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadBackendTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadBackendTest.java
@@ -32,6 +32,7 @@
import jakarta.websocket.WebSocketContainer;
import java.net.URI;
import java.util.Locale;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -81,8 +82,11 @@ private static void startProxy() throws Exception {
proxy.addConnector(proxyConnector);
/* start Knox with WebsocketAdapter to test */
+ final ExecutorService pool = Executors.newFixedThreadPool(10);
+ final URI backendURI = new URI(BAD_BACKEND);
+
final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(
- new ProxyWebSocketAdapter(new URI(BAD_BACKEND), Executors.newFixedThreadPool(10), gatewayConfig));
+ () -> new ProxyWebSocketAdapter(backendURI, pool, gatewayConfig));
ContextHandler context = new ContextHandler();
context.setContextPath("/");
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadSocket.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadSocket.java
index 0744f8c21b..d459044e8c 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadSocket.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadSocket.java
@@ -17,40 +17,34 @@
*/
package org.apache.knox.gateway.websockets;
-import org.eclipse.jetty.io.RuntimeIOException;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.websocket.api.BatchMode;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import java.nio.ByteBuffer;
+import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
-
-import java.io.IOException;
/**
* Simulate a bad socket.
*
* @since 0.10
*/
-class BadSocket extends WebSocketAdapter {
+class BadSocket extends Session.Listener.AbstractAutoDemanding {
+
+ private Session session;
+
@Override
- public void onWebSocketConnect(final Session session) {
+ public void onWebSocketOpen(final Session session) {
+ super.onWebSocketOpen(session);
+ this.session = session;
}
@Override
- public void onWebSocketBinary(byte[] payload, int offset, int len) {
- if (isNotConnected()) {
+ public void onWebSocketBinary(ByteBuffer payload, Callback callback) {
+ if (session == null || !session.isOpen()) {
+ callback.fail(new IllegalStateException("Session closed"));
return;
}
- try {
- RemoteEndpoint remote = getRemote();
- remote.sendBytes(BufferUtil.toBuffer(payload, offset, len), null);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException x) {
- throw new RuntimeIOException(x);
- }
+ // Echo the binary payload back to the client and complete the callback
+ session.sendBinary(payload, callback);
}
@Override
@@ -60,10 +54,10 @@ public void onWebSocketError(Throwable cause) {
@Override
public void onWebSocketText(String message) {
- if (isNotConnected()) {
+ if (session == null || !session.isOpen()) {
return;
}
// Throw an exception on purpose
throw new RuntimeException("Simulating bad connection ...");
}
-}
+}
\ No newline at end of file
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadUrlTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadUrlTest.java
index d32ec1c013..5582508e83 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadUrlTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BadUrlTest.java
@@ -43,7 +43,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -154,7 +154,7 @@ private static void startGatewayServer() throws Exception {
gatewayServer.addConnector(connector);
/* workaround so we can add our handler later at runtime */
- HandlerCollection handlers = new HandlerCollection(true);
+ ContextHandlerCollection handlers = new ContextHandlerCollection(true);
/* add some initial handlers */
ContextHandler context = new ContextHandler();
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BigEchoSocketHandler.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BigEchoSocketHandler.java
index 58ecec05bb..900d63a631 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BigEchoSocketHandler.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/BigEchoSocketHandler.java
@@ -17,33 +17,31 @@
*/
package org.apache.knox.gateway.websockets;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
+
+import java.util.function.Supplier;
/**
- * A Mock websocket handler that just Echos messages
+ * A websocket handler that just Echos messages
*/
-class BigEchoSocketHandler extends WebSocketHandler implements WebSocketCreator {
- private final WebSocketAdapter socket;
+class BigEchoSocketHandler extends AbstractWebSocketHandler {
+ private final Supplier socketSupplier;
- BigEchoSocketHandler(final WebSocketAdapter socket) {
- this.socket = socket;
+ BigEchoSocketHandler(final Supplier socketSupplier) {
+ this.socketSupplier = socketSupplier;
}
@Override
- public void configure(WebSocketServletFactory factory) {
- factory.getPolicy().setMaxTextMessageSize(66000);
- factory.getPolicy().setMaxTextMessageBufferSize(66000);
- factory.setCreator(this);
+ protected void configure(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(66000);
}
@Override
- public Object createWebSocket(ServletUpgradeRequest req,
- ServletUpgradeResponse resp) {
- return socket;
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ return socketSupplier.get();
}
-}
+}
\ No newline at end of file
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ConnectionDroppedTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ConnectionDroppedTest.java
index 25d3a08513..c462aa0444 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ConnectionDroppedTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ConnectionDroppedTest.java
@@ -31,6 +31,7 @@
import java.io.IOException;
import java.net.URI;
import java.util.Locale;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -95,7 +96,7 @@ private static void startBackend() throws Exception {
/* start backend with Echo socket */
final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(
- new BadSocket());
+ BadSocket::new);
ContextHandler context = new ContextHandler();
context.setContextPath("/");
@@ -120,8 +121,9 @@ private static void startProxy() throws Exception {
proxy.addConnector(proxyConnector);
/* start Knox with WebsocketAdapter to test */
+ final ExecutorService pool = Executors.newFixedThreadPool(10);
final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(
- new ProxyWebSocketAdapter(serverUri, Executors.newFixedThreadPool(10), gatewayConfig));
+ () -> new ProxyWebSocketAdapter(serverUri, pool, gatewayConfig));
ContextHandler context = new ContextHandler();
context.setContextPath("/");
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/EchoSocket.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/EchoSocket.java
index 3e025e7e27..ca82127328 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/EchoSocket.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/EchoSocket.java
@@ -17,55 +17,51 @@
*/
package org.apache.knox.gateway.websockets;
-import java.io.IOException;
-
-import org.eclipse.jetty.io.RuntimeIOException;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.websocket.api.BatchMode;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import java.nio.ByteBuffer;
+import org.eclipse.jetty.websocket.api.Callback;
+import org.eclipse.jetty.websocket.api.Session;
/**
* A simple Echo socket
*/
-public class EchoSocket extends WebSocketAdapter {
+public class EchoSocket extends Session.Listener.AbstractAutoDemanding {
- @Override
- public void onWebSocketBinary(byte[] payload, int offset, int len) {
- if (isNotConnected()) {
- return;
- }
+ private Session session;
- try {
- RemoteEndpoint remote = getRemote();
- remote.sendBytes(BufferUtil.toBuffer(payload, offset, len), null);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException x) {
- throw new RuntimeIOException(x);
- }
+ @Override
+ public void onWebSocketOpen(Session session) {
+ super.onWebSocketOpen(session);
+ this.session = session;
}
+ /**
+ * Jetty 12 Native API uses ByteBuffer for binary frames.
+ * The callback must be completed (which happens automatically by passing it to sendBinary)
+ * to signal Jetty to read the next frame.
+ */
@Override
- public void onWebSocketError(Throwable cause) {
- throw new RuntimeException(cause);
+ public void onWebSocketBinary(ByteBuffer payload, Callback callback) {
+ if (session == null || !session.isOpen()) {
+ callback.fail(new IllegalStateException("Session closed"));
+ return;
+ }
+
+ // Echo the binary payload back to the client
+ session.sendBinary(payload, callback);
}
@Override
public void onWebSocketText(String message) {
- if (isNotConnected()) {
+ if (session == null || !session.isOpen()) {
return;
}
- try {
- RemoteEndpoint remote = getRemote();
- remote.sendString(message, null);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException x) {
- throw new RuntimeIOException(x);
- }
+ // Echo the text message back to the client
+ session.sendText(message, Callback.NOOP);
+ }
+
+ @Override
+ public void onWebSocketError(Throwable cause) {
+ throw new RuntimeException(cause);
}
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
index 47331b8071..df4aed6b33 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
@@ -29,10 +29,10 @@
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
import org.easymock.EasyMock;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -44,10 +44,6 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.Locale;
-import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
@@ -80,9 +76,9 @@ public void testValidWebShellRequest() throws Exception{
EasyMock.expect(gatewayConfig.isWebShellEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.getMaximumConcurrentWebshells()).andReturn(3).anyTimes();
GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class);
- // mock ServletUpgradeRequest and ServletUpgradeResponse
- ServletUpgradeRequest req = createServletUpgradeRequest("wss://localhost:8443/gateway/webshell");
- ServletUpgradeResponse resp = createServletUpgradeResponse();
+ // mock ServerUpgradeRequest and ServerUpgradeResponse
+ ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
+ ServerUpgradeResponse resp = createServerUpgradeResponse();
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
@@ -105,9 +101,9 @@ public void testValidWebShellRequestThroughLB() throws Exception{
EasyMock.expect(gatewayConfig.isWebShellEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.getMaximumConcurrentWebshells()).andReturn(3).anyTimes();
GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class);
- // mock ServletUpgradeRequest and ServletUpgradeResponse
- ServletUpgradeRequest req = createServletUpgradeRequest("wss://www.local.com/gateway/webshell");
- ServletUpgradeResponse resp = createServletUpgradeResponse();
+ // mock ServerUpgradeRequest and ServerUpgradeResponse
+ ServerUpgradeRequest req = createServerUpgradeRequest("wss://www.local.com/gateway/webshell");
+ ServerUpgradeResponse resp = createServerUpgradeResponse();
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
@@ -135,9 +131,9 @@ public void testWebShellRequestWithInvalidJWT() throws Exception{
EasyMock.expect(gatewayConfig.isWebShellEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.getMaximumConcurrentWebshells()).andReturn(3).anyTimes();
GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class);
- // mock ServletUpgradeRequest and ServletUpgradeResponse
- ServletUpgradeRequest req = createServletUpgradeRequest("wss://localhost:8443/gateway/webshell");
- ServletUpgradeResponse resp = createServletUpgradeResponse();
+ // mock ServerUpgradeRequest and ServerUpgradeResponse
+ ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
+ ServerUpgradeResponse resp = createServerUpgradeResponse();
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(false).anyTimes();
@@ -160,79 +156,30 @@ public void testDisabledWebShell() throws Exception{
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(gatewayConfig.isWebShellEnabled()).andReturn(false).anyTimes();
GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class);
- // mock ServletUpgradeRequest and ServletUpgradeResponse
- ServletUpgradeRequest req = createServletUpgradeRequest("wss://localhost:8443/gateway/webshell");
- ServletUpgradeResponse resp = createServletUpgradeResponse();
+ // mock ServerUpgradeRequest and ServerUpgradeResponse
+ ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
+ ServerUpgradeResponse resp = createServerUpgradeResponse();
EasyMock.replay(gatewayServices,gatewayConfig);
GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(gatewayConfig,gatewayServices);
gatewayWebsocketHandler.createWebSocket(req,resp);
}
- private ServletUpgradeRequest createServletUpgradeRequest(String url) throws Exception {
- HttpServletRequest mockRequest = new org.apache.knox.test.mock.MockHttpServletRequest() {
- @Override
- public StringBuffer getRequestURL() {
- return new StringBuffer(url);
- }
- @Override
- public Enumeration getHeaderNames() {
- return Collections.emptyEnumeration();
- }
- @Override
- public Map getParameterMap() {
- return Collections.emptyMap();
- }
- @Override
- public Enumeration getAttributeNames() {
- return Collections.emptyEnumeration();
- }
- @Override
- public Enumeration getLocales() {
- return Collections.emptyEnumeration();
- }
- @Override
- public String getRemoteAddr() {
- return "127.0.0.1";
- }
- @Override
- public String getRemoteHost() {
- return "localhost";
- }
- @Override
- public int getRemotePort() {
- return 1234;
- }
- @Override
- public String getLocalAddr() {
- return "127.0.0.1";
- }
- @Override
- public String getLocalName() {
- return "localhost";
- }
- @Override
- public int getLocalPort() {
- return 8443;
- }
- @Override
- public String getServerName() {
- return "localhost";
- }
- @Override
- public int getServerPort() {
- return 8443;
- }
- @Override
- public String getScheme() {
- return "wss";
- }
- };
- return new ServletUpgradeRequest(mockRequest);
+ private ServerUpgradeRequest createServerUpgradeRequest(String url) throws Exception {
+ ServerUpgradeRequest mockRequest = EasyMock.createNiceMock(ServerUpgradeRequest.class);
+ HttpURI httpURI = HttpURI.build(url);
+
+ // Set the expectations needed by KnoxWebSocketCreator and JWTValidator
+ EasyMock.expect(mockRequest.getHttpURI()).andReturn(httpURI).anyTimes();
+ EasyMock.expect(mockRequest.getHeaders()).andReturn(HttpFields.EMPTY).anyTimes();
+
+ EasyMock.replay(mockRequest);
+ return mockRequest;
}
- private ServletUpgradeResponse createServletUpgradeResponse() {
- HttpServletResponse mockResponse = EasyMock.createNiceMock(HttpServletResponse.class);
+ private ServerUpgradeResponse createServerUpgradeResponse() {
+ ServerUpgradeResponse mockResponse = EasyMock.createNiceMock(ServerUpgradeResponse.class);
+
EasyMock.replay(mockResponse);
- return new ServletUpgradeResponse(mockResponse);
+ return mockResponse;
}
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/MessageFailureTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/MessageFailureTest.java
index 3b1f94fa38..b31201ab5a 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/MessageFailureTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/MessageFailureTest.java
@@ -34,6 +34,7 @@
import jakarta.websocket.WebSocketContainer;
import java.net.URI;
import java.util.Locale;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -130,8 +131,7 @@ private static void startBackend() throws Exception {
backend.addConnector(connector);
/* start backend with Echo socket */
- final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(
- new EchoSocket());
+ final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(EchoSocket::new);
ContextHandler context = new ContextHandler();
context.setContextPath("/");
@@ -156,8 +156,10 @@ private static void startProxy() throws Exception {
proxy.addConnector(proxyConnector);
/* start Knox with WebsocketAdapter to test */
+ final ExecutorService pool = Executors.newFixedThreadPool(10);
+
final BigEchoSocketHandler wsHandler = new BigEchoSocketHandler(
- new ProxyWebSocketAdapter(serverUri, Executors.newFixedThreadPool(10), gatewayConfig));
+ () -> new ProxyWebSocketAdapter(serverUri, pool, gatewayConfig));
ContextHandler context = new ContextHandler();
context.setContextPath("/");
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHandler.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHandler.java
index 1049c9bb71..002504d7d7 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHandler.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHandler.java
@@ -17,27 +17,25 @@
*/
package org.apache.knox.gateway.websockets;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
/**
- * A Mock websocket handler that just Echos messages
+ * A websocket handler that just Echos messages
*
*/
-public class WebsocketEchoHandler extends WebSocketHandler implements WebSocketCreator {
- private final EchoSocket socket = new EchoSocket();
+public class WebsocketEchoHandler extends AbstractWebSocketHandler {
- @Override
- public void configure(WebSocketServletFactory factory) {
- factory.getPolicy().setMaxTextMessageSize(2 * 1024 * 1024);
- factory.setCreator(this);
- }
+ @Override
+ protected void configure(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(2 * 1024 * 1024);
+ }
- @Override
- public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) {
- return socket;
- }
+ @Override
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ // Must return a new instance per connection, as Session is stateful
+ return new EchoSocket();
+ }
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java
index 3c26466bd5..9f857e0c63 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java
@@ -33,11 +33,11 @@
import org.apache.knox.gateway.topology.TopologyListener;
import org.apache.knox.test.TestUtils;
import org.easymock.EasyMock;
+import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import java.io.File;
import java.io.IOException;
@@ -94,7 +94,7 @@ public class WebsocketEchoTestBase {
*/
public static URI serverUri;
- public static WebSocketHandler handler;
+ public static Handler handler;
private static File topoDir;
private static Path dataDir;
@@ -185,7 +185,7 @@ private static void startGatewayServer() throws Exception {
gatewayServer.addConnector(connector);
/* workaround so we can add our handler later at runtime */
- HandlerCollection handlers = new HandlerCollection(true);
+ ContextHandlerCollection handlers = new ContextHandlerCollection(true);
/* add some initial handlers */
ContextHandler context = new ContextHandler();
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketMultipleConnectionTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketMultipleConnectionTest.java
index 0a7bd45f9f..d104b8bb4e 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketMultipleConnectionTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketMultipleConnectionTest.java
@@ -43,7 +43,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -218,7 +218,7 @@ private static void startGatewayServer() throws Exception {
gatewayServer.addConnector(connector);
/* workaround so we can add our handler later at runtime */
- HandlerCollection handlers = new HandlerCollection(true);
+ ContextHandlerCollection handlers = new ContextHandlerCollection(true);
/* add some initial handlers */
ContextHandler context = new ContextHandler();
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedMessageTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedMessageTest.java
index 5ff6b869ed..938f4a45da 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedMessageTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedMessageTest.java
@@ -17,23 +17,17 @@
*/
package org.apache.knox.gateway.websockets;
-import org.eclipse.jetty.io.RuntimeIOException;
-import org.eclipse.jetty.websocket.api.BatchMode;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.WebSocketContainer;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
-import jakarta.websocket.ContainerProvider;
-import jakarta.websocket.WebSocketContainer;
-import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeUnit;
@@ -99,25 +93,23 @@ public void testGatewayServerInitiatedEcho() throws Exception {
* A Mock websocket handler
*
*/
- private static class WebsocketServerInitiatedEchoHandler extends WebSocketHandler implements WebSocketCreator {
- private final ServerInitiatingMessageSocket socket = new ServerInitiatingMessageSocket();
+ private static class WebsocketServerInitiatedEchoHandler extends AbstractWebSocketHandler {
@Override
- public void configure(WebSocketServletFactory factory) {
- factory.getPolicy().setMaxTextMessageSize(2 * 1024 * 1024);
- factory.setCreator(this);
+ protected void configure(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(2 * 1024 * 1024);
}
@Override
- public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) {
- return socket;
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ return new ServerInitiatingMessageSocket();
}
}
/**
* A simple socket initiating message on connect
*/
- private static class ServerInitiatingMessageSocket extends WebSocketAdapter {
+ private static class ServerInitiatingMessageSocket extends Session.Listener.AbstractAutoDemanding {
@Override
public void onWebSocketError(Throwable cause) {
@@ -125,18 +117,13 @@ public void onWebSocketError(Throwable cause) {
}
@Override
- public void onWebSocketConnect(Session sess) {
- super.onWebSocketConnect(sess);
-
- try {
- RemoteEndpoint remote = getRemote();
- remote.sendString("echo", null);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException x) {
- throw new RuntimeIOException(x);
- }
+ public void onWebSocketOpen(Session session) {
+ super.onWebSocketOpen(session);
+
+ // In Jetty 12, we send the text directly on the session and provide a Callback.
+ // We use Callback.NOOP since the original code passed null and ignored success/failure.
+ // BatchMode and manual flushing are handled automatically by the Jetty engine.
+ session.sendText("echo", org.eclipse.jetty.websocket.api.Callback.NOOP);
}
}
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java
index f19b7d30d3..9f38d6f57f 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketServerInitiatedPingTest.java
@@ -17,23 +17,18 @@
*/
package org.apache.knox.gateway.websockets;
-import org.eclipse.jetty.io.RuntimeIOException;
-import org.eclipse.jetty.websocket.api.BatchMode;
-import org.eclipse.jetty.websocket.api.RemoteEndpoint;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.server.WebSocketHandler;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
-import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
+
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.WebSocketContainer;
-import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -101,25 +96,23 @@ public void testGatewayServerInitiatedPing() throws Exception {
* A Mock websocket handler
*
*/
- private static class WebsocketServerInitiatedPingHandler extends WebSocketHandler implements WebSocketCreator {
- private final ServerInitiatingPingSocket socket = new ServerInitiatingPingSocket();
+ private static class WebsocketServerInitiatedPingHandler extends AbstractWebSocketHandler {
@Override
- public void configure(WebSocketServletFactory factory) {
- factory.getPolicy().setMaxTextMessageSize(2 * 1024 * 1024);
- factory.setCreator(this);
+ protected void configure(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(2 * 1024 * 1024);
}
@Override
- public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) {
- return socket;
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ return new ServerInitiatingPingSocket();
}
}
/**
* A simple socket initiating message on connect
*/
- private static class ServerInitiatingPingSocket extends WebSocketAdapter {
+ private static class ServerInitiatingPingSocket extends Session.Listener.AbstractAutoDemanding {
@Override
public void onWebSocketError(Throwable cause) {
@@ -127,25 +120,23 @@ public void onWebSocketError(Throwable cause) {
}
@Override
- public void onWebSocketConnect(Session sess) {
- super.onWebSocketConnect(sess);
+ public void onWebSocketOpen(Session session) {
+ super.onWebSocketOpen(session);
+
try {
Thread.sleep(1000);
- } catch (Exception e) {
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
+
final String textMessage = "PingPong";
final ByteBuffer binaryMessage = ByteBuffer.wrap(
- textMessage.getBytes(StandardCharsets.UTF_8));
+ textMessage.getBytes(StandardCharsets.UTF_8));
- try {
- RemoteEndpoint remote = getRemote();
- remote.sendPing(binaryMessage);
- if (remote.getBatchMode() == BatchMode.ON) {
- remote.flush();
- }
- } catch (IOException x) {
- throw new RuntimeIOException(x);
- }
+ // In Jetty 12, we send the ping directly on the session and provide a Callback.
+ // We use Callback.NOOP since the original code didn't do anything on success/failure.
+ // BatchMode and manual flushing are handled automatically by the Jetty engine.
+ session.sendPing(binaryMessage, org.eclipse.jetty.websocket.api.Callback.NOOP);
}
}
}
From a9f1f91d31b9f4318206752379cc42cd5cedc99f Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 24 Apr 2026 17:05:12 +0200
Subject: [PATCH 22/43] KNOX-3238: moving KnoxWebSocketCreator from
GatewayWebsocketHandler to a top-level class and correcting
WebsocketBackendUrlTest.
---
.../websockets/GatewayWebsocketHandler.java | 268 +---------------
.../websockets/KnoxWebSocketCreator.java | 296 ++++++++++++++++++
.../GatewayWebsocketHandlerTest.java | 27 +-
.../websockets/WebsocketBackendUrlTest.java | 8 +-
4 files changed, 319 insertions(+), 280 deletions(-)
create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
index 2c9a3630b8..5ac866c84c 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
@@ -17,47 +17,18 @@
*/
package org.apache.knox.gateway.websockets;
-import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.GatewayServices;
-import org.apache.knox.gateway.services.ServiceType;
-import org.apache.knox.gateway.services.registry.ServiceDefEntry;
-import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry;
-import org.apache.knox.gateway.services.registry.ServiceRegistry;
-import org.apache.knox.gateway.services.security.KeystoreService;
-import org.apache.knox.gateway.services.security.KeystoreServiceException;
-import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
-// Jetty 12 Core & WebSocket Imports
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
-import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
-import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
-import org.eclipse.jetty.websocket.server.WebSocketCreator;
import org.eclipse.jetty.websocket.server.ServerWebSocketContainer;
import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
-import jakarta.websocket.ClientEndpointConfig;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.security.KeyStore;
import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
/**
* Websocket handler that will handle websocket connection request. This class
@@ -68,29 +39,6 @@
*/
public class GatewayWebsocketHandler extends Handler.Wrapper {
- private static final WebsocketLogMessages LOG = MessagesFactory
- .get(WebsocketLogMessages.class);
-
- public static final String WEBSOCKET_PROTOCOL_STRING = "ws://";
-
- public static final String SECURE_WEBSOCKET_PROTOCOL_STRING = "wss://";
-
- static final String REGEX_SPLIT_CONTEXT = "^((?:[^/]*/){2}[^/]*)";
-
- static final String REGEX_SPLIT_SERVICE_PATH = "^((?:[^/]*/){3}[^/]*)";
-
- static final String REGEX_WEBSHELL_REQUEST_PATH =
- "^(" + SECURE_WEBSOCKET_PROTOCOL_STRING+"|"+WEBSOCKET_PROTOCOL_STRING + ")[^/]+/[^/]+/webshell$";
-
- private static final int POOL_SIZE = 10;
- private final AtomicInteger concurrentWebshells;
-
- /**
- * Manage the threads that are spawned
- * @since 0.13
- */
- private final ExecutorService pool;
-
final GatewayConfig config;
final GatewayServices services;
private WebSocketUpgradeHandler wsHandler;
@@ -100,8 +48,6 @@ public GatewayWebsocketHandler(final GatewayConfig config,
super();
this.config = config;
this.services = services;
- pool = Executors.newFixedThreadPool(POOL_SIZE);
- this.concurrentWebshells = new AtomicInteger(0);
// Set the internal handler as the one we are wrapping
setHandler(wsHandler);
}
@@ -146,41 +92,6 @@ public boolean handle(Request request, Response response, Callback callback) thr
return wsHandler.handle(request, response, callback);
}
- private class KnoxWebSocketCreator implements WebSocketCreator {
- @Override
- public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
- try {
- // 1. Get the raw HTTP URI from the Jetty 12 Request
- HttpURI httpURI = req.getHttpURI();
-
- // 2. Translate the scheme to match Jetty 9's behavior (http -> ws, https -> wss)
- String wsScheme = "https".equalsIgnoreCase(httpURI.getScheme()) ? "wss" : "ws";
-
- // 3. Reconstruct the java.net.URI for Knox's internal routing methods
- final URI requestURI = HttpURI.build(httpURI).scheme(wsScheme).toURI();
-
- // Now Knox's regex will work
- if (isWebshellRequest(requestURI)) {
- return handleWebshellRequest(req); // Note: Update handleWebshellRequest to accept ServerUpgradeRequest
- }
-
- final String backendURL = getMatchedBackendURL(requestURI);
- LOG.debugLog("Generated backend URL for websocket connection: " + backendURL);
-
- final ClientEndpointConfig clientConfig = getClientEndpointConfig(req, backendURL);
- clientConfig.getUserProperties().put("org.apache.knox.gateway.websockets.truststore", getTruststore());
-
- return new ProxyWebSocketAdapter(URI.create(backendURL), pool, clientConfig, config);
-
- } catch (final Exception e) {
- LOG.failedCreatingWebSocket(e);
- // In Jetty 12, completing the callback with failure tells the server to reject the upgrade
- callback.failed(e);
- return null;
- }
- }
- }
-
public void configureServerWebSocketContainer(ServerWebSocketContainer container) {
container.setMaxTextMessageSize(config.getWebsocketMaxTextMessageSize());
container.setMaxBinaryMessageSize(config.getWebsocketMaxBinaryMessageSize());
@@ -190,7 +101,7 @@ public void configureServerWebSocketContainer(ServerWebSocketContainer container
// 2. Map ALL incoming requests to our custom Knox routing creator
// "regex|^/.*" acts as a catch-all interceptor.
- container.addMapping("regex|^/.*", new KnoxWebSocketCreator());
+ container.addMapping("regex|^/.*", new KnoxWebSocketCreator(config, services));
//removed in Jetty 12 container.setMaxBinaryMessageBufferSize(config.getWebsocketMaxBinaryMessageBufferSize());
//removed in Jetty 12 container.setMaxTextMessageBufferSize(config.getWebsocketMaxTextMessageBufferSize());
@@ -207,181 +118,4 @@ public void configureServerWebSocketContainer(ServerWebSocketContainer container
}
- private Boolean isWebshellRequest(URI requestURI){
- return requestURI.toString().matches(REGEX_WEBSHELL_REQUEST_PATH);
- }
-
- private WebshellWebSocketAdapter handleWebshellRequest(ServerUpgradeRequest req){
- if (config.isWebShellEnabled()){
- if (concurrentWebshells.get() >= config.getMaximumConcurrentWebshells()){
- throw new RuntimeException("Number of allowed concurrent Web Shell sessions exceeded");
- }
- JWTValidator jwtValidator = JWTValidatorFactory.create(req, services, config);
- if (jwtValidator.validate()) {
- return new WebshellWebSocketAdapter(pool, config, jwtValidator, concurrentWebshells);
- }
- throw new RuntimeException("No valid token found for Web Shell connection");
- }
- throw new RuntimeException("Web Shell not enabled");
- }
-
- private KeyStore getTruststore() throws KeystoreServiceException {
- final KeystoreService ks = this.services
- .getService(ServiceType.KEYSTORE_SERVICE);
- KeyStore trustKeystore = null;
- trustKeystore = ks.getTruststoreForHttpClient();
- if (trustKeystore == null) {
- trustKeystore = ks.getKeystoreForGateway();
- }
- return trustKeystore;
- }
-
-
- /**
- * Returns a {@link ClientEndpointConfig} config that contains the headers
- * to be passed to the backend.
- * @since 0.14.0
- */
- private ClientEndpointConfig getClientEndpointConfig(final ServerUpgradeRequest req, final String backendURL) {
-
- return ClientEndpointConfig.Builder.create()
- .configurator(new ClientEndpointConfig.Configurator() {
-
- @Override
- public void beforeRequest(final Map> headers) {
-
- // 1. Safely iterate over Jetty 12 HttpFields and copy them to the Jakarta map
- for (HttpField field : req.getHeaders()) {
- headers.computeIfAbsent(field.getName(), k -> new ArrayList<>())
- .add(field.getValue());
- }
-
- // 2. Properly construct and override the Host header
- try {
- final URI backendURI = new URI(backendURL);
-
- // Handle implicit ports (where getPort() returns -1) to prevent "Host: example.com:-1"
- int port = backendURI.getPort();
- String hostValue = backendURI.getHost() + (port != -1 ? ":" + port : "");
-
- headers.put("Host", Collections.singletonList(hostValue));
-
- } catch (final URISyntaxException e) {
- LOG.onError(String.format(Locale.ROOT,
- "Error getting backend url, this could cause 'Host does not match SNI' exception. Cause: %s",
- e.toString()));
- }
- }
- }).build();
- }
-
- /**
- * This method looks at the context path and returns the backend websocket
- * url. If websocket url is found it is used as is, or we default to
- * ws://{host}:{port} which might or might not be right.
- * @param requestURI url to match
- * @return Websocket backend url
- */
- protected synchronized String getMatchedBackendURL(final URI requestURI) {
- final String path = requestURI.getRawPath();
- final String query = requestURI.getRawQuery();
-
- final ServiceRegistry serviceRegistryService = services
- .getService(ServiceType.SERVICE_REGISTRY_SERVICE);
-
- final ServiceDefinitionRegistry serviceDefinitionService = services
- .getService(ServiceType.SERVICE_DEFINITION_REGISTRY);
-
- /* Filter out the /cluster/topology to get the context we want */
- String[] pathInfo = path.split(REGEX_SPLIT_CONTEXT);
-
- final ServiceDefEntry entry = serviceDefinitionService
- .getMatchingService(pathInfo[1]);
-
- if (entry == null) {
- throw new RuntimeException(
- String.format(Locale.ROOT, "Cannot find service for the given path: %s", path));
- }
-
- /* Filter out /cluster/topology/service to get endpoint */
- String[] pathService = path.split(REGEX_SPLIT_SERVICE_PATH);
-
- /* URL used to connect to websocket backend */
- String backendURL = urlFromServiceDefinition(serviceRegistryService, entry, path);
- LOG.debugLog("Url obtained from services definition: " + backendURL);
-
- StringBuilder backend = new StringBuilder();
- try {
- if (StringUtils.containsAny(backendURL, WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
- LOG.debugLog("ws or wss protocol found in service url");
- URI serviceUri = new URI(backendURL);
- backend.append(serviceUri);
- String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
- backend.append(pathSuffix);
- } else if (StringUtils.containsAny(requestURI.toString(), WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
- LOG.debugLog("ws or wss protocol found in request url");
- URL serviceUrl = new URL(backendURL);
- final String protocol = (serviceUrl.getProtocol().equals("https")) ? "wss" : "ws";
- backend.append(protocol).append("://");
- backend.append(serviceUrl.getHost()).append(':');
- backend.append(serviceUrl.getPort()).append('/');
- backend.append(serviceUrl.getPath());
- String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
- backend.append(pathSuffix);
- } else {
- LOG.debugLog("ws or wss protocol not found in service url or request url");
- URL serviceUrl = new URL(backendURL);
-
- /* Use http host:port if ws url not configured */
- final String protocol = (serviceUrl.getProtocol().equals("ws")
- || serviceUrl.getProtocol().equals("wss")) ? serviceUrl.getProtocol()
- : "ws";
- backend.append(protocol).append("://");
- backend.append(serviceUrl.getHost()).append(':');
- backend.append(serviceUrl.getPort()).append('/');
- backend.append(serviceUrl.getPath());
- }
- /* in case we have query params */
- if(!StringUtils.isBlank(query)) {
- backend.append('?').append(query);
- }
- backendURL = backend.toString();
-
- } catch (MalformedURLException e){
- LOG.badUrlError(e);
- throw new RuntimeException(e.toString());
- } catch (Exception e1) {
- LOG.failedCreatingWebSocket(e1);
- throw new RuntimeException(e1.toString());
- }
-
- return backendURL;
- }
-
- private static String urlFromServiceDefinition(
- final ServiceRegistry serviceRegistry, final ServiceDefEntry entry,
- final String path) {
-
- final String[] contexts = path.split("/");
-
- /*
- * we have a match, if ws:// is present it is returned else http:// is
- * returned
- */
- return serviceRegistry.lookupServiceURL(contexts[2],
- entry.getName().toUpperCase(Locale.ROOT));
- }
-
- private String generateUrlSuffix(String backendPart, String[] pathService) {
- /* Avoid Zeppelin Regression - as this would require ambari changes and break current knox websocket use case*/
- if (!StringUtils.endsWith(backendPart, "/ws") && pathService.length > 0
- && pathService[1] != null) {
- String newPathSuffix = pathService[1];
- if ((backendPart.endsWith("/")) && (pathService[1].startsWith("/"))) {
- newPathSuffix = pathService[1].substring(1);
- }
- return newPathSuffix;
- }
- return "";
- }
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java
new file mode 100644
index 0000000000..f190a0bd61
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.websockets;
+
+import jakarta.websocket.ClientEndpointConfig;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.registry.ServiceDefEntry;
+import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry;
+import org.apache.knox.gateway.services.registry.ServiceRegistry;
+import org.apache.knox.gateway.services.security.KeystoreService;
+import org.apache.knox.gateway.services.security.KeystoreServiceException;
+import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
+import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
+import org.eclipse.jetty.websocket.server.WebSocketCreator;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class KnoxWebSocketCreator implements WebSocketCreator {
+ private static final WebsocketLogMessages LOG = MessagesFactory
+ .get(WebsocketLogMessages.class);
+
+ public static final String WEBSOCKET_PROTOCOL_STRING = "ws://";
+
+ public static final String SECURE_WEBSOCKET_PROTOCOL_STRING = "wss://";
+
+ static final String REGEX_SPLIT_CONTEXT = "^((?:[^/]*/){2}[^/]*)";
+
+ static final String REGEX_SPLIT_SERVICE_PATH = "^((?:[^/]*/){3}[^/]*)";
+
+ static final String REGEX_WEBSHELL_REQUEST_PATH =
+ "^(" + SECURE_WEBSOCKET_PROTOCOL_STRING+"|"+WEBSOCKET_PROTOCOL_STRING + ")[^/]+/[^/]+/webshell$";
+
+ private static final int POOL_SIZE = 10;
+ private final AtomicInteger concurrentWebshells;
+
+ /**
+ * Manage the threads that are spawned
+ * @since 0.13
+ */
+ private final ExecutorService pool;
+
+ final GatewayConfig config;
+ final GatewayServices services;
+
+ public KnoxWebSocketCreator(GatewayConfig config, GatewayServices services) {
+ this.config = config;
+ this.services = services;
+ this.pool = Executors.newFixedThreadPool(POOL_SIZE);
+ this.concurrentWebshells = new AtomicInteger(0);
+ }
+
+ @Override
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
+ try {
+ // 1. Get the raw HTTP URI from the Jetty 12 Request
+ HttpURI httpURI = req.getHttpURI();
+
+ // 2. Translate the scheme to match Jetty 9's behavior (http -> ws, https -> wss)
+ String wsScheme = "https".equalsIgnoreCase(httpURI.getScheme()) ? "wss" : "ws";
+
+ // 3. Reconstruct the java.net.URI for Knox's internal routing methods
+ final URI requestURI = HttpURI.build(httpURI).scheme(wsScheme).toURI();
+
+ // Now Knox's regex will work
+ if (isWebshellRequest(requestURI)) {
+ return handleWebshellRequest(req); // Note: Update handleWebshellRequest to accept ServerUpgradeRequest
+ }
+
+ final String backendURL = getMatchedBackendURL(requestURI);
+ LOG.debugLog("Generated backend URL for websocket connection: " + backendURL);
+
+ final ClientEndpointConfig clientConfig = getClientEndpointConfig(req, backendURL);
+ clientConfig.getUserProperties().put("org.apache.knox.gateway.websockets.truststore", getTruststore());
+
+ return new ProxyWebSocketAdapter(URI.create(backendURL), pool, clientConfig, config);
+
+ } catch (final Exception e) {
+ LOG.failedCreatingWebSocket(e);
+ // In Jetty 12, completing the callback with failure tells the server to reject the upgrade
+ callback.failed(e);
+ return null;
+ }
+ }
+
+ private boolean isWebshellRequest(URI requestURI){
+ return requestURI.toString().matches(REGEX_WEBSHELL_REQUEST_PATH);
+ }
+
+ private WebshellWebSocketAdapter handleWebshellRequest(ServerUpgradeRequest req){
+ if (config.isWebShellEnabled()){
+ if (concurrentWebshells.get() >= config.getMaximumConcurrentWebshells()){
+ throw new RuntimeException("Number of allowed concurrent Web Shell sessions exceeded");
+ }
+ JWTValidator jwtValidator = JWTValidatorFactory.create(req, services, config);
+ if (jwtValidator.validate()) {
+ return new WebshellWebSocketAdapter(pool, config, jwtValidator, concurrentWebshells);
+ }
+ throw new RuntimeException("No valid token found for Web Shell connection");
+ }
+ throw new RuntimeException("Web Shell not enabled");
+ }
+
+ private KeyStore getTruststore() throws KeystoreServiceException {
+ final KeystoreService ks = this.services
+ .getService(ServiceType.KEYSTORE_SERVICE);
+ KeyStore trustKeystore = null;
+ trustKeystore = ks.getTruststoreForHttpClient();
+ if (trustKeystore == null) {
+ trustKeystore = ks.getKeystoreForGateway();
+ }
+ return trustKeystore;
+ }
+
+ /**
+ * Returns a {@link ClientEndpointConfig} config that contains the headers
+ * to be passed to the backend.
+ * @since 0.14.0
+ */
+ private ClientEndpointConfig getClientEndpointConfig(final ServerUpgradeRequest req, final String backendURL) {
+
+ return ClientEndpointConfig.Builder.create()
+ .configurator(new ClientEndpointConfig.Configurator() {
+
+ @Override
+ public void beforeRequest(final Map> headers) {
+
+ // 1. Safely iterate over Jetty 12 HttpFields and copy them to the Jakarta map
+ for (HttpField field : req.getHeaders()) {
+ headers.computeIfAbsent(field.getName(), k -> new ArrayList<>())
+ .add(field.getValue());
+ }
+
+ // 2. Properly construct and override the Host header
+ try {
+ final URI backendURI = new URI(backendURL);
+
+ // Handle implicit ports (where getPort() returns -1) to prevent "Host: example.com:-1"
+ int port = backendURI.getPort();
+ String hostValue = backendURI.getHost() + (port != -1 ? ":" + port : "");
+
+ headers.put("Host", Collections.singletonList(hostValue));
+
+ } catch (final URISyntaxException e) {
+ LOG.onError(String.format(Locale.ROOT,
+ "Error getting backend url, this could cause 'Host does not match SNI' exception. Cause: %s",
+ e.toString()));
+ }
+ }
+ }).build();
+ }
+
+ /**
+ * This method looks at the context path and returns the backend websocket
+ * url. If websocket url is found it is used as is, or we default to
+ * ws://{host}:{port} which might or might not be right.
+ * @param requestURI url to match
+ * @return Websocket backend url
+ */
+ protected synchronized String getMatchedBackendURL(final URI requestURI) {
+ final String path = requestURI.getRawPath();
+ final String query = requestURI.getRawQuery();
+
+ final ServiceRegistry serviceRegistryService = services
+ .getService(ServiceType.SERVICE_REGISTRY_SERVICE);
+
+ final ServiceDefinitionRegistry serviceDefinitionService = services
+ .getService(ServiceType.SERVICE_DEFINITION_REGISTRY);
+
+ /* Filter out the /cluster/topology to get the context we want */
+ String[] pathInfo = path.split(REGEX_SPLIT_CONTEXT);
+
+ final ServiceDefEntry entry = serviceDefinitionService
+ .getMatchingService(pathInfo[1]);
+
+ if (entry == null) {
+ throw new RuntimeException(
+ String.format(Locale.ROOT, "Cannot find service for the given path: %s", path));
+ }
+
+ /* Filter out /cluster/topology/service to get endpoint */
+ String[] pathService = path.split(REGEX_SPLIT_SERVICE_PATH);
+
+ /* URL used to connect to websocket backend */
+ String backendURL = urlFromServiceDefinition(serviceRegistryService, entry, path);
+ LOG.debugLog("Url obtained from services definition: " + backendURL);
+
+ StringBuilder backend = new StringBuilder();
+ try {
+ if (StringUtils.containsAny(backendURL, WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
+ LOG.debugLog("ws or wss protocol found in service url");
+ URI serviceUri = new URI(backendURL);
+ backend.append(serviceUri);
+ String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
+ backend.append(pathSuffix);
+ } else if (StringUtils.containsAny(requestURI.toString(), WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
+ LOG.debugLog("ws or wss protocol found in request url");
+ URL serviceUrl = new URL(backendURL);
+ final String protocol = (serviceUrl.getProtocol().equals("https")) ? "wss" : "ws";
+ backend.append(protocol).append("://");
+ backend.append(serviceUrl.getHost()).append(':');
+ backend.append(serviceUrl.getPort()).append('/');
+ backend.append(serviceUrl.getPath());
+ String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
+ backend.append(pathSuffix);
+ } else {
+ LOG.debugLog("ws or wss protocol not found in service url or request url");
+ URL serviceUrl = new URL(backendURL);
+
+ /* Use http host:port if ws url not configured */
+ final String protocol = (serviceUrl.getProtocol().equals("ws")
+ || serviceUrl.getProtocol().equals("wss")) ? serviceUrl.getProtocol()
+ : "ws";
+ backend.append(protocol).append("://");
+ backend.append(serviceUrl.getHost()).append(':');
+ backend.append(serviceUrl.getPort()).append('/');
+ backend.append(serviceUrl.getPath());
+ }
+ /* in case we have query params */
+ if(!StringUtils.isBlank(query)) {
+ backend.append('?').append(query);
+ }
+ backendURL = backend.toString();
+
+ } catch (MalformedURLException e){
+ LOG.badUrlError(e);
+ throw new RuntimeException(e.toString());
+ } catch (Exception e1) {
+ LOG.failedCreatingWebSocket(e1);
+ throw new RuntimeException(e1.toString());
+ }
+
+ return backendURL;
+ }
+
+ private static String urlFromServiceDefinition(
+ final ServiceRegistry serviceRegistry, final ServiceDefEntry entry,
+ final String path) {
+
+ final String[] contexts = path.split("/");
+
+ /*
+ * we have a match, if ws:// is present it is returned else http:// is
+ * returned
+ */
+ return serviceRegistry.lookupServiceURL(contexts[2],
+ entry.getName().toUpperCase(Locale.ROOT));
+ }
+
+ private String generateUrlSuffix(String backendPart, String[] pathService) {
+ /* Avoid Zeppelin Regression - as this would require ambari changes and break current knox websocket use case*/
+ if (!StringUtils.endsWith(backendPart, "/ws") && pathService.length > 0
+ && pathService[1] != null) {
+ String newPathSuffix = pathService[1];
+ if ((backendPart.endsWith("/")) && (pathService[1].startsWith("/"))) {
+ newPathSuffix = pathService[1].substring(1);
+ }
+ return newPathSuffix;
+ }
+ return "";
+ }
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
index df4aed6b33..b259095214 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandlerTest.java
@@ -31,6 +31,7 @@
import org.easymock.EasyMock;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
import org.junit.Assert;
@@ -79,6 +80,8 @@ public void testValidWebShellRequest() throws Exception{
// mock ServerUpgradeRequest and ServerUpgradeResponse
ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
ServerUpgradeResponse resp = createServerUpgradeResponse();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
@@ -90,8 +93,9 @@ public void testValidWebShellRequest() throws Exception{
EasyMock.replay(gatewayServices,gatewayConfig,jwtValidator);
PowerMock.replayAll();
- GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(gatewayConfig,gatewayServices);
- Assert.assertTrue(gatewayWebsocketHandler.createWebSocket(req,resp) instanceof WebshellWebSocketAdapter);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig,gatewayServices);
+
+ Assert.assertTrue(knoxWebSocketCreator.createWebSocket(req,resp,callback) instanceof WebshellWebSocketAdapter);
}
@Test
@@ -104,7 +108,8 @@ public void testValidWebShellRequestThroughLB() throws Exception{
// mock ServerUpgradeRequest and ServerUpgradeResponse
ServerUpgradeRequest req = createServerUpgradeRequest("wss://www.local.com/gateway/webshell");
ServerUpgradeResponse resp = createServerUpgradeResponse();
-
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
PowerMock.mockStatic(JWTValidatorFactory.class);
@@ -115,8 +120,8 @@ public void testValidWebShellRequestThroughLB() throws Exception{
EasyMock.replay(gatewayServices,gatewayConfig,jwtValidator);
PowerMock.replayAll();
- GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(gatewayConfig,gatewayServices);
- Assert.assertTrue(gatewayWebsocketHandler.createWebSocket(req,resp) instanceof WebshellWebSocketAdapter);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig,gatewayServices);
+ Assert.assertTrue(knoxWebSocketCreator.createWebSocket(req,resp, callback) instanceof WebshellWebSocketAdapter);
}
@Rule
@@ -134,6 +139,8 @@ public void testWebShellRequestWithInvalidJWT() throws Exception{
// mock ServerUpgradeRequest and ServerUpgradeResponse
ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
ServerUpgradeResponse resp = createServerUpgradeResponse();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(false).anyTimes();
@@ -143,8 +150,8 @@ public void testWebShellRequestWithInvalidJWT() throws Exception{
EasyMock.replay(gatewayServices,gatewayConfig,jwtValidator);
PowerMock.replayAll();
- GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(gatewayConfig,gatewayServices);
- gatewayWebsocketHandler.createWebSocket(req,resp);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig,gatewayServices);
+ knoxWebSocketCreator.createWebSocket(req,resp,callback);
}
@@ -159,9 +166,11 @@ public void testDisabledWebShell() throws Exception{
// mock ServerUpgradeRequest and ServerUpgradeResponse
ServerUpgradeRequest req = createServerUpgradeRequest("wss://localhost:8443/gateway/webshell");
ServerUpgradeResponse resp = createServerUpgradeResponse();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
EasyMock.replay(gatewayServices,gatewayConfig);
- GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(gatewayConfig,gatewayServices);
- gatewayWebsocketHandler.createWebSocket(req,resp);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig,gatewayServices);
+ knoxWebSocketCreator.createWebSocket(req,resp,callback);
}
private ServerUpgradeRequest createServerUpgradeRequest(String url) throws Exception {
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java
index adab58546a..b909482697 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java
@@ -70,8 +70,8 @@ public static void tearDownAfterClass() {
@Test
public void testWebsocketBackendUrl() throws Exception {
URI requestURI = new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels");
- GatewayWebsocketHandler gwh = new GatewayWebsocketHandler(gatewayConfig, services);
- String backendUrl = gwh.getMatchedBackendURL(requestURI);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig, services);
+ String backendUrl = knoxWebSocketCreator.getMatchedBackendURL(requestURI);
String expectedBackendUrl = backendServerUri.toString() + "channels";
assertThat(backendUrl, is(expectedBackendUrl));
}
@@ -83,8 +83,8 @@ public void testWebsocketBackendUrl() throws Exception {
public void testWebsocketBackendUrlWithQueryParams() throws Exception {
final String pathContext = "channels?EIO=3&transport=websocket";
URI requestURI = new URI(String.format(Locale.ROOT, "%sgateway/websocket/123foo456bar/%s",serverUri.toString(), pathContext));
- GatewayWebsocketHandler gwh = new GatewayWebsocketHandler(gatewayConfig, services);
- String backendUrl = gwh.getMatchedBackendURL(requestURI);
+ KnoxWebSocketCreator knoxWebSocketCreator = new KnoxWebSocketCreator(gatewayConfig, services);
+ String backendUrl = knoxWebSocketCreator.getMatchedBackendURL(requestURI);
String expectedBackendUrl = backendServerUri.toString() + pathContext;
assertThat(backendUrl, is(expectedBackendUrl));
}
From 9deacebbe83ea72a2347e1b66eb77d38c86a6967 Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Fri, 24 Apr 2026 17:23:51 +0200
Subject: [PATCH 23/43] KNOX-3238: correct MockConsoleFactory
---
.../org/apache/knox/gateway/mock/MockConsoleFactory.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/mock/MockConsoleFactory.java b/gateway-server/src/test/java/org/apache/knox/gateway/mock/MockConsoleFactory.java
index d40ef269a5..b82e9c7410 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/mock/MockConsoleFactory.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/mock/MockConsoleFactory.java
@@ -19,8 +19,8 @@
import org.apache.knox.test.mock.MockServlet;
import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
public class MockConsoleFactory {
@@ -31,7 +31,7 @@ public static Handler create() {
ServletContextHandler consoleContext = new ServletContextHandler( ServletContextHandler.SESSIONS );
consoleContext.setContextPath( "/console" );
- consoleContext.setResourceBase( "target/classes" );
+ consoleContext.setBaseResourceAsString( "target/classes" );
consoleContext.addServlet( consoleHolder, "/*" );
return consoleContext;
From 6ad6a0ecd46ee5955d0d741ebf2a4d0ef04a958c Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Sat, 25 Apr 2026 19:09:32 +0200
Subject: [PATCH 24/43] KNOX-3238: correcting dependencies and build errors
(without HadoopAuthFilter)
---
gateway-provider-identity-assertion-common/pom.xml | 1 +
.../pom.xml | 11 +++++------
gateway-server/pom.xml | 4 ++++
.../java/org/apache/knox/gateway/GatewayServer.java | 1 -
.../knox/gateway/filter/RequestUpdateHandler.java | 4 ----
.../apache/knox/gateway/trace/TraceResponse.java | 5 -----
gateway-service-admin/pom.xml | 8 ++++----
gateway-service-knoxtoken/pom.xml | 4 ++--
gateway-service-metadata/pom.xml | 9 ++++-----
gateway-service-session/pom.xml | 9 ++++-----
pom.xml | 13 ++++++++++---
11 files changed, 34 insertions(+), 35 deletions(-)
diff --git a/gateway-provider-identity-assertion-common/pom.xml b/gateway-provider-identity-assertion-common/pom.xml
index 9c4b680d7d..238b4d5138 100644
--- a/gateway-provider-identity-assertion-common/pom.xml
+++ b/gateway-provider-identity-assertion-common/pom.xml
@@ -91,6 +91,7 @@
org.eclipse.jetty
jetty-http
+ test
org.eclipse.jetty
diff --git a/gateway-provider-rewrite-func-service-registry/pom.xml b/gateway-provider-rewrite-func-service-registry/pom.xml
index 6947315e86..fe2ddb89fa 100644
--- a/gateway-provider-rewrite-func-service-registry/pom.xml
+++ b/gateway-provider-rewrite-func-service-registry/pom.xml
@@ -59,12 +59,6 @@
gateway-util-urltemplate
-
- org.eclipse.jetty
- jetty-http
-
-
-
@@ -73,6 +67,11 @@
httpclient
test
+
+ org.eclipse.jetty
+ jetty-http
+ test
+
org.eclipse.jetty
jetty-util
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index b1c1b44e3c..ebe6b369ae 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -70,6 +70,10 @@
jakarta.json
jakarta.json-api
+
+ jakarta.websocket
+ jakarta.websocket-client-api
+
org.eclipse.parsson
parsson
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index d4567e6155..681a462a45 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -86,7 +86,6 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
-import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
index c8f5e24aab..9d95ccccae 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/filter/RequestUpdateHandler.java
@@ -32,10 +32,6 @@
*/
public final class RequestUpdateHandler {
- private RequestUpdateHandler() {
- // utility holder for the ForwardedRequest wrapper
- }
-
/**
* A request wrapper class that wraps a request and prepends a context path
* to the request URI.
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceResponse.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceResponse.java
index 2cf63ae6e7..bb9ed891f5 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceResponse.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceResponse.java
@@ -19,18 +19,13 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-
-import jakarta.servlet.ServletOutputStream;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
-
-import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.Collection;
import java.util.Locale;
import java.util.Set;
diff --git a/gateway-service-admin/pom.xml b/gateway-service-admin/pom.xml
index ecaed0cfb4..4aee03b800 100644
--- a/gateway-service-admin/pom.xml
+++ b/gateway-service-admin/pom.xml
@@ -57,6 +57,10 @@
org.glassfish
jakarta.json
+
+ jakarta.inject
+ jakarta.inject-api
+
jakarta.json
jakarta.json-api
@@ -65,10 +69,6 @@
org.eclipse.parsson
parsson
-
- org.glassfish.hk2.external
- javax.inject
-
commons-io
commons-io
diff --git a/gateway-service-knoxtoken/pom.xml b/gateway-service-knoxtoken/pom.xml
index ec31b311f4..8524e1e4ce 100644
--- a/gateway-service-knoxtoken/pom.xml
+++ b/gateway-service-knoxtoken/pom.xml
@@ -60,8 +60,8 @@
jakarta.annotation-api
- org.glassfish.hk2.external
- javax.inject
+ jakarta.inject
+ jakarta.inject-api
jakarta.ws.rs
diff --git a/gateway-service-metadata/pom.xml b/gateway-service-metadata/pom.xml
index ded250c03e..3a1b5f4f7d 100644
--- a/gateway-service-metadata/pom.xml
+++ b/gateway-service-metadata/pom.xml
@@ -49,7 +49,10 @@
org.apache.knox
gateway-util-common
-
+
+ jakarta.inject
+ jakarta.inject-api
+
jakarta.servlet
jakarta.servlet-api
@@ -70,10 +73,6 @@
org.eclipse.persistence
org.eclipse.persistence.moxy
-
- org.glassfish.hk2.external
- javax.inject
-
io.swagger
swagger-annotations
diff --git a/gateway-service-session/pom.xml b/gateway-service-session/pom.xml
index 5e2e69b25f..300429ad73 100644
--- a/gateway-service-session/pom.xml
+++ b/gateway-service-session/pom.xml
@@ -45,7 +45,10 @@
org.apache.knox
gateway-util-common
-
+
+ jakarta.inject
+ jakarta.inject-api
+
jakarta.servlet
jakarta.servlet-api
@@ -66,9 +69,5 @@
org.eclipse.persistence
org.eclipse.persistence.moxy
-
- org.glassfish.hk2.external
- javax.inject
-
diff --git a/pom.xml b/pom.xml
index eaab6157c4..5aa6afa9ef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -228,6 +228,8 @@
2.0.1
2.1.4
2.1.1
+ 5.0.1
+ 2.0.1
2.0.1
2.1.3
6.0.0
@@ -1538,9 +1540,14 @@
- org.glassfish.hk2.external
- javax.inject
- ${javax.inject.version}
+ jakarta.el
+ jakarta.el-api
+ ${jakarta.el-api.version}
+
+
+ jakarta.inject
+ jakarta.inject-api
+ ${jakarta.inject-api.version}
jakarta.servlet
From 483e98d33b3f1c5f8e11fd9c8fc1785f3b55a45b Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Sat, 25 Apr 2026 19:48:38 +0200
Subject: [PATCH 25/43] KNOX-3238: restore hadoop-common javax exclusions
---
gateway-service-knoxtoken/pom.xml | 4 ++--
gateway-spi-common/pom.xml | 4 ++--
gateway-spi/pom.xml | 4 ++--
pom.xml | 8 ++++----
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/gateway-service-knoxtoken/pom.xml b/gateway-service-knoxtoken/pom.xml
index 8524e1e4ce..8fca63562f 100644
--- a/gateway-service-knoxtoken/pom.xml
+++ b/gateway-service-knoxtoken/pom.xml
@@ -91,8 +91,8 @@
hadoop-common
- jakarta.activation
- jakarta.activation-api
+ javax.activation
+ javax.activation-api
dnsjava
diff --git a/gateway-spi-common/pom.xml b/gateway-spi-common/pom.xml
index 3f381f709e..336a4b23d1 100644
--- a/gateway-spi-common/pom.xml
+++ b/gateway-spi-common/pom.xml
@@ -42,8 +42,8 @@
hadoop-common
- jakarta.activation
- jakarta.activation-api
+ javax.activation
+ javax.activation-api
dnsjava
diff --git a/gateway-spi/pom.xml b/gateway-spi/pom.xml
index 450856a4ea..7704526ce1 100644
--- a/gateway-spi/pom.xml
+++ b/gateway-spi/pom.xml
@@ -189,8 +189,8 @@
hadoop-common
- jakarta.activation
- jakarta.activation-api
+ javax.activation
+ javax.activation-api
dnsjava
diff --git a/pom.xml b/pom.xml
index 5aa6afa9ef..af7cf9d418 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1931,12 +1931,12 @@
- jakarta.servlet
- jakarta.servlet-api
+ javax.servlet
+ javax.servlet-api
- jakarta.servlet.jsp
- jakarta.servlet.jsp-api
+ javax.servlet.jsp
+ jsp-api
From 643ce187ee409852679ccec8c3545b951725179f Mon Sep 17 00:00:00 2001
From: bonampak <14160522+bonampak@users.noreply.github.com>
Date: Sat, 25 Apr 2026 22:36:26 +0200
Subject: [PATCH 26/43] KNOX-3238: upgraded dropwizard metrics to 4.2.38 (Jetty
12 needs 4.2.x) and changed to metrics-jakarta-servlets.
---
gateway-server/pom.xml | 4 ++--
.../GatewayMetricsServletContextListener.java | 2 +-
gateway-service-health/pom.xml | 2 +-
.../knox/gateway/service/health/MetricsResource.java | 2 +-
pom.xml | 11 ++++++++---
5 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index ebe6b369ae..6bd66bd982 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -301,7 +301,7 @@
io.dropwizard.metrics
- metrics-jetty9
+ metrics-jetty12
io.dropwizard.metrics
@@ -313,7 +313,7 @@
io.dropwizard.metrics
- metrics-servlets
+ metrics-jakarta-servlets