getAliasMapper()
+ {
+ return _aliasMapper;
+ }
+
+ /**
+ * Sets a function that transforms the alias into a possibly different alias,
+ * invoked when the SNI logic must choose the alias to pick the right certificate.
+ * This function is required when using the
+ * {@link SslContextFactory.Server#setKeyManagerFactoryAlgorithm(String) PKIX KeyManagerFactory algorithm}
+ * which suffers from bug https://bugs.openjdk.java.net/browse/JDK-8246262,
+ * where aliases are returned by the OpenJDK implementation to the application
+ * in the form {@code N.0.alias} where {@code N} is an always increasing number.
+ * Such mangled aliases won't match the aliases in the keystore, so that for
+ * example SNI matching will always fail.
+ * Other implementations such as BouncyCastle have been reported to mangle
+ * the alias in a different way, namely {@code 0.alias.N}.
+ * This function allows to "unmangle" the alias from the implementation
+ * specific mangling back to just {@code alias} so that SNI matching will work
+ * again.
+ *
+ * @param aliasMapper the function that transforms the alias
+ */
+ public void setAliasMapper(UnaryOperator aliasMapper)
+ {
+ _aliasMapper = Objects.requireNonNull(aliasMapper);
+ }
+
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
{
@@ -80,10 +116,15 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
protected String chooseServerAlias(String keyType, Principal[] issuers, Collection matchers, SSLSession session)
{
// Look for the aliases that are suitable for the keyType and issuers.
- String[] aliases = _delegate.getServerAliases(keyType, issuers);
- if (aliases == null || aliases.length == 0)
+ String[] mangledAliases = _delegate.getServerAliases(keyType, issuers);
+ if (mangledAliases == null || mangledAliases.length == 0)
return null;
+ // Apply the alias mapping, keeping the alias order.
+ Map aliasMap = new LinkedHashMap<>();
+ Arrays.stream(mangledAliases)
+ .forEach(alias -> aliasMap.put(getAliasMapper().apply(alias), alias));
+
// Find our SNIMatcher. There should only be one and it always matches (always returns true
// from AliasSNIMatcher.matches), but it will capture the SNI Host if one was presented.
String host = matchers == null ? null : matchers.stream()
@@ -96,35 +137,40 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
try
{
// Filter the certificates by alias.
- Collection certificates = Arrays.stream(aliases)
+ Collection certificates = aliasMap.keySet().stream()
.map(_sslContextFactory::getX509)
.filter(Objects::nonNull)
.collect(Collectors.toList());
- // delegate the decision to accept to the sniSelector
+ // Delegate the decision to accept to the sniSelector.
SniSelector sniSelector = _sslContextFactory.getSNISelector();
if (sniSelector == null)
sniSelector = _sslContextFactory;
String alias = sniSelector.sniSelect(keyType, issuers, session, host, certificates);
- // Check selected alias
- if (alias != null && alias != SniSelector.DELEGATE)
- {
- // Make sure we got back an alias from the acceptable aliases.
- X509 x509 = _sslContextFactory.getX509(alias);
- if (!Arrays.asList(aliases).contains(alias) || x509 == null)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Invalid X509 match for SNI {}: {}", host, alias);
- return null;
- }
+ // Check the selected alias.
+ if (alias == null || alias == SniSelector.DELEGATE)
+ return alias;
+ // Make sure we got back an alias from the acceptable aliases.
+ X509 x509 = _sslContextFactory.getX509(alias);
+ if (!aliasMap.containsKey(alias) || x509 == null)
+ {
if (LOG.isDebugEnabled())
- LOG.debug("Matched SNI {} with X509 {} from {}", host, x509, Arrays.asList(aliases));
- if (session != null)
- session.putValue(SNI_X509, x509);
+ LOG.debug("Invalid X509 match for SNI {}: {}", host, alias);
+ return null;
}
- return alias;
+
+ if (session != null)
+ session.putValue(SNI_X509, x509);
+
+ // Convert the selected alias back to the original
+ // value before the alias mapping performed above.
+ String mangledAlias = aliasMap.get(alias);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Matched SNI {} with alias {}, certificate {} from aliases {}", host, mangledAlias, x509, aliasMap.keySet());
+ return mangledAlias;
}
catch (Throwable x)
{
@@ -141,10 +187,11 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
String alias = (socket == null)
? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
: chooseServerAlias(keyType, issuers, sslSocket.getSSLParameters().getSNIMatchers(), sslSocket.getHandshakeSession());
- if (alias == SniSelector.DELEGATE)
+ boolean delegate = alias == SniSelector.DELEGATE;
+ if (delegate)
alias = _delegate.chooseServerAlias(keyType, issuers, socket);
if (LOG.isDebugEnabled())
- LOG.debug("Chose alias {}/{} on {}", alias, keyType, socket);
+ LOG.debug("Chose {} alias {}/{} on {}", delegate ? "delegate" : "explicit", alias, keyType, socket);
return alias;
}
@@ -154,10 +201,11 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
String alias = (engine == null)
? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
: chooseServerAlias(keyType, issuers, engine.getSSLParameters().getSNIMatchers(), engine.getHandshakeSession());
- if (alias == SniSelector.DELEGATE)
+ boolean delegate = alias == SniSelector.DELEGATE;
+ if (delegate)
alias = _delegate.chooseEngineServerAlias(keyType, issuers, engine);
if (LOG.isDebugEnabled())
- LOG.debug("Chose alias {}/{} on {}", alias, keyType, engine);
+ LOG.debug("Chose {} alias {}/{} on {}", delegate ? "delegate" : "explicit", alias, keyType, engine);
return alias;
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java
index edce0f218be..d5f8da57c5f 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java
@@ -20,14 +20,28 @@ package org.eclipse.jetty.util.ssl;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.X509ExtendedKeyManager;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.util.resource.Resource;
@@ -43,6 +57,8 @@ import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesRegex;
import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -362,4 +378,81 @@ public class SslContextFactoryTest
assertNull(cf.getEndpointIdentificationAlgorithm());
}
+
+ @Test
+ public void testSNIWithPKIX() throws Exception
+ {
+ SslContextFactory.Server serverTLS = new SslContextFactory.Server()
+ {
+ @Override
+ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
+ {
+ SniX509ExtendedKeyManager result = new SniX509ExtendedKeyManager(keyManager, this);
+ result.setAliasMapper(alias ->
+ {
+ // Workaround for https://bugs.openjdk.java.net/browse/JDK-8246262.
+ Matcher matcher = Pattern.compile(".*\\..*\\.(.*)").matcher(alias);
+ if (matcher.matches())
+ return matcher.group(1);
+ return alias;
+ });
+ return result;
+ }
+ };
+ // This test requires a SNI keystore so that the X509ExtendedKeyManager is wrapped.
+ serverTLS.setKeyStoreResource(Resource.newSystemResource("keystore_sni.p12"));
+ serverTLS.setKeyStorePassword("storepwd");
+ serverTLS.setKeyManagerPassword("keypwd");
+ serverTLS.setKeyManagerFactoryAlgorithm("PKIX");
+ // Don't pick a default certificate if SNI does not match.
+ serverTLS.setSniRequired(true);
+ serverTLS.start();
+
+ SslContextFactory.Client clientTLS = new SslContextFactory.Client(true);
+ clientTLS.start();
+
+ try (SSLServerSocket serverSocket = serverTLS.newSslServerSocket(null, 0, 128);
+ SSLSocket clientSocket = clientTLS.newSslSocket())
+ {
+ SSLParameters sslParameters = clientSocket.getSSLParameters();
+ String hostName = "jetty.eclipse.org";
+ sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
+ clientSocket.setSSLParameters(sslParameters);
+ clientSocket.connect(new InetSocketAddress("localhost", serverSocket.getLocalPort()), 5000);
+ try (SSLSocket sslSocket = (SSLSocket)serverSocket.accept())
+ {
+ byte[] data = "HELLO".getBytes(StandardCharsets.UTF_8);
+ new Thread(() ->
+ {
+ try
+ {
+ // Start the TLS handshake and verify that
+ // the client got the right server certificate.
+ clientSocket.startHandshake();
+ Certificate[] certificates = clientSocket.getSession().getPeerCertificates();
+ assertThat(certificates.length, greaterThan(0));
+ X509Certificate certificate = (X509Certificate)certificates[0];
+ assertThat(certificate.getSubjectX500Principal().getName(), startsWith("CN=" + hostName));
+ // Send some data to verify communication is ok.
+ OutputStream output = clientSocket.getOutputStream();
+ output.write(data);
+ output.flush();
+ clientSocket.close();
+ }
+ catch (Throwable x)
+ {
+ x.printStackTrace();
+ }
+ }).start();
+ // Verify that we received the data the client sent.
+ sslSocket.setSoTimeout(5000);
+ InputStream input = sslSocket.getInputStream();
+ byte[] bytes = IO.readBytes(input);
+ assertArrayEquals(data, bytes);
+ }
+ }
+
+ clientTLS.stop();
+ serverTLS.stop();
+ }
}
diff --git a/jetty-util/src/test/resources/jetty-logging.properties b/jetty-util/src/test/resources/jetty-logging.properties
index 7367559fac9..33c6ec7e8a0 100644
--- a/jetty-util/src/test/resources/jetty-logging.properties
+++ b/jetty-util/src/test/resources/jetty-logging.properties
@@ -1,4 +1,3 @@
-# Setup default logging implementation for during testing
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.util.LEVEL=DEBUG
#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/HugeResourceTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/HugeResourceTest.java
index 4c75b95bd17..d530cd8a4f6 100644
--- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/HugeResourceTest.java
+++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/HugeResourceTest.java
@@ -244,6 +244,31 @@ public class HugeResourceTest
}
}
+ @ParameterizedTest
+ @MethodSource("staticFiles")
+ public void testHead(String filename, long expectedSize) throws Exception
+ {
+ URI destUri = server.getURI().resolve("/" + filename);
+ InputStreamResponseListener responseListener = new InputStreamResponseListener();
+
+ Request request = client.newRequest(destUri)
+ .method(HttpMethod.HEAD);
+ request.send(responseListener);
+ Response response = responseListener.get(5, TimeUnit.SECONDS);
+
+ try (InputStream in = responseListener.getInputStream())
+ {
+ assertThat(in.read(), is(-1));
+ }
+
+ assertThat("HTTP Response Code", response.getStatus(), is(200));
+ // dumpResponse(response);
+
+ String contentLength = response.getHeaders().get(HttpHeader.CONTENT_LENGTH);
+ long contentLengthLong = Long.parseLong(contentLength);
+ assertThat("Http Response Header: \"Content-Length: " + contentLength + "\"", contentLengthLong, is(expectedSize));
+ }
+
@ParameterizedTest
@MethodSource("staticFiles")
public void testUpload(String filename, long expectedSize) throws Exception
diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SingleOnMessageTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SingleOnMessageTest.java
new file mode 100644
index 00000000000..03acf813ed5
--- /dev/null
+++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SingleOnMessageTest.java
@@ -0,0 +1,177 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.tests;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.api.util.WSURI;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.server.NativeWebSocketServletContainerInitializer;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SingleOnMessageTest
+{
+ private final Server server = new Server();
+ private final WebSocketClient client = new WebSocketClient();
+ private final EventSocket serverSocket = new EventSocket();
+ private URI serverUri;
+
+ @BeforeEach
+ public void start() throws Exception
+ {
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ contextHandler.setContextPath("/");
+ NativeWebSocketServletContainerInitializer.configure(contextHandler,
+ (context, container) -> container.addMapping("/", (req, resp) -> serverSocket));
+ WebSocketUpgradeFilter.configure(contextHandler);
+
+ server.setHandler(contextHandler);
+ server.start();
+ serverUri = WSURI.toWebsocket(server.getURI());
+
+ client.start();
+ }
+
+ @AfterEach
+ public void stop() throws Exception
+ {
+ client.stop();
+ server.stop();
+ }
+
+ @Test
+ public void testTextHandler() throws Exception
+ {
+ TextOnlyHandler handler = new TextOnlyHandler();
+ client.connect(handler, serverUri);
+ assertTrue(handler.openLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(serverSocket.openLatch.await(5, TimeUnit.SECONDS));
+
+ // The server sends a sequence of Binary and Text messages
+ RemoteEndpoint remote = serverSocket.session.getRemote();
+ remote.sendBytes(BufferUtil.toBuffer("this should get rejected"));
+ remote.sendString("WebSocket_Data0");
+ remote.sendString("WebSocket_Data1");
+ serverSocket.session.close(StatusCode.NORMAL, "test complete");
+
+ // The client receives the messages and has discarded the binary message.
+ assertThat(handler.messages.poll(5, TimeUnit.SECONDS), is("WebSocket_Data0"));
+ assertThat(handler.messages.poll(5, TimeUnit.SECONDS), is("WebSocket_Data1"));
+ assertTrue(handler.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(handler.closeCode, is(StatusCode.NORMAL));
+ assertThat(handler.closeReason, is("test complete"));
+ }
+
+ @Test
+ public void testBinaryHandler() throws Exception
+ {
+ BinaryOnlyHandler handler = new BinaryOnlyHandler();
+ client.connect(handler, serverUri);
+ assertTrue(handler.openLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(serverSocket.openLatch.await(5, TimeUnit.SECONDS));
+
+ // The server sends a sequence of Binary and Text messages
+ RemoteEndpoint remote = serverSocket.session.getRemote();
+ remote.sendString("this should get rejected");
+ remote.sendBytes(BufferUtil.toBuffer("WebSocket_Data0"));
+ remote.sendBytes(BufferUtil.toBuffer("WebSocket_Data1"));
+ serverSocket.session.close(StatusCode.NORMAL, "test complete");
+
+ // The client receives the messages and has discarded the binary message.
+ assertThat(handler.messages.poll(5, TimeUnit.SECONDS), is(BufferUtil.toBuffer("WebSocket_Data0")));
+ assertThat(handler.messages.poll(5, TimeUnit.SECONDS), is(BufferUtil.toBuffer("WebSocket_Data1")));
+ assertTrue(handler.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(handler.closeCode, is(StatusCode.NORMAL));
+ assertThat(handler.closeReason, is("test complete"));
+ }
+
+ @WebSocket
+ public static class TextOnlyHandler extends AbstractHandler
+ {
+ final BlockingArrayQueue messages = new BlockingArrayQueue<>();
+
+ @OnWebSocketMessage
+ public void onMessage(String message)
+ {
+ messages.add(message);
+ }
+ }
+
+ @WebSocket
+ public static class BinaryOnlyHandler extends AbstractHandler
+ {
+ final BlockingArrayQueue messages = new BlockingArrayQueue<>();
+
+ @OnWebSocketMessage
+ public void onMessage(byte[] array, int offset, int length)
+ {
+ messages.add(BufferUtil.toBuffer(array, offset, length));
+ }
+ }
+
+ @WebSocket
+ public static class AbstractHandler
+ {
+ final CountDownLatch openLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ Session session;
+ int closeCode;
+ String closeReason;
+
+ @OnWebSocketClose
+ public void onClose(int statusCode, String reason)
+ {
+ this.closeCode = statusCode;
+ this.closeReason = reason;
+ this.closeLatch.countDown();
+ }
+
+ @OnWebSocketConnect
+ public void onConnect(Session session)
+ {
+ this.session = session;
+ this.openLatch.countDown();
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java
index 2c54ace5c18..d69fe68044f 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java
@@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.common.events;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.Objects;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
@@ -42,7 +43,7 @@ import org.eclipse.jetty.websocket.common.message.MessageAppender;
*/
public abstract class AbstractEventDriver extends AbstractLifeCycle implements IncomingFrames, EventDriver
{
- private static final Logger LOG = Log.getLogger(AbstractEventDriver.class);
+ private final Logger logger;
protected final Logger targetLog;
protected WebSocketPolicy policy;
protected final Object websocket;
@@ -51,8 +52,9 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
public AbstractEventDriver(WebSocketPolicy policy, Object websocket)
{
+ this.logger = Log.getLogger(this.getClass());
this.policy = policy;
- this.websocket = websocket;
+ this.websocket = Objects.requireNonNull(websocket, "WebSocket endpoint may not be null");
this.targetLog = Log.getLogger(websocket.getClass());
}
@@ -87,9 +89,9 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
@Override
public void incomingFrame(Frame frame)
{
- if (LOG.isDebugEnabled())
+ if (logger.isDebugEnabled())
{
- LOG.debug("incomingFrame({})", frame);
+ logger.debug("incomingFrame({})", frame);
}
try
@@ -112,9 +114,9 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
}
case OpCode.PING:
{
- if (LOG.isDebugEnabled())
+ if (logger.isDebugEnabled())
{
- LOG.debug("PING: {}", BufferUtil.toDetailString(frame.getPayload()));
+ logger.debug("PING: {}", BufferUtil.toDetailString(frame.getPayload()));
}
ByteBuffer pongBuf;
if (frame.hasPayload())
@@ -133,9 +135,9 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
}
case OpCode.PONG:
{
- if (LOG.isDebugEnabled())
+ if (logger.isDebugEnabled())
{
- LOG.debug("PONG: {}", BufferUtil.toDetailString(frame.getPayload()));
+ logger.debug("PONG: {}", BufferUtil.toDetailString(frame.getPayload()));
}
onPong(frame.getPayload());
break;
@@ -157,8 +159,8 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
}
default:
{
- if (LOG.isDebugEnabled())
- LOG.debug("Unhandled OpCode: {}", opcode);
+ if (logger.isDebugEnabled())
+ logger.debug("Unhandled OpCode: {}", opcode);
}
}
}
@@ -202,10 +204,9 @@ public abstract class AbstractEventDriver extends AbstractLifeCycle implements I
@Override
public void openSession(WebSocketSession session)
{
- if (LOG.isDebugEnabled())
+ if (logger.isDebugEnabled())
{
- LOG.debug("openSession({})", session);
- LOG.debug("objectFactory={}", session.getContainerScope().getObjectFactory());
+ logger.debug("openSession({}) objectFactory={}", session, session.getContainerScope().getObjectFactory());
}
this.session = session;
this.session.getContainerScope().getObjectFactory().decorate(this.websocket);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java
index cb993d213d0..c48d0d9de24 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java
@@ -22,7 +22,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
+import java.util.Objects;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
@@ -36,6 +38,7 @@ import org.eclipse.jetty.websocket.common.message.MessageReader;
import org.eclipse.jetty.websocket.common.message.NullMessage;
import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+import org.eclipse.jetty.websocket.common.util.TextUtil;
/**
* Handler for Annotated User WebSocket objects.
@@ -45,12 +48,12 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
private static final Logger LOG = Log.getLogger(JettyAnnotatedEventDriver.class);
private final JettyAnnotatedMetadata events;
private boolean hasCloseBeenCalled = false;
- private BatchMode batchMode;
+ private final BatchMode batchMode;
public JettyAnnotatedEventDriver(WebSocketPolicy policy, Object websocket, JettyAnnotatedMetadata events)
{
super(policy, websocket);
- this.events = events;
+ this.events = Objects.requireNonNull(events, "JettyAnnotatedMetadata may not be null");
WebSocket anno = websocket.getClass().getAnnotation(WebSocket.class);
// Setup the policy
@@ -71,6 +74,11 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
this.policy.setIdleTimeout(anno.maxIdleTime());
}
this.batchMode = anno.batchMode();
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("ctor / object={}, policy={}, batchMode={}, events={}", websocket, policy, batchMode, events);
+ }
}
@Override
@@ -82,20 +90,20 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
{
- if (events.onBinary == null)
+ if (LOG.isDebugEnabled())
{
- // not interested in binary events
- if (activeMessage == null)
- {
- activeMessage = NullMessage.INSTANCE;
- }
-
- return;
+ LOG.debug("onBinaryFrame({}, {}) - events.onBinary={}, activeMessage={}",
+ BufferUtil.toDetailString(buffer), fin, events.onBinary, activeMessage);
}
if (activeMessage == null)
{
- if (events.onBinary.isStreaming())
+ if (events.onBinary == null)
+ {
+ // not interested in binary events
+ activeMessage = NullMessage.INSTANCE;
+ }
+ else if (events.onBinary.isStreaming())
{
final MessageInputStream inputStream = new MessageInputStream(session);
activeMessage = inputStream;
@@ -126,6 +134,11 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onBinaryMessage(byte[] data)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onBinaryMessage([{}]) - events.onBinary={}", data.length, events.onBinary);
+ }
+
if (events.onBinary != null)
{
events.onBinary.call(websocket, session, data, 0, data.length);
@@ -141,6 +154,12 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
return;
}
hasCloseBeenCalled = true;
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onClose({}) - events.onClose={}", close, events.onClose);
+ }
+
if (events.onClose != null)
{
events.onClose.call(websocket, session, close.getStatusCode(), close.getReason());
@@ -150,6 +169,11 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onConnect()
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onConnect() - events.onConnect={}", events.onConnect);
+ }
+
if (events.onConnect != null)
{
events.onConnect.call(websocket, session);
@@ -159,6 +183,11 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onError(Throwable cause)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onError({}) - events.onError={}", cause.getClass().getName(), events.onError);
+ }
+
if (events.onError != null)
{
events.onError.call(websocket, session, cause);
@@ -172,6 +201,11 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onFrame(Frame frame)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onFrame({}) - events.onFrame={}", frame, events.onFrame);
+ }
+
if (events.onFrame != null)
{
events.onFrame.call(websocket, session, frame);
@@ -181,6 +215,13 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onInputStream(InputStream stream)
{
+ Objects.requireNonNull(stream, "InputStream may not be null");
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onInputStream({}) - events.onBinary={}", stream.getClass().getName(), events.onBinary);
+ }
+
if (events.onBinary != null)
{
events.onBinary.call(websocket, session, stream);
@@ -190,6 +231,13 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onReader(Reader reader)
{
+ Objects.requireNonNull(reader, "Reader may not be null");
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onReader({}) - events.onText={}", reader.getClass().getName(), events.onText);
+ }
+
if (events.onText != null)
{
events.onText.call(websocket, session, reader);
@@ -199,19 +247,20 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
{
- if (events.onText == null)
+ if (LOG.isDebugEnabled())
{
- // not interested in text events
- if (activeMessage == null)
- {
- activeMessage = NullMessage.INSTANCE;
- }
- return;
+ LOG.debug("onTextFrame({}, {}) - events.onText={}, activeMessage={}",
+ BufferUtil.toDetailString(buffer), fin, events.onText, activeMessage);
}
if (activeMessage == null)
{
- if (events.onText.isStreaming())
+ if (events.onText == null)
+ {
+ // not interested in text events
+ activeMessage = NullMessage.INSTANCE;
+ }
+ else if (events.onText.isStreaming())
{
MessageInputStream inputStream = new MessageInputStream(session);
activeMessage = new MessageReader(inputStream);
@@ -243,6 +292,12 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver
@Override
public void onTextMessage(String message)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onTextMessage([{}] \"{}\") - events.onText={}",
+ message.length(), TextUtil.maxStringLength(60, message), events.onText);
+ }
+
if (events.onText != null)
{
events.onText.call(websocket, session, message);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java
index 536faeaaf2a..c56f9efb89f 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java
@@ -24,27 +24,27 @@ import org.eclipse.jetty.websocket.common.events.annotated.OptionalSessionCallab
public class JettyAnnotatedMetadata
{
/**
- * @OnWebSocketConnect ()
+ * {@code @OnWebSocketConnect ()}
*/
public CallableMethod onConnect;
/**
- * @OnWebSocketMessage (byte[], or ByteBuffer, or InputStream)
+ * {@code @OnWebSocketMessage (byte[], or ByteBuffer, or InputStream)}
*/
public OptionalSessionCallableMethod onBinary;
/**
- * @OnWebSocketMessage (String, or Reader)
+ * {@code @OnWebSocketMessage (String, or Reader)}
*/
public OptionalSessionCallableMethod onText;
/**
- * @OnWebSocketFrame (Frame)
+ * {@code @OnWebSocketFrame (Frame)}
*/
public OptionalSessionCallableMethod onFrame;
/**
- * @OnWebSocketError (Throwable)
+ * {@code @OnWebSocketError (Throwable)}
*/
public OptionalSessionCallableMethod onError;
/**
- * @OnWebSocketClose (Frame)
+ * {@code @OnWebSocketClose (Frame)}
*/
public OptionalSessionCallableMethod onClose;
@@ -52,7 +52,8 @@ public class JettyAnnotatedMetadata
public String toString()
{
StringBuilder s = new StringBuilder();
- s.append("JettyPojoMetadata[");
+ s.append(this.getClass().getSimpleName());
+ s.append("[");
s.append("onConnect=").append(onConnect);
s.append(",onBinary=").append(onBinary);
s.append(",onText=").append(onText);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java
index 9af52f99937..dc9ed1059c3 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java
@@ -22,7 +22,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
+import java.util.Objects;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -38,6 +40,7 @@ import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.frames.ReadOnlyDelegatedFrame;
import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+import org.eclipse.jetty.websocket.common.util.TextUtil;
/**
* Handler for {@link WebSocketListener} based User WebSocket implementations.
@@ -58,12 +61,26 @@ public class JettyListenerEventDriver extends AbstractEventDriver
public JettyListenerEventDriver(WebSocketPolicy policy, WebSocketConnectionListener listener)
{
super(policy, listener);
- this.listener = listener;
+ this.listener = Objects.requireNonNull(listener, "Listener may not be null");
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("ctor / listener={}, policy={}", listener.getClass().getName(), policy);
+ }
}
@Override
public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onBinaryFrame({}, {}) - webSocketListener={}, webSocketPartialListener={}, listener={}, activeMessage={}",
+ BufferUtil.toDetailString(buffer), fin,
+ (listener instanceof WebSocketListener),
+ (listener instanceof WebSocketPartialListener),
+ listener.getClass().getName(),
+ activeMessage);
+ }
+
if (listener instanceof WebSocketListener)
{
if (activeMessage == null)
@@ -98,6 +115,14 @@ public class JettyListenerEventDriver extends AbstractEventDriver
@Override
public void onBinaryMessage(byte[] data)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onBinaryMessage([{}]) - webSocketListener={}, listener={}",
+ data.length,
+ (listener instanceof WebSocketListener),
+ this.listener.getClass().getName());
+ }
+
if (listener instanceof WebSocketListener)
{
((WebSocketListener)listener).onWebSocketBinary(data, 0, data.length);
@@ -116,6 +141,11 @@ public class JettyListenerEventDriver extends AbstractEventDriver
int statusCode = close.getStatusCode();
String reason = close.getReason();
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onClose({},{}) - listener={}", statusCode, reason, this.listener.getClass().getName());
+ }
listener.onWebSocketClose(statusCode, reason);
}
@@ -123,19 +153,34 @@ public class JettyListenerEventDriver extends AbstractEventDriver
public void onConnect()
{
if (LOG.isDebugEnabled())
- LOG.debug("onConnect({})", session);
+ {
+ LOG.debug("onConnect({}) - listener={}", session, this.listener.getClass().getName());
+ }
listener.onWebSocketConnect(session);
}
@Override
public void onError(Throwable cause)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onError({}) - listener={}", cause.getClass().getName(), this.listener.getClass().getName());
+ }
listener.onWebSocketError(cause);
}
@Override
public void onFrame(Frame frame)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onFrame({}) - frameListener={}, pingPongListener={}, listener={}",
+ frame,
+ (listener instanceof WebSocketFrameListener),
+ (listener instanceof WebSocketPingPongListener),
+ this.listener.getClass().getName());
+ }
+
if (listener instanceof WebSocketFrameListener)
{
((WebSocketFrameListener)listener).onWebSocketFrame(new ReadOnlyDelegatedFrame(frame));
@@ -169,6 +214,17 @@ public class JettyListenerEventDriver extends AbstractEventDriver
@Override
public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onTextFrame({}, {}) - webSocketListener={}, webSocketPartialListener={}, listener={}, activeMessage={}",
+ BufferUtil.toDetailString(buffer),
+ fin,
+ (listener instanceof WebSocketListener),
+ (listener instanceof WebSocketPartialListener),
+ listener.getClass().getName(),
+ activeMessage);
+ }
+
if (listener instanceof WebSocketListener)
{
if (activeMessage == null)
@@ -226,6 +282,15 @@ public class JettyListenerEventDriver extends AbstractEventDriver
@Override
public void onTextMessage(String message)
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onTextMessage([{}] \"{}\") - webSocketListener={}, listener={}",
+ message.length(),
+ TextUtil.maxStringLength(60, message),
+ (listener instanceof WebSocketListener),
+ listener.getClass().getName());
+ }
+
if (listener instanceof WebSocketListener)
{
((WebSocketListener)listener).onWebSocketText(message);
@@ -234,6 +299,16 @@ public class JettyListenerEventDriver extends AbstractEventDriver
public void onContinuationFrame(ByteBuffer buffer, boolean fin) throws IOException
{
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onContinuationFrame({}, {}) - webSocketListener={}, webSocketPartialListener={}, listener={}, activeMessage={}",
+ BufferUtil.toDetailString(buffer), fin,
+ (listener instanceof WebSocketListener),
+ (listener instanceof WebSocketPartialListener),
+ listener.getClass().getName(),
+ activeMessage);
+ }
+
if (listener instanceof WebSocketPartialListener)
{
switch (partialMode)
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java
index 0f10c71e9bd..145ea510e96 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java
@@ -139,6 +139,9 @@ public class CallableMethod
@Override
public String toString()
{
- return String.format("%s[%s]", this.getClass().getSimpleName(), method.toGenericString());
+ return String.format("%s[pojo=%s,method=%s]",
+ this.getClass().getSimpleName(),
+ pojo.getName(),
+ method.toGenericString());
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java
index aca97a2eb01..115a6d12417 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java
@@ -86,6 +86,11 @@ public class OptionalSessionCallableMethod extends CallableMethod
@Override
public String toString()
{
- return String.format("%s[%s]", this.getClass().getSimpleName(), method.toGenericString());
+ return String.format("%s[pojo=%s,method=%s,wantsSession=%b,streaming=%s]",
+ this.getClass().getSimpleName(),
+ pojo.getName(),
+ method.toGenericString(),
+ wantsSession,
+ streaming);
}
}
diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java
index 02b87fdfcb8..b7cd0f84648 100644
--- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java
+++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/CreationTest.java
@@ -28,13 +28,17 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ListenerHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.servlet.Source;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StacklessLogging;
@@ -56,7 +60,62 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class CreationTest
{
+ static ThreadLocal currentRequest = new ThreadLocal<>();
+ @Test
+ public void testRequestGetSessionInsideListener() throws Exception
+ {
+ String contextPath = "";
+ String servletMapping = "/server";
+
+ DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
+ cacheFactory.setEvictionPolicy(SessionCache.EVICT_ON_SESSION_EXIT);
+ SessionDataStoreFactory storeFactory = new TestSessionDataStoreFactory();
+
+ TestServer server1 = new TestServer(0, -1, -1, cacheFactory, storeFactory);
+ TestServlet servlet = new TestServlet();
+ ServletHolder holder = new ServletHolder(servlet);
+ ServletContextHandler contextHandler = server1.addContext(contextPath);
+
+ ListenerHolder h = contextHandler.getServletHandler().newListenerHolder(Source.EMBEDDED);
+ h.setListener(new MySessionListener());
+ contextHandler.getServletHandler().addListener(h);
+
+ TestHttpChannelCompleteListener scopeListener = new TestHttpChannelCompleteListener();
+ server1.getServerConnector().addBean(scopeListener);
+ contextHandler.addServlet(holder, servletMapping);
+ servlet.setStore(contextHandler.getSessionHandler().getSessionCache().getSessionDataStore());
+ server1.start();
+ int port1 = server1.getPort();
+ try (StacklessLogging stackless = new StacklessLogging(Log.getLogger("org.eclipse.jetty.server.session")))
+ {
+ HttpClient client = new HttpClient();
+ client.start();
+
+ //make a session
+ String url = "http://localhost:" + port1 + contextPath + servletMapping + "?action=create&check=false";
+
+ CountDownLatch synchronizer = new CountDownLatch(1);
+ scopeListener.setExitSynchronizer(synchronizer);
+
+ //make a request to set up a session on the server
+ ContentResponse response = client.GET(url);
+ assertEquals(HttpServletResponse.SC_OK, response.getStatus());
+
+ String sessionCookie = response.getHeaders().get("Set-Cookie");
+ assertTrue(sessionCookie != null);
+
+ //ensure request has finished being handled
+ synchronizer.await(5, TimeUnit.SECONDS);
+ }
+ finally
+ {
+ server1.stop();
+ }
+
+ }
+
+
/**
* Test creating a session when the cache is set to
* evict after the request exits.
@@ -387,6 +446,21 @@ public class CreationTest
}
}
+ public static class MySessionListener implements HttpSessionListener
+ {
+ @Override
+ public void sessionCreated(HttpSessionEvent se)
+ {
+ currentRequest.get().getSession(true);
+ }
+
+ @Override
+ public void sessionDestroyed(HttpSessionEvent se)
+ {
+ }
+
+ }
+
public static class TestServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
@@ -436,6 +510,7 @@ public class CreationTest
}
else if (action != null && action.startsWith("create"))
{
+ currentRequest.set(request);
HttpSession session = request.getSession(true);
_id = session.getId();
session.setAttribute("value", new Integer(1));