Author: markt Date: Tue Mar 26 21:17:18 2013 New Revision: 1461317 URL: http://svn.apache.org/r1461317 Log: First cut of an SSL client implementation for WebSocket and a simple test case to demonstrate that it works.
Modified: tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java tomcat/trunk/test/org/apache/tomcat/util/net/TesterSupport.java tomcat/trunk/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java Modified: tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java Tue Mar 26 21:17:18 2013 @@ -21,6 +21,8 @@ import java.nio.channels.CompletionHandl import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLException; + /** * This is a wrapper for a {@link java.nio.channels.AsynchronousSocketChannel} * that limits the methods available thereby simplifying the process of @@ -40,4 +42,6 @@ public interface AsyncChannelWrapper { CompletionHandler<Long,? super A> handler); void close(); + + Future<Void> handshake() throws SSLException; } Modified: tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java Tue Mar 26 21:17:18 2013 @@ -20,8 +20,10 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Generally, just passes calls straight to the wrapped @@ -30,6 +32,8 @@ import java.util.concurrent.TimeUnit; */ public class AsyncChannelWrapperNonSecure implements AsyncChannelWrapper { + private static final Future<Void> NOOP_FUTURE = new NoOpFuture(); + private final AsynchronousSocketChannel socketChannel; public AsyncChannelWrapperNonSecure( @@ -69,4 +73,40 @@ public class AsyncChannelWrapperNonSecur // Ignore } } + + @Override + public Future<Void> handshake() { + return NOOP_FUTURE; + } + + + private static final class NoOpFuture implements Future<Void> { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + return null; + } + } } Modified: tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java Tue Mar 26 21:17:18 2013 @@ -16,53 +16,505 @@ */ package org.apache.tomcat.websocket; +import java.io.EOFException; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; /** - * Wraps the {@link AsynchronousSocketChannel} with SSL/TLS. + * Wraps the {@link AsynchronousSocketChannel} with SSL/TLS. This needs a lot + * more testing before it can be considered robust. */ public class AsyncChannelWrapperSecure implements AsyncChannelWrapper { + private static final ByteBuffer DUMMY = ByteBuffer.allocate(8192); private final AsynchronousSocketChannel socketChannel; + private final SSLEngine sslEngine; + private final ByteBuffer socketReadBuffer; + private final ByteBuffer socketWriteBuffer; + // One thread for read, one for write + private final ExecutorService executor = Executors.newFixedThreadPool(2); + private AtomicBoolean writing = new AtomicBoolean(false); + private AtomicBoolean reading = new AtomicBoolean(false); - public AsyncChannelWrapperSecure(AsynchronousSocketChannel socketChannel) { + public AsyncChannelWrapperSecure(AsynchronousSocketChannel socketChannel, + SSLEngine sslEngine) { this.socketChannel = socketChannel; + this.sslEngine = sslEngine; + + int socketBufferSize = sslEngine.getSession().getPacketBufferSize(); + socketReadBuffer = ByteBuffer.allocateDirect(socketBufferSize); + socketWriteBuffer = ByteBuffer.allocateDirect(socketBufferSize); } @Override public Future<Integer> read(ByteBuffer dst) { - // TODO - throw new UnsupportedOperationException(); + WrapperFuture<Integer,Void> future = new WrapperFuture<>(); + + if (!reading.compareAndSet(false, true)) { + // TODO + throw new IllegalStateException(); + } + + ReadTask readTask = new ReadTask(dst, future); + + executor.execute(readTask); + + return future; } @Override public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer,? super A> handler) { - // TODO - throw new UnsupportedOperationException(); + + WrapperFuture<Integer,? super A> future = + new WrapperFuture<>(handler, attachment); + + if (!reading.compareAndSet(false, true)) { + // TODO + throw new IllegalStateException(); + } + + ReadTask readTask = new ReadTask(dst, future); + + executor.execute(readTask); } @Override public Future<Integer> write(ByteBuffer src) { - // TODO - throw new UnsupportedOperationException(); + + WrapperFuture<Long,Void> inner = new WrapperFuture<>(); + + if (!writing.compareAndSet(false, true)) { + // TODO + throw new IllegalStateException(); + } + + WriteTask writeTask = + new WriteTask(new ByteBuffer[] {src}, 0, 1, inner); + + executor.execute(writeTask); + + Future<Integer> future = new LongToIntegerFuture(inner); + return future; } @Override public <A> void write(ByteBuffer[] srcs, int offset, int length, long timeout, TimeUnit unit, A attachment, CompletionHandler<Long,? super A> handler) { - // TODO - throw new UnsupportedOperationException(); + + WrapperFuture<Long,? super A> future = + new WrapperFuture<>(handler, attachment); + + if (!writing.compareAndSet(false, true)) { + // TODO + throw new IllegalStateException(); + } + + WriteTask writeTask = new WriteTask(srcs, offset, length, future); + + executor.execute(writeTask); } @Override public void close() { - // TODO - throw new UnsupportedOperationException(); + try { + socketChannel.close(); + } catch (IOException e) { + // TODO + } + } + + @Override + public Future<Void> handshake() throws SSLException { + + WrapperFuture<Void,Void> wFuture = new WrapperFuture<>(); + + Thread t = new WebSocketSslHandshakeThread(wFuture); + t.start(); + + return wFuture; + } + + + private class WriteTask implements Runnable { + + private final ByteBuffer[] srcs; + private final int offset; + private final int length; + private final WrapperFuture<Long,?> future; + + public WriteTask(ByteBuffer[] srcs, int offset, int length, + WrapperFuture<Long,?> future) { + this.srcs = srcs; + this.future = future; + this.offset = offset; + this.length = length; + } + + @Override + public void run() { + long written = 0; + + try { + for (int i = offset; i < offset + length; i++) { + ByteBuffer src = srcs[i]; + while (src.hasRemaining()) { + socketWriteBuffer.clear(); + + // Encrypt the data + SSLEngineResult r = sslEngine.wrap(src, socketWriteBuffer); + written += r.bytesConsumed(); + Status s = r.getStatus(); + + if (s == Status.OK || s == Status.BUFFER_OVERFLOW) { + // Need to write out the bytes and may need to read from + // the source again to empty it + } else { + // Status.BUFFER_UNDERFLOW - only happens on unwrap + // Status.CLOSED - unexpected + // TODO + throw new IllegalStateException(); + } + + // Check for tasks + if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable = sslEngine.getDelegatedTask(); + while (runnable != null) { + runnable.run(); + runnable = sslEngine.getDelegatedTask(); + } + } + + socketWriteBuffer.flip(); + + // Do the write + Future<Integer> f = socketChannel.write(socketWriteBuffer); + Integer socketWrite = f.get(); + if (socketWrite.intValue() != r.bytesProduced()) { + throw new IOException(); + } + } + } + + + if (writing.compareAndSet(true, false)) { + future.complete(Long.valueOf(written)); + } else { + // TODO + future.fail(new IllegalStateException()); + } + } catch (Exception e) { + future.fail(e); + } + } + } + + + private class ReadTask implements Runnable { + + private final ByteBuffer dest; + private final WrapperFuture<Integer,?> future; + + public ReadTask(ByteBuffer dest, WrapperFuture<Integer,?> future) { + this.dest = dest; + this.future = future; + } + + @Override + public void run() { + int read = 0; + + boolean forceRead = false; + + try { + while (read == 0) { + socketReadBuffer.compact(); + + if (forceRead) { + Future<Integer> f = + socketChannel.read(socketReadBuffer); + Integer socketRead = f.get(); + if (socketRead.intValue() == -1) { + // TODO + throw new EOFException(); + } + } + + socketReadBuffer.flip(); + + if (socketReadBuffer.hasRemaining()) { + // Decrypt the data in the buffer + SSLEngineResult r = + sslEngine.unwrap(socketReadBuffer, dest); + read += r.bytesProduced(); + Status s = r.getStatus(); + + if (s == Status.OK || s == Status.BUFFER_OVERFLOW) { + // Bytes available for reading and there may be + // sufficientNeed data in the socketReadBuffer to + // support further reads without reading from the + // socket + } else if (s == Status.BUFFER_UNDERFLOW) { + // There is partial data in the socketReadBuffer + if (read == 0) { + // Need more data before the partial data can be + // processed and some output generated + forceRead = true; + } + // else return the data we have and deal with the + // partial data on the next read + } else { + // Status.CLOSED - unexpected + // TODO + throw new IllegalStateException(); + } + + // Check for tasks + if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable = sslEngine.getDelegatedTask(); + while (runnable != null) { + runnable.run(); + runnable = sslEngine.getDelegatedTask(); + } + } + } else { + forceRead = true; + } + } + + + if (reading.compareAndSet(true, false)) { + future.complete(Integer.valueOf(read)); + } else { + // TODO + future.fail(new IllegalStateException()); + } + } catch (Exception e) { + future.fail(e); + } + } + } + + + private class WebSocketSslHandshakeThread extends Thread { + + private final WrapperFuture<Void,Void> hFuture; + + public WebSocketSslHandshakeThread(WrapperFuture<Void,Void> hFuture) { + this.hFuture = hFuture; + } + + @Override + public void run() { + try { + sslEngine.beginHandshake(); + // So the first compact does the right thing + socketReadBuffer.position(socketReadBuffer.limit()); + + HandshakeStatus handshakeStatus = + sslEngine.getHandshakeStatus(); + boolean handshaking = true; + + while(handshaking) { + switch (handshakeStatus) { + case NEED_WRAP: { + socketWriteBuffer.clear(); + SSLEngineResult r = + sslEngine.wrap(DUMMY, socketWriteBuffer); + handshakeStatus = checkResult(r, true); + socketWriteBuffer.flip(); + Future<Integer> fWrite = + socketChannel.write(socketWriteBuffer); + fWrite.get(); + break; + } + case NEED_UNWRAP: { + socketReadBuffer.compact(); + if (socketReadBuffer.position() == 0) { + Future<Integer> fRead = + socketChannel.read(socketReadBuffer); + fRead.get(); + } + socketReadBuffer.flip(); + SSLEngineResult r = + sslEngine.unwrap(socketReadBuffer, DUMMY); + handshakeStatus = checkResult(r, false); + break; + } + case NEED_TASK: { + Runnable r = null; + while ((r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + handshakeStatus = sslEngine.getHandshakeStatus(); + break; + } + case FINISHED: { + handshaking = false; + break; + } + default: { + throw new SSLException("TODO"); + } + } + } + } catch (SSLException | InterruptedException | + ExecutionException e) { + hFuture.fail(e); + } + + hFuture.complete(null); + } + + private HandshakeStatus checkResult(SSLEngineResult result, + boolean wrap) throws SSLException { + + if (result.getStatus() != Status.OK) { + throw new SSLException("TODO"); + } + if (wrap && result.bytesConsumed() != 0) { + throw new SSLException("TODO"); + } + if (!wrap && result.bytesProduced() != 0) { + throw new SSLException("TODO"); + } + return result.getHandshakeStatus(); + } + } + + + private static class WrapperFuture<T,A> implements Future<T> { + + private final CompletionHandler<T,A> handler; + private final A attachment; + + private volatile T result = null; + private volatile Throwable throwable = null; + private CountDownLatch completionLatch = new CountDownLatch(1); + + public WrapperFuture() { + this(null, null); + } + + public WrapperFuture(CompletionHandler<T,A> handler, A attachment) { + this.handler = handler; + this.attachment = attachment; + } + + public void complete(T result) { + this.result = result; + completionLatch.countDown(); + if (handler != null) { + handler.completed(result, attachment); + } + } + + public void fail(Throwable t) { + throwable = t; + completionLatch.countDown(); + if (handler != null) { + handler.failed(throwable, attachment); + } + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + // Could support cancellation by closing the connection + return false; + } + + @Override + public final boolean isCancelled() { + // Could support cancellation by closing the connection + return false; + } + + @Override + public final boolean isDone() { + return completionLatch.getCount() > 0; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + completionLatch.await(); + if (throwable != null) { + throw new ExecutionException(throwable); + } + return result; + } + + @Override + public T get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + completionLatch.await(timeout, unit); + if (throwable != null) { + throw new ExecutionException(throwable); + } + return result; + } + } + + private static final class LongToIntegerFuture implements Future<Integer> { + + private final Future<Long> wrapped; + + public LongToIntegerFuture(Future<Long> wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return wrapped.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return wrapped.isCancelled(); + } + + @Override + public boolean isDone() { + return wrapped.isDone(); + } + + @Override + public Integer get() throws InterruptedException, ExecutionException { + Long result = wrapped.get(); + if (result.longValue() > Integer.MAX_VALUE) { + // TODO + throw new ExecutionException("Result too big", null); + } + return new Integer(result.intValue()); + } + + @Override + public Integer get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + Long result = wrapped.get(timeout, unit); + if (result.longValue() > Integer.MAX_VALUE) { + // TODO + throw new ExecutionException("Result too big", null); + } + return new Integer(result.intValue()); + } } } Modified: tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java Tue Mar 26 21:17:18 2013 @@ -16,13 +16,17 @@ */ package org.apache.tomcat.websocket; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.charset.Charset; +import java.security.KeyStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,6 +42,10 @@ import java.util.concurrent.ConcurrentHa import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; import javax.websocket.ClientEndpoint; import javax.websocket.ClientEndpointConfig; import javax.websocket.ClientEndpointConfig.Builder; @@ -57,6 +65,19 @@ import org.apache.tomcat.websocket.pojo. public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess { + /** + * Property name to set to configure the value that is passed to + * {@link SSLEngine#setEnabledProtocols(String[])}. The value should be a + * comma separated string. + */ + public static final String SSL_PROTOCOLS_PROPERTY = + "org.apache.tomcat.websocket.SSL_PROTOCOLS"; + public static final String SSL_TRUSTSTORE_PROPERTY = + "org.apache.tomcat.websocket.SSL_TRUSTSTORE"; + public static final String SSL_TRUSTSTORE_PWD_PROPERTY = + "org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD"; + public static final String SSL_TRUSTSTORE_PWD_DEFAULT = "changeit"; + private static final StringManager sm = StringManager.getManager(Constants.PACKAGE_NAME); private static final Random random = new Random(); @@ -202,6 +223,9 @@ public class WsWebSocketContainer sm.getString("wsWebSocketContainer.invalidScheme")); } } else { + if ("wss".equalsIgnoreCase(scheme)) { + secure = true; + } sa = new InetSocketAddress(host, port); } @@ -216,16 +240,21 @@ public class WsWebSocketContainer AsyncChannelWrapper channel; if (secure) { - channel = new AsyncChannelWrapperSecure(socketChannel); + SSLEngine sslEngine = createSSLEngine( + clientEndpointConfiguration.getUserProperties()); + channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine); } else { channel = new AsyncChannelWrapperNonSecure(socketChannel); } - ByteBuffer response; String subProtocol; try { fConnect.get(); + + Future<Void> fHandshake = channel.handshake(); + fHandshake.get(); + int toWrite = request.limit(); Future<Integer> fWrite = channel.write(request); @@ -256,7 +285,7 @@ public class WsWebSocketContainer throw new DeploymentException( sm.getString("Sec-WebSocket-Protocol")); } - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException | InterruptedException | SSLException e) { throw new DeploymentException( sm.getString("wsWebSocketContainer.httpRequestFailed"), e); } @@ -524,6 +553,55 @@ public class WsWebSocketContainer } + private SSLEngine createSSLEngine(Map<String,Object> userProperties) + throws DeploymentException { + + try { + // Create the SSL Context + SSLContext sslContext = SSLContext.getInstance("TLS"); + + // Trust store + String sslTrustStoreValue = + (String) userProperties.get(SSL_TRUSTSTORE_PROPERTY); + if (sslTrustStoreValue != null) { + String sslTrustStorePwdValue = (String) userProperties.get( + SSL_TRUSTSTORE_PWD_PROPERTY); + if (sslTrustStorePwdValue == null) { + sslTrustStorePwdValue = SSL_TRUSTSTORE_PWD_DEFAULT; + } + + File keyStoreFile = new File(sslTrustStoreValue); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(keyStoreFile)) { + ks.load(is, sslTrustStorePwdValue.toCharArray()); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + sslContext.init(null, tmf.getTrustManagers(), null); + } else { + sslContext.init(null, null, null); + } + + SSLEngine engine = sslContext.createSSLEngine(); + + String sslProtocolsValue = + (String) userProperties.get(SSL_PROTOCOLS_PROPERTY); + if (sslProtocolsValue != null) { + engine.setEnabledProtocols(sslProtocolsValue.split(",")); + } + + engine.setUseClientMode(true); + + return engine; + } catch (Exception e) { + throw new DeploymentException("TODO", e); + } + } + + @Override public long getDefaultMaxSessionIdleTimeout() { return defaultMaxSessionIdleTimeout; Modified: tomcat/trunk/test/org/apache/tomcat/util/net/TesterSupport.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/net/TesterSupport.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/net/TesterSupport.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/net/TesterSupport.java Tue Mar 26 21:17:18 2013 @@ -77,7 +77,7 @@ public final class TesterSupport { RFC_5746_SUPPORTED = result; } - protected static void initSsl(Tomcat tomcat) { + public static void initSsl(Tomcat tomcat) { initSsl(tomcat, "localhost.jks", null, null); } Modified: tomcat/trunk/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java?rev=1461317&r1=1461316&r2=1461317&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java (original) +++ tomcat/trunk/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java Tue Mar 26 21:17:18 2013 @@ -46,6 +46,7 @@ import org.apache.catalina.Context; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.coyote.http11.Http11Protocol; +import org.apache.tomcat.util.net.TesterSupport; import org.apache.tomcat.websocket.TesterSingleMessageClient.BasicBinary; import org.apache.tomcat.websocket.TesterSingleMessageClient.BasicHandler; import org.apache.tomcat.websocket.TesterSingleMessageClient.BasicText; @@ -710,4 +711,43 @@ public class TestWsWebSocketContainer ex } } + + @Test + public void testConnectToServerEndpointSSL() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("", System.getProperty("java.io.tmpdir")); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + + TesterSupport.initSsl(tomcat); + + tomcat.start(); + + WebSocketContainer wsContainer = + ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = + ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put( + WsWebSocketContainer.SSL_TRUSTSTORE_PROPERTY, + "test/org/apache/tomcat/util/net/ca.jks"); + Session wsSession = wsContainer.connectToServer( + TesterProgrammaticEndpoint.class, + clientEndpointConfig, + new URI("wss://localhost:" + getPort() + + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText(MESSAGE_STRING_1); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + List<String> messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(MESSAGE_STRING_1, messages.get(0)); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org