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 fc0e6312ba..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
@@ -16,112 +16,52 @@
*/
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 javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
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");
- }
-
- 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);
- }
- }
+public final class RequestUpdateHandler {
/**
- * 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 +80,4 @@ public String getContextPath() {
return this.contextPath;
}
}
-}
+}
\ No newline at end of file
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/filter/SessionFilter.java b/gateway-server/src/main/java/org/apache/knox/gateway/filter/SessionFilter.java
index e1b8288761..5f476ebe2c 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/filter/SessionFilter.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/filter/SessionFilter.java
@@ -17,10 +17,10 @@
*/
package org.apache.knox.gateway.filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayMetricsServletContextListener.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayMetricsServletContextListener.java
index 981b52b566..24d15a8998 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayMetricsServletContextListener.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayMetricsServletContextListener.java
@@ -18,7 +18,7 @@
package org.apache.knox.gateway.services;
import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.servlets.MetricsServlet;
+import io.dropwizard.metrics.servlets.MetricsServlet;
import org.apache.knox.gateway.services.metrics.impl.DefaultMetricsService;
public class GatewayMetricsServletContextListener extends MetricsServlet.ContextListener {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayServicesContextListener.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayServicesContextListener.java
index 65c4ab6d79..02bd8ebff3 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayServicesContextListener.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/GatewayServicesContextListener.java
@@ -17,8 +17,8 @@
*/
package org.apache.knox.gateway.services;
-import javax.servlet.ServletContextEvent;
-import javax.servlet.ServletContextListener;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
import org.apache.knox.gateway.GatewayServer;
import org.apache.knox.gateway.services.topology.TopologyService;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/metrics/impl/instr/InstrumentedGatewayFilter.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/metrics/impl/instr/InstrumentedGatewayFilter.java
index 70f6c4c9ad..6dc7eb55d1 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/metrics/impl/instr/InstrumentedGatewayFilter.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/metrics/impl/instr/InstrumentedGatewayFilter.java
@@ -21,13 +21,13 @@
import com.codahale.metrics.Timer;
import org.apache.knox.gateway.GatewayFilter;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Locale;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/AccessHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/AccessHandler.java
index c21e5eb8a4..47aa5c0563 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/AccessHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/AccessHandler.java
@@ -24,6 +24,8 @@
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import java.util.concurrent.TimeUnit;
+
public class AccessHandler extends AbstractLifeCycle implements RequestLog {
private static final Logger log = LogManager.getLogger( "org.apache.knox.gateway.access" );
@@ -32,20 +34,21 @@ public void log( Request request, Response response ) {
if( log.isTraceEnabled() ) {
StringBuilder sb = new StringBuilder();
TraceUtil.appendCorrelationContext(sb);
+ long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - request.getBeginNanoTime());
sb.append('|')
- .append(request.getRemoteAddr())
- .append('|')
- .append(request.getMethod())
- .append('|')
- .append(request.getHttpURI())
- .append('|')
- .append(request.getContentLength())
- .append('|')
- .append(response.getStatus())
- .append('|')
- .append(response.getContentCount())
- .append('|')
- .append(System.currentTimeMillis() - request.getTimeStamp());
+ .append(Request.getRemoteAddr(request)) // Static helper or request.getConnectionMetaData().getRemoteSocketAddress()
+ .append('|')
+ .append(request.getMethod())
+ .append('|')
+ .append(request.getHttpURI().toString())
+ .append('|')
+ .append(request.getLength()) // .getContentLength() is now .getLength()
+ .append('|')
+ .append(response.getStatus())
+ .append('|')
+ .append(Response.getContentBytesWritten(response)) // Use static helper for bytes written
+ .append('|')
+ .append(durationMillis);
log.trace(sb);
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/KnoxErrorHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/KnoxErrorHandler.java
index 10fefb531b..3ec532db5b 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/KnoxErrorHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/KnoxErrorHandler.java
@@ -18,12 +18,10 @@
package org.apache.knox.gateway.trace;
import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.Callback;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
import java.util.Set;
public class KnoxErrorHandler extends ErrorHandler {
@@ -35,10 +33,9 @@ public void setTracedBodyFilter( String s ) {
}
@Override
- public void handle( String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response )
- throws IOException, ServletException {
- HttpServletResponse traceResponse = new TraceResponse( response, bodyFilter );
- super.handle( target, baseRequest, request, traceResponse );
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ Response newResponse = new TraceResponse(request, response, bodyFilter);
+ return super.handle(request, newResponse, callback);
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceHandler.java
index 67f5f227f9..5c47ac966c 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceHandler.java
@@ -17,16 +17,14 @@
*/
package org.apache.knox.gateway.trace;
+import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
import java.util.Set;
-public class TraceHandler extends HandlerWrapper {
+public class TraceHandler extends Handler.Wrapper {
static final String HTTP_LOGGER = "org.apache.knox.gateway.http";
static final String HTTP_REQUEST_LOGGER = HTTP_LOGGER + ".request";
@@ -44,11 +42,10 @@ public void setTracedBodyFilter( String s ) {
}
@Override
- public void handle( String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response )
- throws IOException, ServletException {
- HttpServletRequest newRequest = new TraceRequest( request );
- HttpServletResponse newResponse = new TraceResponse( response, bodyFilter );
- super.handle( target, baseRequest, newRequest, newResponse );
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ Request newRequest = new TraceRequest(request);
+ Response newResponse = new TraceResponse(request, response, bodyFilter);
+ return super.handle(newRequest, newResponse, callback);
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceInput.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceInput.java
index 846c6c83c5..ccd7fb37e8 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceInput.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceInput.java
@@ -17,54 +17,42 @@
*/
package org.apache.knox.gateway.trace;
-import org.apache.knox.gateway.servlet.SynchronousServletInputStreamAdapter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import javax.servlet.ServletInputStream;
-import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.Locale;
-class TraceInput extends SynchronousServletInputStreamAdapter {
+class TraceInput {
private static final Logger log = LogManager.getLogger( TraceHandler.HTTP_REQUEST_LOGGER );
private static final Logger bodyLog = LogManager.getLogger( TraceHandler.HTTP_REQUEST_BODY_LOGGER );
- private ServletInputStream delegate;
-
private static final int BUFFER_LIMIT = 1024;
- private StringBuilder buffer = new StringBuilder( BUFFER_LIMIT );
+ private final StringBuilder buffer = new StringBuilder( BUFFER_LIMIT );
- TraceInput( ServletInputStream delegate ) {
- this.delegate = delegate;
- }
- @Override
- public int read() throws IOException {
- int b = delegate.read();
- if( b >= 0 ) {
- buffer.append( (char)b );
- if( buffer.length() == BUFFER_LIMIT || delegate.available() == 0 ) {
- traceBody();
+ public synchronized void extractContent(ByteBuffer view, boolean last) {
+ if (view != null && view.hasRemaining()) {
+ while (view.hasRemaining() && buffer.length() < BUFFER_LIMIT) {
+ String s = StandardCharsets.UTF_8.decode(view).toString();
+ buffer.append(s);
}
}
- return b;
- }
-
- @Override
- public void close() throws IOException {
- traceBody();
- delegate.close();
+ if (buffer.length() >= BUFFER_LIMIT || last) {
+ traceBody();
+ }
}
private synchronized void traceBody() {
- if( buffer.length() > 0 ) {
+ if (!buffer.isEmpty()) {
String body = buffer.toString();
- buffer.setLength( 0 );
+ buffer.setLength(0);
StringBuilder sb = new StringBuilder();
- TraceUtil.appendCorrelationContext( sb );
- sb.append( String.format(Locale.ROOT, "|RequestBody[%d]%n\t%s", body.length(), body ) );
- if( bodyLog.isTraceEnabled() ) {
- log.trace( sb.toString() );
+ TraceUtil.appendCorrelationContext(sb);
+ sb.append(String.format(Locale.ROOT, "|RequestBody[%d]%n\t%s", body.length(), body));
+ if (bodyLog.isTraceEnabled()) {
+ log.trace(sb.toString());
}
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceOutput.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceOutput.java
index 4bb123a5f5..5b3720f5a9 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceOutput.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceOutput.java
@@ -17,59 +17,41 @@
*/
package org.apache.knox.gateway.trace;
-import org.apache.knox.gateway.servlet.SynchronousServletOutputStreamAdapter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import javax.servlet.ServletOutputStream;
-import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.Locale;
-class TraceOutput extends SynchronousServletOutputStreamAdapter {
+class TraceOutput {
private static final Logger log = LogManager.getLogger( TraceHandler.HTTP_RESPONSE_LOGGER );
private static final Logger bodyLog = LogManager.getLogger( TraceHandler.HTTP_RESPONSE_BODY_LOGGER );
- private ServletOutputStream delegate;
-
private static final int BUFFER_LIMIT = 1024;
- private StringBuilder buffer = new StringBuilder( BUFFER_LIMIT );
-
- TraceOutput( ServletOutputStream delegate ) {
- this.delegate = delegate;
- }
+ private final StringBuilder buffer = new StringBuilder( BUFFER_LIMIT );
- @Override
- public synchronized void write( int b ) throws IOException {
- if( b >= 0 ) {
- buffer.append( (char)b );
- if( buffer.length() == BUFFER_LIMIT ) {
- traceBody();
+ public synchronized void extractContent(ByteBuffer view, boolean last) {
+ if (view != null && view.hasRemaining()) {
+ while (view.hasRemaining() && buffer.length() < BUFFER_LIMIT) {
+ String s = StandardCharsets.UTF_8.decode(view).toString();
+ buffer.append(s);
}
}
- delegate.write( b );
- }
-
- @Override
- public void flush() throws IOException {
- traceBody();
- delegate.flush();
- }
-
- @Override
- public void close() throws IOException {
- traceBody();
- delegate.close();
+ if (buffer.length() >= BUFFER_LIMIT || last) {
+ traceBody();
+ }
}
- private synchronized void traceBody() {
- if( buffer.length() > 0 ) {
+ private void traceBody() {
+ if (!buffer.isEmpty()) {
String body = buffer.toString();
- buffer.setLength( 0 );
+ buffer.setLength(0);
StringBuilder sb = new StringBuilder();
- TraceUtil.appendCorrelationContext( sb );
- sb.append( String.format(Locale.ROOT, "|ResponseBody[%d]%n\t%s", body.length(), body ) );
+ TraceUtil.appendCorrelationContext(sb);
+ sb.append(String.format(Locale.ROOT, "|ResponseBody[%d]%n\t%s", body.length(), body));
if( bodyLog.isTraceEnabled() ) {
- log.trace( sb.toString() );
+ log.trace(sb.toString());
}
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceRequest.java b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceRequest.java
index d8b84d2d04..8dbfe2743a 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceRequest.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/trace/TraceRequest.java
@@ -20,46 +20,56 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import javax.servlet.ServletInputStream;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import java.io.IOException;
-import java.util.Enumeration;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Request;
+
+import java.nio.ByteBuffer;
import java.util.Locale;
-class TraceRequest extends HttpServletRequestWrapper {
+class TraceRequest extends Request.Wrapper {
private static final Logger log = LogManager.getLogger( TraceHandler.HTTP_REQUEST_LOGGER );
private static final Logger headLog = LogManager.getLogger( TraceHandler.HTTP_REQUEST_HEADER_LOGGER );
- private ServletInputStream input;
+ private TraceInput delegate;
- TraceRequest( HttpServletRequest request ) {
- super( request );
- if( log.isTraceEnabled() ) {
+ TraceRequest(Request request) {
+ super(request);
+ if (log.isTraceEnabled()) {
+ delegate = new TraceInput();
traceRequestDetails();
}
}
@Override
- public synchronized ServletInputStream getInputStream() throws IOException {
- if( log.isTraceEnabled() ) {
- if( input == null ) {
- input = new TraceInput( super.getInputStream() );
+ public void demand(Runnable demandCallback) {
+ super.demand(demandCallback);
+ }
+
+ @Override
+ public Content.Chunk read() {
+ Content.Chunk chunk = super.read();
+ if (chunk != null && log.isTraceEnabled()) {
+ if (Content.Chunk.isFailure(chunk)) {
+ // Log that the request failed if you want
+ return chunk;
}
- return input;
- } else {
- return super.getInputStream();
+ ByteBuffer data = chunk.getByteBuffer();
+ // Use slice() so the tracer doesn't interfere with the data
+ delegate.extractContent(data != null ? data.slice() : null, chunk.isLast());
}
+ return chunk;
}
private void traceRequestDetails() {
StringBuilder sb = new StringBuilder();
TraceUtil.appendCorrelationContext( sb );
sb.append("|Request=")
- .append(getMethod())
- .append(' ')
- .append(getRequestURI());
- String qs = getQueryString();
+ .append(getMethod())
+ .append(' ')
+ .append(getHttpURI().getPath());
+ String qs = getHttpURI().getQuery();
if( qs != null ) {
sb.append('?').append(qs);
}
@@ -67,15 +77,12 @@ private void traceRequestDetails() {
log.trace(sb.toString());
}
- private void appendHeaders( StringBuilder sb ) {
- if( headLog.isTraceEnabled() ) {
- Enumeration names = getHeaderNames();
- while( names.hasMoreElements() ) {
- String name = names.nextElement();
- Enumeration values = getHeaders( name );
- while( values.hasMoreElements() ) {
- String value = values.nextElement();
- sb.append( String.format(Locale.ROOT, "%n\tHeader[%s]=%s", name, value ) );
+ private void appendHeaders(StringBuilder sb) {
+ if (headLog.isTraceEnabled()) {
+ HttpFields requestHeaders = getHeaders();
+ if (requestHeaders != null) {
+ for (HttpField header: requestHeaders) {
+ sb.append( String.format(Locale.ROOT, "%n\tHeader[%s]=%s", header.getName(), header.getValue()));
}
}
}
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 3912bce369..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,59 +19,63 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+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 javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
-import java.io.IOException;
-import java.util.Collection;
+import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.Set;
-class TraceResponse extends HttpServletResponseWrapper {
+class TraceResponse extends Response.Wrapper {
private static final Logger log = LogManager.getLogger( TraceHandler.HTTP_RESPONSE_LOGGER );
private static final Logger headLog = LogManager.getLogger( TraceHandler.HTTP_RESPONSE_HEADER_LOGGER );
- private ServletOutputStream output;
- private Set filter;
+ private final TraceOutput output;
+ private final Set filter;
- TraceResponse( HttpServletResponse response, Set filter ) {
- super( response );
+ TraceResponse(Request request, Response wrapped, Set filter ) {
+ super(request, wrapped);
this.filter = filter;
+ this.output = new TraceOutput();
+ if (log.isTraceEnabled()) {
+ traceResponseDetails();
+ }
}
@Override
- public synchronized ServletOutputStream getOutputStream() throws IOException {
+ public void write(boolean last, ByteBuffer content, Callback callback) {
if( log.isTraceEnabled() ) {
- traceResponseDetails();
- if( output == null && ( filter == null || filter.isEmpty() || filter.contains( getStatus() ) ) ) {
- output = new TraceOutput( super.getOutputStream() );
+ if (filter == null || filter.isEmpty() || filter.contains(getStatus())) {
+ ByteBuffer view = (content != null) ? content.slice() : null;
+ output.extractContent(view, last);
}
- return output;
- } else {
- return super.getOutputStream();
}
+ super.write(last, content, callback);
}
private void traceResponseDetails() {
StringBuilder sb = new StringBuilder();
TraceUtil.appendCorrelationContext( sb );
sb.append( "|Response=" )
- .append( getStatus() );
+ .append( getStatus() );
appendHeaders( sb );
log.trace( sb.toString() );
}
private void appendHeaders( StringBuilder sb ) {
if( headLog.isTraceEnabled() ) {
- Collection names = getHeaderNames();
- for( String name : names ) {
- for( String value : getHeaders( name ) ) {
- sb.append( String.format(Locale.ROOT, "%n\tHeader[%s]=%s", name, value ) );
+ HttpFields responseHeaders = getHeaders();
+ if (responseHeaders != null) {
+ for (HttpField header: responseHeaders) {
+ sb.append( String.format(Locale.ROOT, "%n\tHeader[%s]=%s", header.getName(), header.getValue()));
}
}
}
}
+
}
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 f275ee9eeb..14e983a520 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,35 +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;
-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 javax.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.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;
+
+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.ServerWebSocketContainer;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
+
+import java.time.Duration;
/**
* Websocket handler that will handle websocket connection request. This class
@@ -54,256 +37,83 @@
*
* @since 0.10
*/
-public class GatewayWebsocketHandler extends WebSocketHandler
- 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;
+public class GatewayWebsocketHandler extends Handler.Wrapper {
final GatewayConfig config;
final GatewayServices services;
-
+ private WebSocketUpgradeHandler wsHandler;
public GatewayWebsocketHandler(final GatewayConfig config,
final GatewayServices services) {
super();
this.config = config;
this.services = services;
- pool = Executors.newFixedThreadPool(POOL_SIZE);
- this.concurrentWebshells = new AtomicInteger(0);
}
@Override
- public void configure(final WebSocketServletFactory factory) {
- factory.setCreator(this);
- factory.getPolicy()
- .setMaxTextMessageSize(config.getWebsocketMaxTextMessageSize());
- factory.getPolicy()
- .setMaxBinaryMessageSize(config.getWebsocketMaxBinaryMessageSize());
+ protected void doStart() throws Exception {
+ Server server = getServer();
+ if (server == null) {
+ throw new IllegalStateException("GatewayWebsocketHandler must be attached to a Server before starting");
+ }
- factory.getPolicy().setMaxBinaryMessageBufferSize(
- config.getWebsocketMaxBinaryMessageBufferSize());
- factory.getPolicy().setMaxTextMessageBufferSize(
- config.getWebsocketMaxTextMessageBufferSize());
+ // 1. Get or create the global ServerWebSocketContainer (no ContextHandler needed)
+ ServerWebSocketContainer container = ServerWebSocketContainer.ensure(server, null);
+ configureServerWebSocketContainer(container);
- factory.getPolicy()
- .setInputBufferSize(config.getWebsocketInputBufferSize());
+ // 3. Create the UpgradeHandler.
+ this.wsHandler = new WebSocketUpgradeHandler(container);
- factory.getPolicy()
- .setAsyncWriteTimeout(config.getWebsocketAsyncWriteTimeout());
- factory.getPolicy().setIdleTimeout(config.getWebsocketIdleTimeout());
+ // 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());
- }
-
- private Boolean isWebshellRequest(URI requestURI){
- return requestURI.toString().matches(REGEX_WEBSHELL_REQUEST_PATH);
- }
+ // Start the internal handler
+ this.wsHandler.start();
- private WebshellWebSocketAdapter handleWebshellRequest(ServletUpgradeRequest 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");
+ super.doStart();
}
-
@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);
+ protected void doStop() throws Exception {
+ if (this.wsHandler != null) {
+ this.wsHandler.stop();
}
+ super.doStop();
}
- 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 ServletUpgradeRequest req) {
-
- 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();
+ @Override
+ 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);
}
- /**
- * 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);
+ public void configureServerWebSocketContainer(ServerWebSocketContainer container) {
+ container.setMaxTextMessageSize(config.getWebsocketMaxTextMessageSize());
+ container.setMaxBinaryMessageSize(config.getWebsocketMaxBinaryMessageSize());
+ container.setInputBufferSize(config.getWebsocketInputBufferSize());
- final ServiceDefinitionRegistry serviceDefinitionService = services
- .getService(ServiceType.SERVICE_DEFINITION_REGISTRY);
+ container.setIdleTimeout(Duration.ofMillis(config.getWebsocketIdleTimeout()));
- /* Filter out the /cluster/topology to get the context we want */
- String[] pathInfo = path.split(REGEX_SPLIT_CONTEXT);
+ // 2. Map ALL incoming requests to our custom Knox routing creator
+ // "regex|^/.*" acts as a catch-all interceptor.
+ container.addMapping("regex|^/.*", new KnoxWebSocketCreator(config, services));
- 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));
- }
+ //removed in Jetty 12 container.setMaxBinaryMessageBufferSize(config.getWebsocketMaxBinaryMessageBufferSize());
+ //removed in Jetty 12 container.setMaxTextMessageBufferSize(config.getWebsocketMaxTextMessageBufferSize());
- /* Filter out /cluster/topology/service to get endpoint */
- String[] pathService = path.split(REGEX_SPLIT_SERVICE_PATH);
+ //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(...)).
- /* URL used to connect to websocket backend */
- String backendURL = urlFromServiceDefinition(serviceRegistryService, entry, path);
- LOG.debugLog("Url obtained from services definition: " + backendURL);
+ // removed, idle timeout is used or specified in send() methods:
+ // container.setAsyncSendTimeout(config.getWebsocketAsyncWriteTimeout());
- 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);
+ //same as setIdleTimeout: container.setDefaultMaxSessionIdleTimeout(config.getWebsocketIdleTimeout());
- /* 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/JWTValidatorFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/JWTValidatorFactory.java
index 9cb93bd9a2..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.websocket.servlet.ServletUpgradeRequest;
-import javax.servlet.ServletException;
-import java.net.HttpCookie;
+import jakarta.servlet.ServletException;
+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(ServletUpgradeRequest 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(ServletUpgradeRequest 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/KnoxWebSocketCreator.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java
new file mode 100644
index 0000000000..a73db079c2
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/KnoxWebSocketCreator.java
@@ -0,0 +1,315 @@
+/*
+ * 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.util.Callback;
+import org.eclipse.jetty.websocket.api.util.WSURI;
+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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+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 Set IGNORED_HEADERS = new HashSet<>(Arrays.asList(
+ "sec-websocket-key",
+ "sec-websocket-version",
+ "sec-websocket-extensions",
+ "sec-websocket-accept",
+ "sec-websocket-protocol",
+ "upgrade",
+ "connection",
+ "host" // The Jetty client will automatically set the correct Host for the backend
+ ));
+
+ 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) throws Exception {
+ try {
+ // 1. Get the raw HTTP URI from the Jetty 12 Request and convert it to ws URI
+ final URI requestURI = WSURI.toWebsocket(req.getHttpURI().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);
+ throw e;
+ }
+ }
+
+ 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()) {
+ String headerName = field.getName();
+ if (!IGNORED_HEADERS.contains(headerName.toLowerCase(Locale.ROOT))) {
+ headers.computeIfAbsent(headerName, 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());
+ // Only append a slash if getPath() doesn't already start with one
+ String serviceUrlPath = serviceUrl.getPath();
+ if (StringUtils.isNotEmpty(serviceUrlPath) && !serviceUrlPath.startsWith("/")) {
+ backend.append('/');
+ }
+ backend.append(serviceUrlPath);
+ 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());
+ // Only append a slash if getPath() doesn't already start with one
+ String serviceUrlPath = serviceUrl.getPath();
+ if (StringUtils.isNotEmpty(serviceUrlPath) && !serviceUrlPath.startsWith("/")) {
+ backend.append('/');
+ }
+ backend.append(serviceUrlPath);
+ }
+ /* 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/MessageEventCallback.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/MessageEventCallback.java
index 2f7fcbed49..15ec4647ef 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/MessageEventCallback.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/MessageEventCallback.java
@@ -17,8 +17,8 @@
*/
package org.apache.knox.gateway.websockets;
-import javax.websocket.CloseReason;
-import javax.websocket.PongMessage;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.PongMessage;
/**
* A simple callback interface used when evens happen on the Websocket client socket.
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundClient.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundClient.java
index a1797eda24..bbe9e81894 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundClient.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundClient.java
@@ -16,12 +16,12 @@
*/
package org.apache.knox.gateway.websockets;
-import javax.websocket.CloseReason;
-import javax.websocket.Endpoint;
-import javax.websocket.EndpointConfig;
-import javax.websocket.MessageHandler;
-import javax.websocket.Session;
-import javax.websocket.PongMessage;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.MessageHandler;
+import jakarta.websocket.Session;
+import jakarta.websocket.PongMessage;
/**
* A Websocket client with callback which is not annotation based.
@@ -52,7 +52,7 @@ public ProxyInboundClient(final MessageEventCallback callback) {
* @param config the configuration used to configure this endpoint.
*/
@Override
- public void onOpen(final javax.websocket.Session backendSession, final EndpointConfig config) {
+ public void onOpen(final jakarta.websocket.Session backendSession, final EndpointConfig config) {
this.session = backendSession;
this.config = config;
@@ -105,13 +105,13 @@ public void onMessage(final PongMessage pongMessage) {
}
@Override
- public void onClose(final javax.websocket.Session backendSession, final CloseReason closeReason) {
+ public void onClose(final jakarta.websocket.Session backendSession, final CloseReason closeReason) {
callback.onConnectionClose(closeReason);
this.session = null;
}
@Override
- public void onError(final javax.websocket.Session backendSession, final Throwable cause) {
+ public void onError(final jakarta.websocket.Session backendSession, final Throwable cause) {
callback.onError(cause);
this.session = null;
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundSocket.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundSocket.java
index e133b0f255..56665bf5c9 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundSocket.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/ProxyInboundSocket.java
@@ -17,12 +17,12 @@
*/
package org.apache.knox.gateway.websockets;
-import javax.websocket.ClientEndpoint;
-import javax.websocket.CloseReason;
-import javax.websocket.OnClose;
-import javax.websocket.OnError;
-import javax.websocket.OnMessage;
-import javax.websocket.OnOpen;
+import jakarta.websocket.ClientEndpoint;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.OnClose;
+import jakarta.websocket.OnError;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.OnOpen;
/**
* A Websocket client with callback.
@@ -43,7 +43,7 @@ public ProxyInboundSocket(final MessageEventCallback callback) {
/* Client methods */
@OnOpen
- public void onClientOpen(final javax.websocket.Session backendSession) {
+ public void onClientOpen(final jakarta.websocket.Session backendSession) {
callback.onConnectionOpen(backendSession);
@@ -61,14 +61,14 @@ public void onClientError(Throwable cause) {
@OnMessage(maxMessageSize = Integer.MAX_VALUE)
public void onBackendMessage(final String message,
- final javax.websocket.Session session) {
+ final jakarta.websocket.Session session) {
callback.onMessageText(message, session);
}
@OnMessage(maxMessageSize = Integer.MAX_VALUE)
public void onBackendMessageBinary(final byte[] message, final boolean last,
- final javax.websocket.Session session) {
+ final jakarta.websocket.Session session) {
callback.onMessageBinary(message, last, session);
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 37c94a6ac5..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 javax.websocket.ClientEndpointConfig;
-import javax.websocket.CloseReason;
-import javax.websocket.ContainerProvider;
-import javax.websocket.DeploymentException;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.CloseReason;
+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 */
- private javax.websocket.Session backendSession;
+ /** 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(javax.websocket.PongMessage message, Object session) {
- LOG.logMessage("[From Backend <---]: PING");
+ public void onMessagePong(jakarta.websocket.PongMessage message, Object session) {
+ 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
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebSocketFilterConfig.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebSocketFilterConfig.java
index b0d0602fd7..33baab9c5f 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebSocketFilterConfig.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebSocketFilterConfig.java
@@ -17,8 +17,8 @@
*/
package org.apache.knox.gateway.websockets;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/AuditLoggingTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/AuditLoggingTest.java
index 2a5d27ce8f..3ae5521b74 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/AuditLoggingTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/AuditLoggingTest.java
@@ -41,13 +41,13 @@
import org.junit.Before;
import org.junit.Test;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayFilterTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayFilterTest.java
index 4dadbd9690..9610f2b603 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayFilterTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayFilterTest.java
@@ -32,13 +32,13 @@
import org.junit.Test;
import org.junit.experimental.categories.Category;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URISyntaxException;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayForwardingServletTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayForwardingServletTest.java
index 72876b0c51..48a0711830 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayForwardingServletTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayForwardingServletTest.java
@@ -19,12 +19,12 @@
import java.io.IOException;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
@@ -43,6 +43,7 @@ public void testRedirectDefaults() throws ServletException, IOException {
EasyMock.expect(config.getServletName()).andStubReturn("default");
EasyMock.expect(config.getServletContext()).andStubReturn(context);
EasyMock.expect(config.getInitParameter("redirectTo")).andReturn("/gateway/sandbox");
+ EasyMock.expect(config.getInitParameter("jakarta.servlet.http.legacyDoHead")).andReturn("false");
EasyMock.expect(request.getMethod()).andReturn("GET").anyTimes();
EasyMock.expect(request.getPathInfo()).andReturn("/webhdfs/v1/tmp").anyTimes();
EasyMock.expect(request.getQueryString()).andReturn("op=LISTSTATUS");
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/filter/ForwardedRequestTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/filter/ForwardedRequestTest.java
index f72e86f490..755429056a 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/filter/ForwardedRequestTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/filter/ForwardedRequestTest.java
@@ -20,7 +20,7 @@
import org.junit.Assert;
import org.junit.Test;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
import java.util.Locale;
public class ForwardedRequestTest {
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/filter/HSTSHandlerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/filter/HSTSHandlerTest.java
index e6dcc410c0..f0761b6c79 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/filter/HSTSHandlerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/filter/HSTSHandlerTest.java
@@ -16,26 +16,27 @@
*/
package org.apache.knox.gateway.filter;
-import junit.framework.TestCase;
import org.easymock.EasyMock;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.server.Response;
import org.junit.Test;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
+import static org.junit.Assert.assertEquals;
-public class HSTSHandlerTest extends TestCase {
+public class HSTSHandlerTest {
@Test
- public void testHandle() throws ServletException, IOException {
- HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
- response.setHeader("Strict-Transport-Security", "max-age=1000");
- EasyMock.expectLastCall().once();
+ public void testHandle() throws Exception {
+ Response response = EasyMock.createNiceMock(Response.class);
+ HttpFields.Mutable headers = HttpFields.build();
+ EasyMock.expect(response.getHeaders()).andReturn(headers).anyTimes();
+
EasyMock.replay(response);
HSTSHandler hstsHandler = new HSTSHandler("max-age=1000");
- hstsHandler.handle("", null, null, response);
+ hstsHandler.handle(null, response, null);
EasyMock.verify(response);
+ assertEquals("max-age=1000", headers.get("Strict-Transport-Security"));
}
}
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;
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 4366ae732f..a0b040f15f 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
@@ -27,11 +27,12 @@
import org.junit.Test;
import org.easymock.EasyMock;
-import javax.websocket.CloseReason;
-import javax.websocket.ContainerProvider;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.ContainerProvider;
+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;
@@ -66,7 +67,7 @@ public void testBadBackEnd() throws Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- javax.websocket.Session session = container.connectToServer(client,
+ jakarta.websocket.Session session = container.connectToServer(client,
proxyUri);
session.getBasicRemote().sendText(message);
@@ -76,13 +77,17 @@ public void testBadBackEnd() throws Exception {
private static void startProxy() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.replay(gatewayConfig);
proxy = new Server();
proxyConnector = new ServerConnector(proxy);
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..7ef26f4a46 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 {
+public 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 09402a0ead..7aaff46189 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,15 +43,15 @@
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;
import org.junit.Test;
-import javax.websocket.CloseReason;
-import javax.websocket.ContainerProvider;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.WebSocketContainer;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@@ -149,19 +149,25 @@ public void testBadUrl() throws Exception {
}
private static void startGatewayServer() throws Exception {
+ setupGatewayConfig(BACKEND);
gatewayServer = new Server();
final ServerConnector connector = new ServerConnector(gatewayServer);
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();
context.setContextPath("/");
handlers.addHandler(context);
- gatewayServer.setHandler(handlers);
+ /* Setup websocket handler */
+ final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
+ gatewayConfig, services);
+ gatewayWebsocketHandler.setHandler(handlers);
+
+ gatewayServer.setHandler(gatewayWebsocketHandler);
// Start Server
gatewayServer.start();
@@ -172,14 +178,6 @@ private static void startGatewayServer() throws Exception {
}
int port = connector.getLocalPort();
serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
-
- /* Setup websocket handler */
- setupGatewayConfig(BACKEND);
-
- final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
- gatewayConfig, services);
- handlers.addHandler(gatewayWebsocketHandler);
- gatewayWebsocketHandler.start();
}
/*
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 b2cf2433bf..0129b45dcc 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
@@ -26,11 +26,12 @@
import org.junit.Test;
import org.easymock.EasyMock;
-import javax.websocket.ContainerProvider;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.WebSocketContainer;
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;
@@ -79,7 +80,7 @@ public void testDroppedConnection() throws IOException, Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- javax.websocket.Session session = container.connectToServer(client,
+ jakarta.websocket.Session session = container.connectToServer(client,
proxyUri);
session.getBasicRemote().sendText(message);
@@ -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("/");
@@ -115,13 +116,15 @@ private static void startBackend() throws Exception {
private static void startProxy() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.replay(gatewayConfig);
proxy = new Server();
proxyConnector = new ServerConnector(proxy);
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..68c08360c0 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,12 @@
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.util.Callback;
+import org.eclipse.jetty.websocket.api.util.WSURI;
+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 +46,7 @@
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.net.URI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
@@ -80,12 +79,15 @@ 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();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
+ EasyMock.expect(jwtValidator.getUsername()).andReturn("testUser").anyTimes();
PowerMock.mockStatic(JWTValidatorFactory.class);
EasyMock.expect(JWTValidatorFactory.create(req, gatewayServices, gatewayConfig)).andReturn(jwtValidator).anyTimes();
@@ -94,8 +96,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
@@ -105,12 +108,14 @@ 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();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(true).anyTimes();
+ EasyMock.expect(jwtValidator.getUsername()).andReturn("testUser").anyTimes();
PowerMock.mockStatic(JWTValidatorFactory.class);
EasyMock.expect(JWTValidatorFactory.create(req, gatewayServices, gatewayConfig)).andReturn(jwtValidator).anyTimes();
@@ -119,8 +124,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
@@ -135,9 +140,11 @@ 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();
+ Callback callback = EasyMock.createNiceMock(Callback.class);
+ EasyMock.replay(callback);
JWTValidator jwtValidator = EasyMock.createNiceMock(JWTValidator.class);
EasyMock.expect(jwtValidator.validate()).andReturn(false).anyTimes();
@@ -147,8 +154,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);
}
@@ -160,79 +167,35 @@ 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();
+ 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 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);
+ URI httpUri = WSURI.toHttp(new URI(url));
+
+ HttpURI httpURI = HttpURI.build(httpUri);
+
+ // 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/JWTValidatorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/JWTValidatorTest.java
index e79bea6d7c..529c4c9db3 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,10 @@
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.HttpCookie;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
import org.junit.After;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -44,7 +47,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;
@@ -54,8 +56,10 @@
import java.security.interfaces.RSAPublicKey;
import java.text.MessageFormat;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@@ -112,11 +116,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){
+ List mockedCookies = Arrays.asList(
+ HttpCookie.from("hadoop-jwt", "garbage"),
+ HttpCookie.from("hadoop-jwt", "ljm" + jwt.serialize()),
+ HttpCookie.from("hadoop-jwt", jwt.serialize())
+ );
+ EasyMock.expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(mockedCookies).anyTimes();
}
private static SignedJWT getJWT(final String issuer,
@@ -190,7 +196,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 +217,11 @@ 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.expect(request.getAttribute(Request.COOKIE_ATTRIBUTE))
+ .andReturn(Collections.emptyList())
+ .anyTimes();
EasyMock.replay(request);
JWTValidator jwtValidator = JWTValidatorFactory.create(request, gatewayServices, gatewayConfig);
EasyMock.expect(authorityService.verifyToken(jwtValidator.getToken(), publicKey)).andReturn(true).anyTimes();
@@ -233,7 +242,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 +264,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 +286,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 +308,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 +329,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 +351,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);
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 3bde19e6e3..b2f6eb3d3c 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
@@ -29,11 +29,12 @@
import org.junit.Test;
import org.easymock.EasyMock;
-import javax.websocket.CloseReason;
-import javax.websocket.ContainerProvider;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.ContainerProvider;
+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;
@@ -77,7 +78,7 @@ public void testMessageTooBig() throws Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- javax.websocket.Session session = container.connectToServer(client,
+ jakarta.websocket.Session session = container.connectToServer(client,
proxyUri);
session.getBasicRemote().sendText(bigMessage);
@@ -91,12 +92,13 @@ public void testMessageTooBig() throws Exception {
*/
@Test(timeout = 8000)
public void testMessageBiggerThanDefault() throws Exception {
+ //Note: default is WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE = 65536
final String bigMessage = RandomStringUtils.randomAscii(66000);
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- javax.websocket.Session session = container.connectToServer(client,
+ jakarta.websocket.Session session = container.connectToServer(client,
proxyUri);
session.getBasicRemote().sendText(bigMessage);
@@ -115,7 +117,7 @@ public void testMessageOk() throws Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- javax.websocket.Session session = container.connectToServer(client,
+ jakarta.websocket.Session session = container.connectToServer(client,
proxyUri);
session.getBasicRemote().sendText(message);
@@ -130,8 +132,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("/");
@@ -151,13 +152,17 @@ private static void startBackend() throws Exception {
private static void startProxy() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.replay(gatewayConfig);
+
proxy = new Server();
proxyConnector = new ServerConnector(proxy);
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/ProxyInboundClientTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ProxyInboundClientTest.java
index 08c4111d6a..eabae24323 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ProxyInboundClientTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/ProxyInboundClientTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.knox.gateway.websockets;
+import jakarta.websocket.ClientEndpointConfig;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -25,12 +26,12 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import javax.websocket.CloseReason;
-import javax.websocket.ContainerProvider;
-import javax.websocket.DeploymentException;
-import javax.websocket.PongMessage;
-import javax.websocket.Session;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.DeploymentException;
+import jakarta.websocket.PongMessage;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
@@ -130,9 +131,9 @@ public void onMessagePong(PongMessage message, Object session) {
}
});
- Assert.assertThat(client, instanceOf(javax.websocket.Endpoint.class));
-
- Session session = container.connectToServer(client, serverUri);
+ Assert.assertThat(client, instanceOf(jakarta.websocket.Endpoint.class));
+ ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().build();
+ Session session = container.connectToServer(client, clientConfig, serverUri);
session.getBasicRemote().sendText(textMessage);
@@ -187,9 +188,9 @@ public void onMessagePong(PongMessage message, Object session) {
}
});
- Assert.assertThat(client, instanceOf(javax.websocket.Endpoint.class));
-
- Session session = container.connectToServer(client, serverUri);
+ Assert.assertThat(client, instanceOf(jakarta.websocket.Endpoint.class));
+ ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().build();
+ Session session = container.connectToServer(client, clientConfig, serverUri);
session.getBasicRemote().sendBinary(binarymessage);
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..ae871e2e29 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
@@ -17,6 +17,13 @@
*/
package org.apache.knox.gateway.websockets;
+import org.apache.knox.gateway.config.GatewayConfig;
+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.easymock.EasyMock;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -70,8 +77,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,9 +90,50 @@ 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));
}
+
+ @Test
+ public void testMatchedBackendURLDoubleSlashPrevention() throws Exception {
+ GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
+ ServiceRegistry serviceRegistry = EasyMock.createNiceMock(ServiceRegistry.class);
+ ServiceDefinitionRegistry serviceDefinitionRegistry = EasyMock.createNiceMock(ServiceDefinitionRegistry.class);
+ ServiceDefEntry serviceDefEntry = EasyMock.createNiceMock(ServiceDefEntry.class);
+
+ EasyMock.expect(services.getService(ServiceType.SERVICE_REGISTRY_SERVICE))
+ .andReturn(serviceRegistry).anyTimes();
+ EasyMock.expect(services.getService(ServiceType.SERVICE_DEFINITION_REGISTRY))
+ .andReturn(serviceDefinitionRegistry).anyTimes();
+
+ EasyMock.expect(serviceDefinitionRegistry.getMatchingService(EasyMock.anyString()))
+ .andReturn(serviceDefEntry).anyTimes();
+ EasyMock.expect(serviceDefEntry.getName())
+ .andReturn("WEBSOCKET").anyTimes();
+
+ // Simulate the backend topology returning an HTTP URL with a path that starts with a slash
+ EasyMock.expect(serviceRegistry.lookupServiceURL(EasyMock.anyString(), EasyMock.anyString()))
+ .andReturn("http://localhost:53170/ws").anyTimes();
+
+ EasyMock.replay(config, services, serviceRegistry, serviceDefinitionRegistry, serviceDefEntry);
+
+ KnoxWebSocketCreator creator = new KnoxWebSocketCreator(config, services);
+ URI requestURI = new URI("ws://localhost:8443/gateway/websocket/123foo456bar/channels");
+ String backendURL = creator.getMatchedBackendURL(requestURI);
+
+ // Assert that the double slash is prevented
+ assertThat(backendURL, is("ws://localhost:53170/ws"));
+
+ // Let's also verify that HTTPS correctly maps to WSS without double slashes
+ EasyMock.reset(serviceRegistry);
+ EasyMock.expect(serviceRegistry.lookupServiceURL(EasyMock.anyString(), EasyMock.anyString()))
+ .andReturn("https://localhost:53170/ws").anyTimes();
+ EasyMock.replay(serviceRegistry);
+
+ backendURL = creator.getMatchedBackendURL(requestURI);
+ assertThat(backendURL, is("wss://localhost:53170/ws"));
+ }
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketClient.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketClient.java
index 287331543d..5339b95713 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketClient.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketClient.java
@@ -26,14 +26,14 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import javax.websocket.ClientEndpoint;
-import javax.websocket.CloseReason;
-import javax.websocket.OnClose;
-import javax.websocket.OnError;
-import javax.websocket.OnMessage;
-import javax.websocket.OnOpen;
-import javax.websocket.PongMessage;
-import javax.websocket.Session;
+import jakarta.websocket.ClientEndpoint;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.OnClose;
+import jakarta.websocket.OnError;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.OnOpen;
+import jakarta.websocket.PongMessage;
+import jakarta.websocket.Session;
import org.eclipse.jetty.util.BlockingArrayQueue;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java
index 717a45489a..8e12b04794 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java
@@ -21,9 +21,9 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import javax.websocket.ContainerProvider;
-import javax.websocket.Session;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
import java.net.URI;
import java.util.concurrent.TimeUnit;
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/WebsocketEchoTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
index 1d3dbb886d..ae059b86f1 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
@@ -22,9 +22,9 @@
import org.junit.Test;
import java.net.URI;
-import javax.websocket.ContainerProvider;
-import javax.websocket.Session;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.is;
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..1c42998b46 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.Wrapper handler;
private static File topoDir;
private static Path dataDir;
@@ -157,8 +157,8 @@ private static void startWebsocketServer(final String type, final String context
ContextHandler context = new ContextHandler();
context.setContextPath("/");
- context.setHandler(handler);
- backendServer.setHandler(context);
+ handler.setHandler(context);
+ backendServer.setHandler(handler);
// Start Server
backendServer.start();
@@ -184,15 +184,22 @@ private static void startGatewayServer() throws Exception {
final ServerConnector connector = new ServerConnector(gatewayServer);
gatewayServer.addConnector(connector);
+ setupGatewayConfig(backendServerUri.toString());
+
/* 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();
context.setContextPath("/");
handlers.addHandler(context);
- gatewayServer.setHandler(handlers);
+ /* Setup websocket handler */
+ final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
+ gatewayConfig, services);
+ gatewayWebsocketHandler.setHandler(handlers);
+
+ gatewayServer.setHandler(gatewayWebsocketHandler);
// Start Server
gatewayServer.start();
@@ -203,14 +210,6 @@ private static void startGatewayServer() throws Exception {
}
int port = connector.getLocalPort();
serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
-
- /* Setup websocket handler */
- setupGatewayConfig(backendServerUri.toString());
-
- final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
- gatewayConfig, services);
- handlers.addHandler(gatewayWebsocketHandler);
- gatewayWebsocketHandler.start();
}
/**
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 00a1c26975..4d7bce00e0 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
@@ -26,6 +26,7 @@
import com.mycila.xmltool.XMLDoc;
import com.mycila.xmltool.XMLTag;
+import jakarta.websocket.ClientEndpointConfig;
import org.apache.commons.io.FileUtils;
import org.apache.knox.gateway.GatewayServer;
import org.apache.knox.gateway.config.GatewayConfig;
@@ -43,18 +44,18 @@
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;
import org.junit.Test;
-import javax.websocket.ContainerProvider;
-import javax.websocket.Endpoint;
-import javax.websocket.EndpointConfig;
-import javax.websocket.MessageHandler;
-import javax.websocket.Session;
-import javax.websocket.WebSocketContainer;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.MessageHandler;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@@ -164,7 +165,7 @@ public void testMultipleConnections() throws Exception {
public void onMessage(String message) {
latch.countDown();
}
- }, new URI(serverUri.toString() + "gateway/websocket/ws"));
+ }, ClientEndpointConfig.Builder.create().build(), new URI(serverUri.toString() + "gateway/websocket/ws"));
}
for (int i = 0; i < MAX_CONNECTIONS; i++) {
@@ -197,8 +198,8 @@ private static void startWebsocketServer() throws Exception {
ContextHandler context = new ContextHandler();
context.setContextPath("/");
- context.setHandler(handler);
- backendServer.setHandler(context);
+ handler.setHandler(context);
+ backendServer.setHandler(handler);
// Start Server
backendServer.start();
@@ -212,20 +213,26 @@ private static void startWebsocketServer() throws Exception {
}
private static void startGatewayServer() throws Exception {
+ setupGatewayConfig(backendServerUri.toString());
/* use default Max threads */
gatewayServer = new Server(new QueuedThreadPool(254));
final ServerConnector connector = new ServerConnector(gatewayServer);
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();
context.setContextPath("/");
handlers.addHandler(context);
- gatewayServer.setHandler(handlers);
+ /* Setup websocket handler */
+ final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
+ gatewayConfig, services);
+ gatewayWebsocketHandler.setHandler(handlers);
+
+ gatewayServer.setHandler(gatewayWebsocketHandler);
// Start Server
gatewayServer.start();
@@ -237,13 +244,6 @@ private static void startGatewayServer() throws Exception {
int port = connector.getLocalPort();
serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
- /* Setup websocket handler */
- setupGatewayConfig(backendServerUri.toString());
-
- final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
- gatewayConfig, services);
- handlers.addHandler(gatewayWebsocketHandler);
- gatewayWebsocketHandler.start();
}
/**
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 26722dfdac..c148f516b0 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 javax.websocket.ContainerProvider;
-import javax.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 {
+ public 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 7c6d736ef8..740a190677 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,26 +17,24 @@
*/
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.Frame;
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.core.OpCode;
+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 javax.websocket.ContainerProvider;
-import javax.websocket.WebSocketContainer;
-import java.io.IOException;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.WebSocketContainer;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.is;
@@ -64,13 +62,15 @@
*/
public class WebsocketServerInitiatedPingTest extends WebsocketEchoTestBase {
+ private static WebsocketServerInitiatedPingHandler pingHandler;
public WebsocketServerInitiatedPingTest() {
super();
}
@BeforeClass
public static void setUpBeforeClass() throws Exception {
- handler = new WebsocketServerInitiatedPingHandler();
+ pingHandler = new WebsocketServerInitiatedPingHandler();
+ handler = pingHandler;
WebsocketEchoTestBase.setUpBeforeClass();
WebsocketEchoTestBase.startServers("ws");
}
@@ -88,30 +88,30 @@ public void testGatewayServerInitiatedPing() throws Exception {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
WebsocketClient client = new WebsocketClient();
- container.connectToServer(client,
- new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels"));
-
- //session.getBasicRemote().sendText("Echo");
- client.messageQueue.awaitMessages(1, 10000, TimeUnit.MILLISECONDS);
-
- assertThat(client.messageQueue.get(0), is("PingPong"));
+ try (jakarta.websocket.Session session = container.connectToServer(client,
+ new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels"))) {
+ assertThat(session.isOpen(), is(true));
+ //session.getBasicRemote().sendText("Echo");
+ // Wait for the backend server to receive the automatic PONG from Knox's JSR-356 container
+ String pongPayload = pingHandler.socket.pongFuture.get(10000, TimeUnit.MILLISECONDS);
+
+ assertThat(pongPayload, is("PingPong"));
+ }
}
/**
* A Mock websocket handler
*
*/
- private static class WebsocketServerInitiatedPingHandler extends WebSocketHandler implements WebSocketCreator {
- private final ServerInitiatingPingSocket socket = new ServerInitiatingPingSocket();
-
+ private static class WebsocketServerInitiatedPingHandler extends AbstractWebSocketHandler {
+ public final ServerInitiatingPingSocket socket = new ServerInitiatingPingSocket();
@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) {
+ public Object createWebSocket(ServerUpgradeRequest req, ServerUpgradeResponse resp, Callback callback) {
return socket;
}
}
@@ -119,7 +119,8 @@ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse
/**
* A simple socket initiating message on connect
*/
- private static class ServerInitiatingPingSocket extends WebSocketAdapter {
+ public static class ServerInitiatingPingSocket extends Session.Listener.AbstractAutoDemanding {
+ public final CompletableFuture pongFuture = new CompletableFuture<>();
@Override
public void onWebSocketError(Throwable cause) {
@@ -127,25 +128,39 @@ 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();
+ // 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);
+ }
+
+ @Override
+ public void onWebSocketFrame(Frame frame, org.eclipse.jetty.websocket.api.Callback callback) {
+ // Intercept PONG frames returning from Knox
+ if (frame.getOpCode() == OpCode.PONG) {
+ ByteBuffer payload = frame.getPayload();
+ if (payload != null) {
+ byte[] bytes = new byte[payload.remaining()];
+ payload.get(bytes);
+ pongFuture.complete(new String(bytes, StandardCharsets.UTF_8));
+ } else {
+ pongFuture.complete("");
}
- } catch (IOException x) {
- throw new RuntimeIOException(x);
}
+ super.onWebSocketFrame(frame, callback);
}
}
}
diff --git a/gateway-service-admin/pom.xml b/gateway-service-admin/pom.xml
index c9b49ac323..4aee03b800 100644
--- a/gateway-service-admin/pom.xml
+++ b/gateway-service-admin/pom.xml
@@ -54,12 +54,12 @@
gateway-service-definitions