Issue #207 - more testing updates

This commit is contained in:
Joakim Erdfelt 2017-04-21 09:00:30 -07:00
parent a8d4c68bdc
commit ce9cd8d168
29 changed files with 1310 additions and 473 deletions

View File

@ -116,12 +116,12 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
*
* @param executor
* the executor to use
* @deprecated use {@link #WebSocketClient(HttpClient)} instead
*/
@Deprecated
public WebSocketClient(Executor executor)
{
this(null,executor);
this(new HttpClient());
this.httpClient.setExecutor(executor);
}
/**
@ -129,12 +129,11 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
*
* @param bufferPool
* byte buffer pool to use
* @deprecated use {@link #WebSocketClient(HttpClient)} instead
*/
@Deprecated
public WebSocketClient(ByteBufferPool bufferPool)
{
this(null,null,bufferPool);
this(new HttpClient());
this.httpClient.setByteBufferPool(bufferPool);
}
/**
@ -142,12 +141,10 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
*
* @param sslContextFactory
* ssl context factory to use
* @deprecated use {@link #WebSocketClient(HttpClient)} with its own {@link SslContextFactory}
*/
@Deprecated
public WebSocketClient(SslContextFactory sslContextFactory)
{
this(sslContextFactory,null);
this(new HttpClient(sslContextFactory));
}
/**
@ -157,12 +154,11 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketCont
* ssl context factory to use
* @param executor
* the executor to use
* @deprecated use {@link #WebSocketClient(HttpClient)} instead
*/
@Deprecated
public WebSocketClient(SslContextFactory sslContextFactory, Executor executor)
{
this(sslContextFactory,executor,new MappedByteBufferPool());
this(new HttpClient(sslContextFactory));
this.httpClient.setExecutor(executor);
}
/**

View File

@ -411,7 +411,7 @@ public class WebSocketUpgradeRequest extends HttpRequest implements CompleteList
}
this.localEndpoint = localEndpoint;
this.fut = new CompletableFuture<Session>();
this.fut = new CompletableFuture<>();
}
private final String genRandomKey()

View File

@ -20,6 +20,9 @@ package org.eclipse.jetty.websocket.common;
import java.util.concurrent.atomic.AtomicReference;
/**
* Atomic Close State
*/
public class AtomicClose
{
enum State

View File

@ -20,6 +20,9 @@ package org.eclipse.jetty.websocket.common;
import java.util.concurrent.atomic.AtomicReference;
/**
* Atomic Connection State
*/
public class AtomicConnectionState
{
/**
@ -59,7 +62,7 @@ public class AtomicConnectionState
* Connection should be disconnected and no further reads or writes should occur.
* </p>
*/
CLOSED;
CLOSED
}
private AtomicReference<State> state = new AtomicReference<>();

View File

@ -41,8 +41,14 @@ public interface LogicalConnection extends OutgoingFrames, SuspendToken
/**
* Terminate the connection (no close frame sent)
* @param onlyOutput true to only close the output (half-close), false to close fully.
*/
void disconnect();
void disconnect(boolean onlyOutput);
/**
* Register Read Interest in Connection.
*/
void fillInterested();
/**
* Get the ByteBufferPool in use by the connection

View File

@ -68,7 +68,7 @@ public class Parser
PAYLOAD
}
private static final Logger LOG = Log.getLogger(Parser.class);
private final Logger LOG;
private final WebSocketPolicy policy;
private final ByteBufferPool bufferPool;
private final Parser.Handler parserHandler;
@ -102,6 +102,8 @@ public class Parser
this.bufferPool = bufferPool;
this.policy = wspolicy;
this.parserHandler = parserHandler;
LOG = Log.getLogger(Parser.class.getName() + "_" + wspolicy.getBehavior());
}
private void assertSanePayloadLength(long len)
@ -187,7 +189,7 @@ public class Parser
while (parseFrame(buffer))
{
if (LOG.isDebugEnabled())
LOG.debug("{} Parsed Frame: {}", policy.getBehavior(), frame);
LOG.debug("Parsed Frame: {}", frame);
assertBehavior();
@ -268,7 +270,7 @@ public class Parser
{
if (LOG.isDebugEnabled())
{
LOG.debug("{} Parsing {} bytes",policy.getBehavior(),buffer.remaining());
LOG.debug("Parsing {} bytes",buffer.remaining());
}
while (buffer.hasRemaining())
@ -291,8 +293,7 @@ public class Parser
}
if (LOG.isDebugEnabled())
LOG.debug("{} OpCode {}, fin={} rsv={}{}{}",
policy.getBehavior(),
LOG.debug("OpCode {}, fin={} rsv={}{}{}",
OpCode.name(opcode),
fin,
(((b & 0x40) != 0)?'1':'.'),
@ -583,7 +584,7 @@ public class Parser
if (LOG.isDebugEnabled())
{
LOG.debug("{} Raw Payload: {}",policy.getBehavior(),BufferUtil.toDetailString(window));
LOG.debug("Raw Payload: {}",BufferUtil.toDetailString(window));
}
maskProcessor.process(window);

View File

@ -58,6 +58,7 @@ import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
@ -74,8 +75,32 @@ import org.eclipse.jetty.websocket.common.scopes.WebSocketSessionScope;
public class WebSocketSession extends ContainerLifeCycle implements Session, RemoteEndpointFactory,
WebSocketSessionScope, IncomingFrames, LogicalConnection.Listener, Connection.Listener
{
private static final Logger LOG = Log.getLogger(WebSocketSession.class);
private static final Logger LOG_OPEN = Log.getLogger(WebSocketSession.class.getName() + "_OPEN");
public class OnDisconnectCallback implements WriteCallback
{
private final boolean outputOnly;
public OnDisconnectCallback(boolean outputOnly)
{
this.outputOnly = outputOnly;
}
@Override
public void writeFailed(Throwable x)
{
LOG.debug("writeFailed()", x);
disconnect(outputOnly);
}
@Override
public void writeSuccess()
{
LOG.debug("writeSuccess()");
disconnect(outputOnly);
}
}
private final Logger LOG;
private final WebSocketContainerScope containerScope;
private final WebSocketPolicy policy;
private final URI requestURI;
@ -109,6 +134,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
Objects.requireNonNull(containerScope, "Container Scope cannot be null");
Objects.requireNonNull(requestURI, "Request URI cannot be null");
LOG = Log.getLogger(WebSocketSession.class.getName() + "_" + connection.getPolicy().getBehavior().name());
this.classLoader = Thread.currentThread().getContextClassLoader();
this.containerScope = containerScope;
this.requestURI = requestURI;
@ -147,16 +174,31 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public void close(int statusCode, String reason)
{
if(connectionState.onClosing())
if (connectionState.onClosing())
{
LOG.debug("ConnectionState: Transition to CLOSING");
// This is the first CLOSE event
if(closeState.onLocal())
if (closeState.onLocal())
{
LOG.debug("CloseState: Transition to LOCAL");
// this is Local initiated.
CloseInfo closeInfo = new CloseInfo(statusCode, reason);
Frame closeFrame = closeInfo.asFrame();
outgoingHandler.outgoingFrame(closeFrame, new FrameCallback.Adapter(), BatchMode.AUTO);
outgoingHandler.outgoingFrame(closeFrame, new OnDisconnectCallback(true), BatchMode.AUTO);
}
else
{
LOG.debug("CloseState: Expected LOCAL, but was " + closeState.get());
}
}
else if(connectionState.onClosed())
{
LOG.debug("ConnectionState: Transition to CLOSED");
// This is the reply to the CLOSING entry point
CloseInfo closeInfo = new CloseInfo(statusCode, reason);
Frame closeFrame = closeInfo.asFrame();
outgoingHandler.outgoingFrame(closeFrame, new OnDisconnectCallback(false), BatchMode.AUTO);
}
}
@ -166,24 +208,25 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public void disconnect()
{
disconnect(true);
}
private void disconnect(boolean outputOnly)
{
if(connectionState.onClosing())
{
// Is this is a harsh disconnect: OPEN -> CLOSING -> CLOSED
if (closeState.onAbnormal())
{
// notify local endpoint of harsh disconnect
notifyClose(StatusCode.SHUTDOWN, "Harsh disconnect");
}
}
if(connectionState.onClosed())
{
connection.disconnect();
// TODO: notify local endpoint onClose() ?
// TODO: notifyClose(close.getStatusCode(), close.getReason());
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionClosed()", containerScope.getClass().getSimpleName());
containerScope.onSessionClosed(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
// Transition: CLOSING -> CLOSED
connection.disconnect(outputOnly);
if (closeState.onLocal())
{
// notify local endpoint of harsh disconnect
@ -381,8 +424,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public RemoteEndpoint getRemote()
{
if (LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.getRemote()", getPolicy().getBehavior(), this.getClass().getSimpleName());
if (LOG.isDebugEnabled())
LOG.debug("{}.getRemote()", this.getClass().getSimpleName());
AtomicConnectionState.State state = connectionState.get();
@ -397,7 +440,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public InetSocketAddress getRemoteAddress()
{
return remote.getInetSocketAddress();
return connection.getRemoteAddress();
}
public URI getRequestURI()
@ -460,19 +503,26 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
// process handshake
if(connectionState.onClosing())
{
LOG.debug("ConnectionState: Transition to CLOSING");
// we transitioned to CLOSING state
if(closeState.onRemote())
{
LOG.debug("CloseState: Transition to REMOTE");
// Remote initiated.
// Send reply to remote
close(close.getStatusCode(), close.getReason());
}
else
{
LOG.debug("CloseState: Already at LOCAL");
// Local initiated, this was the reply.
disconnect();
}
}
else
{
LOG.debug("ConnectionState: Not CLOSING: was " + connectionState.get());
}
callback.succeed();
return;
@ -655,6 +705,16 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public void onClosed(Connection connection)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("{}.onSessionClosed()", containerScope.getClass().getSimpleName());
containerScope.onSessionClosed(this);
}
catch (Throwable t)
{
LOG.ignore(t);
}
}
/**
@ -664,8 +724,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
@Override
public void onOpened(Connection connection)
{
if (LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.onOpened()", getPolicy().getBehavior(), this.getClass().getSimpleName());
if (LOG.isDebugEnabled())
LOG.debug("{}.onOpened()", this.getClass().getSimpleName());
connectionState.onConnecting();
open();
}
@ -680,8 +740,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
*/
public void open()
{
if (LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open()", getPolicy().getBehavior(), this.getClass().getSimpleName());
if (LOG.isDebugEnabled())
LOG.debug("{}.open()", this.getClass().getSimpleName());
if (remote != null)
{
@ -696,8 +756,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
{
// Connect remote
remote = remoteEndpointFactory.newRemoteEndpoint(connection, outgoingHandler, getBatchMode());
if (LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.open() remote={}", getPolicy().getBehavior(), this.getClass().getSimpleName(), remote);
if (LOG.isDebugEnabled())
LOG.debug("{}.open() remote={}", this.getClass().getSimpleName(), remote);
// Open WebSocket
endpointFunctions.onOpen(this);
@ -726,6 +786,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Rem
{
openFuture.complete(this);
}
connection.fillInterested();
}
}
else

View File

@ -42,7 +42,6 @@ import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.FrameCallback;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
@ -93,21 +92,18 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
}
}
private static final Logger LOG = Log.getLogger(AbstractWebSocketConnection.class);
private static final Logger LOG_OPEN = Log.getLogger(AbstractWebSocketConnection.class.getName() + ".OPEN");
private static final Logger LOG_CLOSE = Log.getLogger(AbstractWebSocketConnection.class.getName() + ".CLOSE");
/**
* Minimum size of a buffer is the determined to be what would be the maximum framing header size (not including payload)
*/
private static final int MIN_BUFFER_SIZE = Generator.MAX_HEADER_LENGTH;
private final Logger LOG;
private final ByteBufferPool bufferPool;
private final Scheduler scheduler;
private final Generator generator;
private final Parser parser;
private final WebSocketPolicy policy;
private final WebSocketBehavior behavior;
private final AtomicBoolean suspendToken;
private final AtomicBoolean closed = new AtomicBoolean();
private final FrameFlusher flusher;
@ -121,13 +117,15 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
public AbstractWebSocketConnection(EndPoint endp, Executor executor, Scheduler scheduler, WebSocketPolicy policy, ByteBufferPool bufferPool, ExtensionStack extensionStack)
{
super(endp,executor);
LOG = Log.getLogger(AbstractWebSocketConnection.class.getName() + "_" + policy.getBehavior());
this.id = String.format("%s:%d->%s:%d",
endp.getLocalAddress().getAddress().getHostAddress(),
endp.getLocalAddress().getPort(),
endp.getRemoteAddress().getAddress().getHostAddress(),
endp.getRemoteAddress().getPort());
this.policy = policy;
this.behavior = policy.getBehavior();
this.bufferPool = bufferPool;
this.extensionStack = extensionStack;
@ -152,31 +150,32 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
}
@Override
public void disconnect()
public void disconnect(boolean onlyOutput)
{
if (LOG_CLOSE.isDebugEnabled())
LOG_CLOSE.debug("{} disconnect()",behavior);
disconnect(false);
}
if (LOG.isDebugEnabled())
LOG.debug("disconnect({})", onlyOutput ? "OUTPUT_ONLY" : "BOTH");
private void disconnect(boolean onlyOutput)
{
if (LOG_CLOSE.isDebugEnabled())
LOG_CLOSE.debug("{} disconnect({})",behavior,onlyOutput?"outputOnly":"both");
// close FrameFlusher, we cannot write anymore at this point.
flusher.close();
EndPoint endPoint = getEndPoint();
// We need to gently close first, to allow
// SSL close alerts to be sent by Jetty
if (LOG_CLOSE.isDebugEnabled())
LOG_CLOSE.debug("Shutting down output {}",endPoint);
if (LOG.isDebugEnabled())
LOG.debug("Shutting down output {}",endPoint);
endPoint.shutdownOutput();
if (!onlyOutput)
{
if (LOG_CLOSE.isDebugEnabled())
LOG_CLOSE.debug("Closing {}",endPoint);
if (LOG.isDebugEnabled())
LOG.debug("Closing {}",endPoint);
endPoint.close();
}
else
{
closed.set(true);
}
}
protected void execute(Runnable task)
@ -269,9 +268,12 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
public void onClose()
{
if (LOG.isDebugEnabled())
LOG.debug("{} onClose()",behavior);
super.onClose();
LOG.debug("onClose()");
closed.set(true);
flusher.close();
super.onClose();
}
@Override
@ -322,7 +324,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
{
try
{
while (true)
while (isOpen())
{
if (suspendToken.get())
{
@ -411,8 +413,8 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
@Override
public void onOpen()
{
if(LOG_OPEN.isDebugEnabled())
LOG_OPEN.debug("[{}] {}.onOpened()",behavior,this.getClass().getSimpleName());
if(LOG.isDebugEnabled())
LOG.debug("{}.onOpened()",this.getClass().getSimpleName());
super.onOpen();
}

View File

@ -79,12 +79,12 @@ public class FrameFlusher
ByteBuffer payload = entry.frame.getPayload();
if (BufferUtil.hasContent(payload))
{
BufferUtil.append(aggregate,payload);
BufferUtil.put(payload, aggregate);
}
}
if (LOG.isDebugEnabled())
{
LOG.debug("{} aggregated {} frames: {}",FrameFlusher.this,entries.size(),entries);
LOG.debug("{} aggregated {} frames in {}: {}", FrameFlusher.this, entries.size(), aggregate, entries);
}
succeeded();
return Action.SCHEDULED;
@ -113,6 +113,7 @@ public class FrameFlusher
{
if (!BufferUtil.isEmpty(aggregate))
{
aggregate.flip();
buffers.add(aggregate);
if (LOG.isDebugEnabled())
{

View File

@ -35,16 +35,12 @@ public class FutureWriteCallback extends FutureCallback implements WriteCallback
@Override
public void writeFailed(Throwable cause)
{
if (LOG.isDebugEnabled())
LOG.debug(".writeFailed",cause);
failed(cause);
}
@Override
public void writeSuccess()
{
if (LOG.isDebugEnabled())
LOG.debug(".writeSuccess");
succeeded();
}
}

View File

@ -20,10 +20,12 @@
* Jetty WebSocket Common : Implementation [<em>Internal Use Only</em>]
* <p>
* A core set of internal implementation classes for the Jetty WebSocket API.
* </p>
* <p>
* Note: do not reference or use classes present in this package space in your code. <br>
* Restrict your usage to the Jetty WebSocket API classes, the Jetty WebSocket Client API,
* or the Jetty WebSocket Servlet API.
* </p>
*/
package org.eclipse.jetty.websocket.common;

View File

@ -74,10 +74,15 @@ public class LocalWebSocketConnection implements LogicalConnection
}
@Override
public void disconnect()
public void disconnect(boolean outputOnly)
{
if (LOG.isDebugEnabled())
LOG.debug("disconnect()");
LOG.debug("disconnect({})", outputOnly);
}
@Override
public void fillInterested()
{
}
@Override

View File

@ -45,7 +45,12 @@ public class DummyConnection implements LogicalConnection
}
@Override
public void disconnect()
public void disconnect(boolean outputOnly)
{
}
@Override
public void fillInterested()
{
}

View File

@ -0,0 +1,74 @@
Documenting the States of a WebSocket.
NEW:
A new WebSocket session.
It has had no connection attempted yet.
CONNECTING:
The connection is being attempted, along with the Upgrade handshake.
CONNECTED:
The connection is established.
The User WebSocket Endpoint has not been notified yet.
OPEN:
User WebSocket Endpoint has been Opened (the onOpen method has called)
CLOSING:
The close handshake has begun.
Either the local initiated the close, waiting for the remote to reply.
Or the remote initiated the close, and the local hasn't replied yet.
This can be considered a logical half-closed state.
CLOSED:
The connection and session is closed.
This means either the close handshake completed, or the connection was
disconnected for other reasons.
----
Normal Client Initiated Close State Transition
WSEndpoint created
Http GET w/Upgrade initiated
State: CONNECTING
Upgrade Handshake negotiated (HTTP 101 response)
State: CONNECTED
WSEndpoint.onOpen() called
State: OPEN
WSEndpoint.onMessage()
Session.close(local close details)
Connection disconnect output
State: CLOSING
Remote: Received CLOSE Frame
Connection disconnect completely
WSEndpoint.onClose(remote close details)
State: CLOSED
----
Normal Remote Initiated Close State Transition
WSEndpoint created
Http GET w/Upgrade initiated
State: CONNECTING
Upgrade Handshake negotiated (HTTP 101 response)
State: CONNECTED
WSEndpoint.onOpen() called
State: OPEN
WSEndpoint.onMessage()
Remote: Receive CLOSE frame
State: CLOSING
Session.close(remote close details)
Connection disconnect completely
WSEndpoint.onClose(local close details)
State: CLOSED

View File

@ -32,5 +32,5 @@ public interface WebSocketHandshake
* @param response the response
* @throws IOException if unable to handshake
*/
public void doHandshakeResponse(ServletUpgradeRequest request, ServletUpgradeResponse response) throws IOException;
void doHandshakeResponse(ServletUpgradeRequest request, ServletUpgradeResponse response) throws IOException;
}

View File

@ -22,6 +22,7 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -244,6 +245,16 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
this.sessionFactories.add(sessionFactory);
}
public void setSessionFactories(SessionFactory... factories)
{
if (factories == null || factories.length < 1)
{
throw new IllegalStateException("Must declare SessionFactory implementations");
}
this.sessionFactories.clear();
this.sessionFactories.addAll(Arrays.asList(factories));
}
private WebSocketSession createSession(URI requestURI, Object websocket, LogicalConnection connection)
{
if (websocket == null)
@ -565,6 +576,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
// Setup Session
WebSocketSession session = createSession(request.getRequestURI(), websocket, wsConnection);
session.setUpgradeRequest(request);
// set true negotiated extension list back to response
response.setExtensions(extensionStack.getNegotiatedExtensions());
session.setUpgradeResponse(response);
@ -589,9 +601,6 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
// Tell jetty about the new upgraded connection
request.setServletAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, wsConnection);
if (LOG.isDebugEnabled())
LOG.debug("Handshake Response: {}", handshaker);
if (getSendServerVersion(connector))
response.setHeader("Server", HttpConfiguration.SERVER_VERSION);
@ -599,7 +608,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
handshaker.doHandshakeResponse(request, response);
if (LOG.isDebugEnabled())
LOG.debug("Websocket upgrade {} {} {} {}", request.getRequestURI(), version, response.getAcceptedSubProtocol(), wsConnection);
LOG.debug("Websocket upgrade {} v={} subprotocol={} connection={}", request.getRequestURI(), version, response.getAcceptedSubProtocol(), wsConnection);
return true;
}

View File

@ -0,0 +1,46 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.FrameCallback;
public class BlockerFrameCallback implements FrameCallback
{
private CompletableFuture<Void> future = new CompletableFuture<>();
@Override
public void fail(Throwable cause)
{
future.completeExceptionally(cause);
}
@Override
public void succeed()
{
future.complete(null);
}
public void block() throws Exception
{
future.get(1, TimeUnit.MINUTES);
}
}

View File

@ -32,7 +32,6 @@ import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@ -42,7 +41,6 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.FrameCallback;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.common.CloseInfo;
@ -55,28 +53,6 @@ import org.eclipse.jetty.websocket.common.WebSocketFrame;
*/
public class Fuzzer extends ContainerLifeCycle
{
public static class BlockerCallback implements FrameCallback
{
private CompletableFuture<Void> future = new CompletableFuture<>();
@Override
public void fail(Throwable cause)
{
future.completeExceptionally(cause);
}
@Override
public void succeed()
{
future.complete(null);
}
public void block() throws Exception
{
future.get(1, TimeUnit.MINUTES);
}
}
public static class Session implements AutoCloseable
{
// Client side framing mask
@ -249,7 +225,7 @@ public class Fuzzer extends ContainerLifeCycle
{
for (WebSocketFrame f : send)
{
BlockerCallback blocker = new BlockerCallback();
BlockerFrameCallback blocker = new BlockerFrameCallback();
session.getOutgoingHandler().outgoingFrame(f, blocker, BatchMode.OFF);
blocker.block();
}

View File

@ -0,0 +1,217 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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 static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
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.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
public class TrackingEndpoint implements WebSocketListener, WebSocketFrameListener
{
private final Logger LOG;
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicReference<CloseInfo> closeInfo = new AtomicReference<>();
public AtomicReference<Throwable> error = new AtomicReference<>();
private CompletableFuture<List<String>> expectedMessagesFuture = new CompletableFuture<>();
private AtomicReference<Integer> expectedMessageCount = new AtomicReference<>();
private List<String> messages = new ArrayList<>();
private CompletableFuture<List<WebSocketFrame>> expectedFramesFuture = new CompletableFuture<>();
private AtomicReference<Integer> expectedFramesCount = new AtomicReference<>();
private List<WebSocketFrame> frames = new ArrayList<>();
private WebSocketSession session;
public TrackingEndpoint(String id)
{
LOG = Log.getLogger(this.getClass().getName() + "_" + id);
}
public void assertClose(String prefix, int expectedCloseStatusCode, Matcher<String> reasonMatcher) throws InterruptedException
{
assertThat(prefix + " endpoint close event received", closeLatch.await(10, TimeUnit.SECONDS), Matchers.is(true));
CloseInfo close = closeInfo.get();
assertThat(prefix + " close info", close, Matchers.notNullValue());
assertThat(prefix + " received close code", close.getStatusCode(), Matchers.is(expectedCloseStatusCode));
assertThat(prefix + " received close reason", close.getReason(), reasonMatcher);
}
public void close(int statusCode, String reason)
{
this.session.close(statusCode, reason);
}
public Future<List<WebSocketFrame>> expectedFrames(int expectedCount)
{
synchronized (expectedFramesCount)
{
if (!expectedFramesCount.compareAndSet(null, expectedCount))
{
throw new IllegalStateException("Can only have 1 registered frame count future");
}
else
{
checkFrameCount();
}
}
return expectedFramesFuture;
}
public Future<List<String>> expectedMessages(int expectedCount)
{
synchronized (expectedMessagesFuture)
{
if (!expectedMessageCount.compareAndSet(null, expectedCount))
{
throw new IllegalStateException("Can only have 1 registered message count future");
}
else
{
checkMessageCount();
}
}
return expectedMessagesFuture;
}
public RemoteEndpoint getRemote()
{
return session.getRemote();
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
if (LOG.isDebugEnabled())
{
LOG.info("onWSBinary({})", BufferUtil.toDetailString(ByteBuffer.wrap(payload, offset, len)));
}
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
this.closeLatch.countDown();
CloseInfo close = new CloseInfo(statusCode, reason);
assertThat("Close only happened once", closeInfo.compareAndSet(null, close), is(true));
}
@Override
public void onWebSocketConnect(Session session)
{
assertThat("Session type", session, instanceOf(WebSocketSession.class));
this.session = (WebSocketSession) session;
if (LOG.isDebugEnabled())
{
LOG.debug("onWebSocketConnect()");
}
this.openLatch.countDown();
}
@Override
public void onWebSocketError(Throwable cause)
{
assertThat("Error must have value", cause, notNullValue());
if (error.compareAndSet(null, cause) == false)
{
LOG.warn("Original Cause", error.get());
LOG.warn("Extra/Excess Cause", cause);
fail("onError should only happen once!");
}
this.expectedMessagesFuture.completeExceptionally(cause);
this.expectedFramesFuture.completeExceptionally(cause);
}
@Override
public void onWebSocketFrame(Frame frame)
{
if (LOG.isDebugEnabled())
{
LOG.debug("onWSFrame({})", frame);
}
synchronized (expectedFramesFuture)
{
frames.add(WebSocketFrame.copy(frame));
checkFrameCount();
}
}
@Override
public void onWebSocketText(String text)
{
if (LOG.isDebugEnabled())
{
LOG.debug("onWSText(\"{}\")", text);
}
synchronized (expectedMessagesFuture)
{
messages.add(text);
checkMessageCount();
}
}
private void checkMessageCount()
{
Integer expected = expectedMessageCount.get();
if (expected != null && messages.size() >= expected.intValue())
{
expectedMessagesFuture.complete(messages);
}
}
private void checkFrameCount()
{
Integer expected = expectedFramesCount.get();
if (expected != null && frames.size() >= expected.intValue())
{
expectedFramesFuture.complete(frames);
}
}
}

View File

@ -30,6 +30,7 @@ import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
@ -51,7 +52,7 @@ public class UntrustedWSClient extends WebSocketClient
public Future<UntrustedWSSession> connect(URI toUri, ClientUpgradeRequest req) throws IOException
{
final Future<Session> connectFut = super.connect(new UntrustedWSEndpoint(), toUri, req);
final Future<Session> connectFut = super.connect(new UntrustedWSEndpoint(WebSocketBehavior.CLIENT.name()), toUri, req);
return new CompletableFuture<UntrustedWSSession>() {
@Override
public UntrustedWSSession get() throws InterruptedException, ExecutionException

View File

@ -27,6 +27,8 @@ import org.eclipse.jetty.util.SharedBlockingCallback;
import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
/**
@ -53,6 +55,17 @@ public class UntrustedWSConnection
internalConnection.getEndPoint().flush();
}
/**
* Forward a frame to the {@Link OutgoingFrames} handler
* @param frame
*/
public void write(Frame frame) throws Exception
{
BlockerFrameCallback blocker = new BlockerFrameCallback();
this.internalConnection.outgoingFrame(frame, blocker, BatchMode.OFF);
blocker.block();
}
/**
* Write arbitrary bytes out the active connection.
*

View File

@ -18,136 +18,121 @@
package org.eclipse.jetty.websocket.tests;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
public class UntrustedWSEndpoint implements WebSocketListener, WebSocketFrameListener
public class UntrustedWSEndpoint extends TrackingEndpoint implements WebSocketListener, WebSocketFrameListener
{
private static final Logger LOG = Log.getLogger(UntrustedWSEndpoint.class);
@SuppressWarnings("unused")
private Session session;
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicReference<CloseInfo> closeInfo = new AtomicReference<>();
public AtomicReference<Throwable> error = new AtomicReference<>();
private UntrustedWSSession untrustedSession;
private CompletableFuture<UntrustedWSSession> connectFuture;
private CompletableFuture<List<String>> expectedMessagesFuture = new CompletableFuture<>();
private AtomicReference<Integer> expectedMessageCount = new AtomicReference<>();
private List<String> messages = new ArrayList<>();
private BiFunction<UntrustedWSSession, String, String> onTextFunction;
private BiFunction<UntrustedWSSession, ByteBuffer, ByteBuffer> onBinaryFunction;
private CompletableFuture<List<WebSocketFrame>> expectedFramesFuture = new CompletableFuture<>();
private AtomicReference<Integer> expectedFramesCount = new AtomicReference<>();
private List<WebSocketFrame> frames = new ArrayList<>();
public Future<List<WebSocketFrame>> expectedFrames(int expectedCount)
public CompletableFuture<UntrustedWSSession> getConnectFuture()
{
if (!expectedFramesCount.compareAndSet(null, expectedCount))
{
throw new IllegalStateException("Can only have 1 registered frame count future");
}
return expectedFramesFuture;
return connectFuture;
}
public Future<List<String>> expectedMessages(int expectedCount)
public UntrustedWSEndpoint(String id)
{
if (!expectedMessageCount.compareAndSet(null, expectedCount))
{
throw new IllegalStateException("Can only have 1 registered message count future");
}
return expectedMessagesFuture;
super(id);
}
@Override
public void onWebSocketConnect(Session session)
{
this.session = session;
this.openLatch.countDown();
assertThat("Session type", session, instanceOf(UntrustedWSSession.class));
this.untrustedSession = (UntrustedWSSession) session;
if (this.connectFuture != null)
{
this.connectFuture.complete(this.untrustedSession);
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
this.closeLatch.countDown();
CloseInfo close = new CloseInfo(statusCode, reason);
assertThat("Close only happened once", closeInfo.compareAndSet(null, close), is(true));
super.onWebSocketConnect(session);
}
@Override
public void onWebSocketError(Throwable cause)
{
assertThat("Error must have value", cause, notNullValue());
if (error.compareAndSet(null, cause) == false)
if (this.connectFuture != null)
{
LOG.warn("Original Cause", error.get());
LOG.warn("Extra/Excess Cause", cause);
fail("onError should only happen once!");
// Always trip this, doesn't matter if if completed normally first.
this.connectFuture.completeExceptionally(cause);
}
synchronized (expectedMessagesFuture)
{
if (expectedMessagesFuture != null)
expectedMessagesFuture.completeExceptionally(cause);
}
synchronized (expectedFramesFuture)
{
if (expectedFramesFuture != null)
expectedFramesFuture.completeExceptionally(cause);
}
super.onWebSocketError(cause);
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
// TODO
super.onWebSocketBinary(payload, offset, len);
if (onBinaryFunction != null)
{
try
{
ByteBuffer msg = ByteBuffer.wrap(payload, offset, len);
ByteBuffer responseBuffer = onBinaryFunction.apply(this.untrustedSession, msg);
if (responseBuffer != null)
{
this.getRemote().sendBytes(responseBuffer);
}
}
catch (Throwable t)
{
LOG.warn("Unable to send binary", t);
}
}
}
@Override
public void onWebSocketText(String text)
{
messages.add(text);
synchronized (expectedMessagesFuture)
{
Integer expected = expectedMessageCount.get();
super.onWebSocketText(text);
if (expected != null && messages.size() >= expected.intValue())
if (onTextFunction != null)
{
expectedMessagesFuture.complete(messages);
try
{
String responseText = onTextFunction.apply(this.untrustedSession, text);
if (responseText != null)
{
this.getRemote().sendString(responseText);
}
}
catch (Throwable t)
{
LOG.warn("Unable to send text", t);
}
}
}
@Override
public void onWebSocketFrame(Frame frame)
public void setConnectFuture(CompletableFuture<UntrustedWSSession> future)
{
frames.add(WebSocketFrame.copy(frame));
synchronized (expectedFramesFuture)
{
Integer expected = expectedFramesCount.get();
this.connectFuture = future;
}
if (expected != null && frames.size() >= expected.intValue())
public void setOnBinaryFunction(BiFunction<UntrustedWSSession, ByteBuffer, ByteBuffer> onBinaryFunction)
{
expectedFramesFuture.complete(frames);
}
this.onBinaryFunction = onBinaryFunction;
}
public void setOnTextFunction(BiFunction<UntrustedWSSession, String, String> onTextFunction)
{
this.onTextFunction = onTextFunction;
}
}

View File

@ -0,0 +1,145 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.util.WSURI;
public class UntrustedWSServer extends ContainerLifeCycle implements UntrustedWSSessionFactory.Listener
{
private static final Logger LOG = Log.getLogger(SimpleServletServer.class);
private Server server;
private ServerConnector connector;
private URI wsUri;
private boolean ssl = false;
private SslContextFactory sslContextFactory;
private Map<URI, CompletableFuture<UntrustedWSSession>> connectionFutures = new ConcurrentHashMap<>();
@Override
protected void doStart() throws Exception
{
// Configure Server
server = new Server();
if (ssl)
{
// HTTP Configuration
HttpConfiguration http_config = new HttpConfiguration();
http_config.setSecureScheme("https");
http_config.setSecurePort(0);
http_config.setOutputBufferSize(32768);
http_config.setRequestHeaderSize(8192);
http_config.setResponseHeaderSize(8192);
http_config.setSendServerVersion(true);
http_config.setSendDateHeader(false);
sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath(MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath());
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setKeyManagerPassword("keypwd");
sslContextFactory.setExcludeCipherSuites("SSL_RSA_WITH_DES_CBC_SHA","SSL_DHE_RSA_WITH_DES_CBC_SHA","SSL_DHE_DSS_WITH_DES_CBC_SHA",
"SSL_RSA_EXPORT_WITH_RC4_40_MD5","SSL_RSA_EXPORT_WITH_DES40_CBC_SHA","SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA");
// SSL HTTP Configuration
HttpConfiguration https_config = new HttpConfiguration(http_config);
https_config.addCustomizer(new SecureRequestCustomizer());
// SSL Connector
connector = new ServerConnector(server,new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),new HttpConnectionFactory(https_config));
connector.setPort(0);
}
else
{
// Basic HTTP connector
connector = new ServerConnector(server);
connector.setPort(0);
}
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);
// Serve untrusted endpoint
context.addServlet(UntrustedWSServlet.class, "/untrusted/*").setInitOrder(1);
// Start Server
addBean(server);
super.doStart();
// Wireup Context related things
UntrustedWSSessionFactory sessionFactory = (UntrustedWSSessionFactory) context.getServletContext().getAttribute(UntrustedWSSessionFactory.class.getName());
sessionFactory.addListener(this);
// Establish the Server URI
URI serverUri = server.getURI();
wsUri = WSURI.toWebsocket(serverUri).resolve("/");
// Some debugging
if (LOG.isDebugEnabled())
{
LOG.debug("WebSocket Server URI: " + wsUri.toASCIIString());
LOG.debug(server.dump());
}
super.doStart();
}
public URI getWsUri()
{
return wsUri;
}
@Override
public void onSessionCreate(UntrustedWSSession session, URI requestURI)
{
// A new session was created (but not connected, yet)
CompletableFuture<UntrustedWSSession> sessionFuture = this.connectionFutures.get(requestURI);
if(sessionFuture != null)
{
session.getUntrustedEndpoint().setConnectFuture(sessionFuture);
}
this.connectionFutures.put(requestURI, session.getUntrustedEndpoint().getConnectFuture());
}
public void registerConnectFuture(URI uri, CompletableFuture<UntrustedWSSession> sessionFuture)
{
this.connectionFutures.put(uri, sessionFuture);
}
}

View File

@ -0,0 +1,46 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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 org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
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.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
public class UntrustedWSServlet extends WebSocketServlet implements WebSocketCreator
{
@Override
public void configure(WebSocketServletFactory factory)
{
WebSocketServerFactory serverFactory = (WebSocketServerFactory) factory;
serverFactory.setCreator(this);
UntrustedWSSessionFactory sessionFactory = new UntrustedWSSessionFactory(serverFactory);
this.getServletContext().setAttribute(UntrustedWSSessionFactory.class.getName(), sessionFactory);
serverFactory.setSessionFactories(sessionFactory);
}
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
return new UntrustedWSEndpoint(WebSocketBehavior.SERVER.name());
}
}

View File

@ -19,7 +19,11 @@
package org.eclipse.jetty.websocket.tests;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.WebSocketConnectionListener;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.common.LogicalConnection;
@ -29,13 +33,31 @@ import org.eclipse.jetty.websocket.common.scopes.WebSocketContainerScope;
public class UntrustedWSSessionFactory implements SessionFactory
{
interface Listener
{
void onSessionCreate(UntrustedWSSession session, URI requestURI);
}
private final static Logger LOG = Log.getLogger(UntrustedWSSessionFactory.class);
private final WebSocketContainerScope containerScope;
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
public UntrustedWSSessionFactory(WebSocketContainerScope containerScope)
{
this.containerScope = containerScope;
}
public boolean addListener(Listener listener)
{
return this.listeners.add(listener);
}
public boolean removeListener(Listener listener)
{
return this.listeners.remove(listener);
}
@Override
public boolean supports(Object websocket)
{
@ -45,6 +67,17 @@ public class UntrustedWSSessionFactory implements SessionFactory
@Override
public WebSocketSession createSession(URI requestURI, Object websocket, LogicalConnection connection)
{
return new UntrustedWSSession(containerScope, requestURI, websocket, connection);
final UntrustedWSSession session = new UntrustedWSSession(containerScope, requestURI, websocket, connection);
listeners.forEach((listener) -> {
try
{
listener.onSessionCreate(session, requestURI);
}
catch (Throwable t)
{
LOG.warn("Unable to notify listener " + listener, t);
}
});
return session;
}
}

View File

@ -16,24 +16,26 @@
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
package org.eclipse.jetty.websocket.tests.client;
import static org.hamcrest.CoreMatchers.containsString;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.TestTracker;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.common.test.IBlockheadServerConnection;
import org.eclipse.jetty.websocket.common.test.LeakTrackingBufferPoolRule;
import org.eclipse.jetty.websocket.common.test.XBlockheadServer;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.tests.LeakTrackingBufferPoolRule;
import org.eclipse.jetty.websocket.tests.UntrustedWSServer;
import org.eclipse.jetty.websocket.tests.UntrustedWSSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
/**
* Tests for conditions due to bad networking.
@ -41,12 +43,12 @@ import org.junit.Test;
public class BadNetworkTest
{
@Rule
public TestTracker tt = new TestTracker();
public TestName testname = new TestName();
@Rule
public LeakTrackingBufferPoolRule bufferPool = new LeakTrackingBufferPoolRule("Test");
private XBlockheadServer server;
private UntrustedWSServer server;
private WebSocketClient client;
@Before
@ -60,7 +62,7 @@ public class BadNetworkTest
@Before
public void startServer() throws Exception
{
server = new XBlockheadServer();
server = new UntrustedWSServer();
server.start();
}
@ -79,24 +81,22 @@ public class BadNetworkTest
@Test
public void testAbruptClientClose() throws Exception
{
JettyTrackingSocket wsocket = new JettyTrackingSocket();
TrackingSocket wsocket = new TrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
IBlockheadServerConnection ssocket = server.accept();
ssocket.upgrade();
Future<Session> future = client.connect(wsocket, wsUri);
// Validate that we are connected
future.get(30,TimeUnit.SECONDS);
wsocket.waitForConnected(30,TimeUnit.SECONDS);
future.get(30, TimeUnit.SECONDS);
wsocket.waitForConnected(30, TimeUnit.SECONDS);
// Have client disconnect abruptly
Session session = wsocket.getSession();
session.disconnect();
// Client Socket should see close
wsocket.waitForClose(10,TimeUnit.SECONDS);
wsocket.waitForClose(10, TimeUnit.SECONDS);
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection
@ -107,23 +107,29 @@ public class BadNetworkTest
@Test
public void testAbruptServerClose() throws Exception
{
JettyTrackingSocket wsocket = new JettyTrackingSocket();
TrackingSocket wsocket = new TrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
IBlockheadServerConnection ssocket = server.accept();
ssocket.upgrade();
CompletableFuture<UntrustedWSSession> sessionFuture = new CompletableFuture<UntrustedWSSession>()
{
@Override
public boolean complete(UntrustedWSSession session)
{
// server disconnect
session.disconnect();
return super.complete(session);
}
};
server.registerConnectFuture(wsURI, sessionFuture);
Future<Session> future = client.connect(wsocket, wsURI);
// Validate that we are connected
future.get(30,TimeUnit.SECONDS);
wsocket.waitForConnected(30,TimeUnit.SECONDS);
// Have server disconnect abruptly
ssocket.disconnect();
future.get(30, TimeUnit.SECONDS);
wsocket.waitForConnected(30, TimeUnit.SECONDS);
// Wait for close (as response to idle timeout)
wsocket.waitForClose(10,TimeUnit.SECONDS);
wsocket.waitForClose(10, TimeUnit.SECONDS);
// Client Socket should see a close event, with status NO_CLOSE
// This event is automatically supplied by the underlying WebSocketClientConnection

View File

@ -16,7 +16,7 @@
// ========================================================================
//
package org.eclipse.jetty.websocket.client;
package org.eclipse.jetty.websocket.tests.client;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
@ -29,15 +29,17 @@ import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@ -57,25 +59,27 @@ import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.websocket.api.ProtocolException;
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.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.Parser;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.frames.TextFrame;
import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
import org.eclipse.jetty.websocket.common.test.IBlockheadServerConnection;
import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.RawFrameBuilder;
import org.eclipse.jetty.websocket.common.test.XBlockheadServer;
import org.eclipse.jetty.websocket.tests.RawFrameBuilder;
import org.eclipse.jetty.websocket.tests.UntrustedWSConnection;
import org.eclipse.jetty.websocket.tests.UntrustedWSEndpoint;
import org.eclipse.jetty.websocket.tests.UntrustedWSServer;
import org.eclipse.jetty.websocket.tests.UntrustedWSSession;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
public class ClientCloseTest
{
@ -97,8 +101,8 @@ public class ClientCloseTest
public void assertNoCloseEvent()
{
assertThat("Client Close Event",closeLatch.getCount(),is(1L));
assertThat("Client Close Event Status Code ",closeCode,is(-1));
assertThat("Client Close Event", closeLatch.getCount(), is(1L));
assertThat("Client Close Event Status Code ", closeCode, is(-1));
}
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher, Matcher<String> reasonMatcher)
@ -106,16 +110,16 @@ public class ClientCloseTest
{
long maxTimeout = clientTimeoutMs * 4;
assertThat("Client Close Event Occurred",closeLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true));
assertThat("Client Close Event Count",closeCount.get(),is(1));
assertThat("Client Close Event Status Code",closeCode,statusCodeMatcher);
assertThat("Client Close Event Occurred", closeLatch.await(maxTimeout, TimeUnit.MILLISECONDS), is(true));
assertThat("Client Close Event Count", closeCount.get(), is(1));
assertThat("Client Close Event Status Code", closeCode, statusCodeMatcher);
if (reasonMatcher == null)
{
assertThat("Client Close Event Reason",closeReason,nullValue());
assertThat("Client Close Event Reason", closeReason, nullValue());
}
else
{
assertThat("Client Close Event Reason",closeReason,reasonMatcher);
assertThat("Client Close Event Reason", closeReason, reasonMatcher);
}
}
@ -123,7 +127,7 @@ public class ClientCloseTest
{
long maxTimeout = clientTimeoutMs * 4;
assertThat("Client Error Event Occurred",errorLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true));
assertThat("Client Error Event Occurred", errorLatch.await(maxTimeout, TimeUnit.MILLISECONDS), is(true));
assertThat("Client Error Type", error.get(), instanceOf(expectedCause));
assertThat("Client Error Message", error.get().getMessage(), messageMatcher);
}
@ -136,8 +140,8 @@ public class ClientCloseTest
@Override
public void onWebSocketClose(int statusCode, String reason)
{
LOG.debug("onWebSocketClose({},{})",statusCode,reason);
super.onWebSocketClose(statusCode,reason);
LOG.debug("onWebSocketClose({},{})", statusCode, reason);
super.onWebSocketClose(statusCode, reason);
closeCount.incrementAndGet();
closeCode = statusCode;
closeReason = reason;
@ -147,7 +151,7 @@ public class ClientCloseTest
@Override
public void onWebSocketConnect(Session session)
{
LOG.debug("onWebSocketConnect({})",session);
LOG.debug("onWebSocketConnect({})", session);
super.onWebSocketConnect(session);
openLatch.countDown();
}
@ -155,7 +159,7 @@ public class ClientCloseTest
@Override
public void onWebSocketError(Throwable cause)
{
LOG.warn("onWebSocketError",cause);
LOG.warn("onWebSocketError", cause);
assertThat("Unique Error Event", error.compareAndSet(null, cause), is(true));
errorLatch.countDown();
}
@ -163,41 +167,47 @@ public class ClientCloseTest
@Override
public void onWebSocketText(String message)
{
LOG.debug("onWebSocketText({})",message);
LOG.debug("onWebSocketText({})", message);
messageQueue.offer(message);
}
public EndPoint getEndPoint() throws Exception
{
Session session = getSession();
assertThat("Session type",session,instanceOf(WebSocketSession.class));
assertThat("Session type", session, instanceOf(WebSocketSession.class));
WebSocketSession wssession = (WebSocketSession)session;
WebSocketSession wssession = (WebSocketSession) session;
Field fld = wssession.getClass().getDeclaredField("connection");
fld.setAccessible(true);
assertThat("Field: connection",fld,notNullValue());
assertThat("Field: connection", fld, notNullValue());
Object val = fld.get(wssession);
assertThat("Connection type",val,instanceOf(AbstractWebSocketConnection.class));
assertThat("Connection type", val, instanceOf(AbstractWebSocketConnection.class));
@SuppressWarnings("resource")
AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection)val;
AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection) val;
return wsconn.getEndPoint();
}
}
@Rule
public TestName testname = new TestName();
@Rule
public TestTracker tt = new TestTracker();
private XBlockheadServer server;
private UntrustedWSServer server;
private WebSocketClient client;
private void confirmConnection(CloseTrackingSocket clientSocket, Future<Session> clientFuture, IBlockheadServerConnection serverConns) throws Exception
private void confirmConnection(CloseTrackingSocket clientSocket, Future<Session> clientFuture, UntrustedWSSession serverSession) throws Exception
{
// Wait for client connect on via future
clientFuture.get(30,TimeUnit.SECONDS);
clientFuture.get(30, TimeUnit.SECONDS);
// Wait for client connect via client websocket
assertThat("Client WebSocket is Open",clientSocket.openLatch.await(30,TimeUnit.SECONDS),is(true));
assertThat("Client WebSocket is Open", clientSocket.openLatch.await(30, TimeUnit.SECONDS), is(true));
UntrustedWSEndpoint serverEndpoint = serverSession.getUntrustedEndpoint();
Future<List<WebSocketFrame>> futFrames = serverEndpoint.expectedFrames(1);
try
{
@ -206,27 +216,26 @@ public class ClientCloseTest
Future<Void> testFut = clientSocket.getRemote().sendStringByFuture(echoMsg);
// Wait for send future
testFut.get(30,TimeUnit.SECONDS);
testFut.get(30, TimeUnit.SECONDS);
// Read Frame on server side
IncomingFramesCapture serverCapture = serverConns.readFrames(1,30,TimeUnit.SECONDS);
serverCapture.assertFrameCount(1);
WebSocketFrame frame = serverCapture.getFrames().poll();
assertThat("Server received frame",frame.getOpCode(),is(OpCode.TEXT));
assertThat("Server received frame payload",frame.getPayloadAsUTF8(),is(echoMsg));
List<WebSocketFrame> frames = futFrames.get(30, TimeUnit.SECONDS);
WebSocketFrame frame = frames.get(0);
assertThat("Server received frame", frame.getOpCode(), is(OpCode.TEXT));
assertThat("Server received frame payload", frame.getPayloadAsUTF8(), is(echoMsg));
// Server send echo reply
serverConns.write(new TextFrame().setPayload(echoMsg));
serverEndpoint.getRemote().sendString(echoMsg);
// Wait for received echo
clientSocket.messageQueue.awaitEventCount(1,1,TimeUnit.SECONDS);
clientSocket.messageQueue.awaitEventCount(1, 1, TimeUnit.SECONDS);
// Verify received message
String recvMsg = clientSocket.messageQueue.poll();
assertThat("Received message",recvMsg,is(echoMsg));
assertThat("Received message", recvMsg, is(echoMsg));
// Verify that there are no errors
assertThat("Error events",clientSocket.error.get(),nullValue());
assertThat("Error events", clientSocket.error.get(), nullValue());
}
finally
{
@ -234,36 +243,17 @@ public class ClientCloseTest
}
}
private void confirmServerReceivedCloseFrame(IBlockheadServerConnection serverConn, int expectedCloseCode, Matcher<String> closeReasonMatcher) throws IOException,
TimeoutException
{
IncomingFramesCapture serverCapture = serverConn.readFrames(1,30,TimeUnit.SECONDS);
serverCapture.assertFrameCount(1);
serverCapture.assertHasFrame(OpCode.CLOSE,1);
WebSocketFrame frame = serverCapture.getFrames().poll();
assertThat("Server received close frame",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo closeInfo = new CloseInfo(frame);
assertThat("Server received close code",closeInfo.getStatusCode(),is(expectedCloseCode));
if (closeReasonMatcher == null)
{
assertThat("Server received close reason",closeInfo.getReason(),nullValue());
}
else
{
assertThat("Server received close reason",closeInfo.getReason(),closeReasonMatcher);
}
}
public static class TestClientTransportOverHTTP extends HttpClientTransportOverHTTP
{
@Override
protected SelectorManager newSelectorManager(HttpClient client)
{
return new ClientSelectorManager(client, 1){
return new ClientSelectorManager(client, 1)
{
@Override
protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key)
{
TestEndPoint endPoint = new TestEndPoint(channel,selector,key,getScheduler());
TestEndPoint endPoint = new TestEndPoint(channel, selector, key, getScheduler());
endPoint.setIdleTimeout(client.getIdleTimeout());
return endPoint;
}
@ -277,7 +267,7 @@ public class ClientCloseTest
public TestEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
{
super((SocketChannel)channel,selector,key,scheduler);
super((SocketChannel) channel, selector, key, scheduler);
}
@Override
@ -301,7 +291,7 @@ public class ClientCloseTest
@Before
public void startServer() throws Exception
{
server = new XBlockheadServer();
server = new UntrustedWSServer();
server.start();
}
@ -324,46 +314,49 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
// client sends close frame (code 1000, normal)
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL,origCloseReason);
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn,StatusCode.NORMAL,is(origCloseReason));
serverSession.getUntrustedEndpoint().assertClose("Server", StatusCode.NORMAL, is(origCloseReason));
// server sends 2 messages
serverConn.write(new TextFrame().setPayload("Hello"));
serverConn.write(new TextFrame().setPayload("World"));
RemoteEndpoint remote = serverSession.getRemote();
remote.sendString("Hello");
remote.sendString("World");
// server sends close frame (code 1000, no reason)
CloseInfo sclose = new CloseInfo(StatusCode.NORMAL,"From Server");
serverConn.write(sclose.asFrame());
serverSession.close(StatusCode.NORMAL, "From Server");
// client receives 2 messages
clientSocket.messageQueue.awaitEventCount(2,1,TimeUnit.SECONDS);
clientSocket.messageQueue.awaitEventCount(2, 1, TimeUnit.SECONDS);
// Verify received messages
String recvMsg = clientSocket.messageQueue.poll();
assertThat("Received message 1",recvMsg,is("Hello"));
assertThat("Received message 1", recvMsg, is("Hello"));
recvMsg = clientSocket.messageQueue.poll();
assertThat("Received message 2",recvMsg,is("World"));
assertThat("Received message 2", recvMsg, is("World"));
// Verify that there are no errors
assertThat("Error events",clientSocket.error.get(),nullValue());
assertThat("Error events", clientSocket.error.get(), nullValue());
// client close event on ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.NORMAL),containsString("From Server"));
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.NORMAL), containsString("From Server"));
}
@Test
@ -373,24 +366,27 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
// client sends BIG frames (until it cannot write anymore)
// server must not read (for test purpose, in order to congest connection)
// when write is congested, client enqueue close frame
// client initiate write, but write never completes
EndPoint endp = clientSocket.getEndPoint();
assertThat("EndPoint is testable",endp,instanceOf(TestEndPoint.class));
TestEndPoint testendp = (TestEndPoint)endp;
assertThat("EndPoint is testable", endp, instanceOf(TestEndPoint.class));
TestEndPoint testendp = (TestEndPoint) endp;
char msg[] = new char[10240];
int writeCount = 0;
@ -399,13 +395,13 @@ public class ClientCloseTest
while (!testendp.congestedFlush.get())
{
int z = i - ((i / 26) * 26);
char c = (char)('a' + z);
Arrays.fill(msg,c);
char c = (char) ('a' + z);
Arrays.fill(msg, c);
clientSocket.getRemote().sendStringByFuture(String.valueOf(msg));
writeCount++;
writeSize += msg.length;
}
LOG.info("Wrote {} frames totalling {} bytes of payload before congestion kicked in",writeCount,writeSize);
LOG.info("Wrote {} frames totalling {} bytes of payload before congestion kicked in", writeCount, writeSize);
// Verify timeout error
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
@ -419,32 +415,35 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
// server sends bad close frame (too big of a reason message)
byte msg[] = new byte[400];
Arrays.fill(msg,(byte)'x');
Arrays.fill(msg, (byte) 'x');
ByteBuffer bad = ByteBuffer.allocate(500);
RawFrameBuilder.putOpFin(bad,OpCode.CLOSE,true);
RawFrameBuilder.putLength(bad,msg.length + 2,false);
bad.putShort((short)StatusCode.NORMAL);
RawFrameBuilder.putOpFin(bad, OpCode.CLOSE, true);
RawFrameBuilder.putLength(bad, msg.length + 2, false);
bad.putShort((short) StatusCode.NORMAL);
bad.put(msg);
BufferUtil.flipToFlush(bad,0);
BufferUtil.flipToFlush(bad, 0);
try (StacklessLogging ignored = new StacklessLogging(Parser.class))
{
serverConn.write(bad);
serverSession.getUntrustedConnection().writeRaw(bad);
// client should have noticed the error
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
@ -452,14 +451,14 @@ public class ClientCloseTest
assertThat("OnError", clientSocket.error.get().getMessage(), containsString("Invalid control frame"));
// client parse invalid frame, notifies server of close (protocol error)
confirmServerReceivedCloseFrame(serverConn,StatusCode.PROTOCOL,allOf(containsString("Invalid control frame"),containsString("length")));
serverSession.getUntrustedEndpoint().assertClose("Server", StatusCode.PROTOCOL, allOf(containsString("Invalid control frame"), containsString("length")));
}
// server disconnects
serverConn.disconnect();
serverSession.disconnect();
// client triggers close event on client ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.PROTOCOL),allOf(containsString("Invalid control frame"),containsString("length")));
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.PROTOCOL), allOf(containsString("Invalid control frame"), containsString("length")));
}
@Test
@ -469,31 +468,34 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
try(StacklessLogging ignored = new StacklessLogging(CloseTrackingSocket.class))
try (StacklessLogging ignored = new StacklessLogging(CloseTrackingSocket.class))
{
// client sends close frame
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL,origCloseReason);
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn,StatusCode.NORMAL,is(origCloseReason));
serverSession.getUntrustedEndpoint().assertClose("Server", StatusCode.NORMAL, is(origCloseReason));
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
// server shuts down connection (no frame reply)
serverConn.disconnect();
serverSession.disconnect();
// client reads -1 (EOF)
clientSocket.assertReceivedErrorEvent(timeout, IOException.class, containsString("EOF"));
@ -509,23 +511,27 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
UntrustedWSConnection serverConn = serverSession.getUntrustedConnection();
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
// client sends close frame
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL,origCloseReason);
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
// server receives close frame
confirmServerReceivedCloseFrame(serverConn,StatusCode.NORMAL,is(origCloseReason));
serverSession.getUntrustedEndpoint().assertClose("Server", StatusCode.NORMAL, is(origCloseReason));
// client should not have received close message (yet)
clientSocket.assertNoCloseEvent();
@ -548,21 +554,24 @@ public class ClientCloseTest
int clientCount = 3;
CloseTrackingSocket clientSockets[] = new CloseTrackingSocket[clientCount];
IBlockheadServerConnection serverConns[] = new IBlockheadServerConnection[clientCount];
UntrustedWSSession serverSessions[] = new UntrustedWSSession[clientCount];
// Connect Multiple Clients
for (int i = 0; i < clientCount; i++)
{
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName() + "/" + i);
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<>();
server.registerConnectFuture(wsURI, serverSessionFut);
// Client Request Upgrade
clientSockets[i] = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSockets[i],server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSockets[i], wsURI);
// Server accepts connection
serverConns[i] = server.accept();
serverConns[i].upgrade();
serverSessions[i] = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSockets[i],clientConnectFuture,serverConns[i]);
confirmConnection(clientSockets[i], clientConnectFuture, serverSessions[i]);
}
// client lifecycle stop
@ -572,13 +581,13 @@ public class ClientCloseTest
for (int i = 0; i < clientCount; i++)
{
// server receives close frame
confirmServerReceivedCloseFrame(serverConns[i],StatusCode.SHUTDOWN,containsString("Shutdown"));
serverSessions[i].getUntrustedEndpoint().assertClose("Server", StatusCode.SHUTDOWN, containsString("Shutdown"));
}
// clients disconnect
for (int i = 0; i < clientCount; i++)
{
clientSockets[i].assertReceivedCloseEvent(timeout,is(StatusCode.SHUTDOWN),containsString("Shutdown"));
clientSockets[i].assertReceivedCloseEvent(timeout, is(StatusCode.SHUTDOWN), containsString("Shutdown"));
}
}
@ -589,16 +598,28 @@ public class ClientCloseTest
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<UntrustedWSSession>()
{
@Override
public boolean complete(UntrustedWSSession session)
{
// echo back text as-well
session.getUntrustedEndpoint().setOnTextFunction((serverSession, text) -> text);
return super.complete(session);
}
};
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
CloseTrackingSocket clientSocket = new CloseTrackingSocket();
Future<Session> clientConnectFuture = client.connect(clientSocket,server.getWsUri());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
IBlockheadServerConnection serverConn = server.accept();
serverConn.upgrade();
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
confirmConnection(clientSocket,clientConnectFuture,serverConn);
confirmConnection(clientSocket, clientConnectFuture, serverSession);
// setup client endpoint for write failure (test only)
EndPoint endp = clientSocket.getEndPoint();
@ -607,7 +628,7 @@ public class ClientCloseTest
// client enqueue close frame
// client write failure
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL,origCloseReason);
clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason);
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class));
@ -615,6 +636,6 @@ public class ClientCloseTest
// client triggers close event on client ws-endpoint
// assert - close code==1006 (abnormal)
// assert - close reason message contains (write failure)
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.ABNORMAL),containsString("EOF"));
clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("EOF"));
}
}

View File

@ -0,0 +1,128 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.client;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.tests.TrackingEndpoint;
import org.eclipse.jetty.websocket.tests.UntrustedWSServer;
import org.eclipse.jetty.websocket.tests.UntrustedWSSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
public class EchoTest
{
@Rule
public TestName testname = new TestName();
private UntrustedWSServer server;
private WebSocketClient client;
@Before
public void startClient() throws Exception
{
client = new WebSocketClient();
client.start();
}
@Before
public void startServer() throws Exception
{
server = new UntrustedWSServer();
server.start();
}
@After
public void stopClient() throws Exception
{
client.stop();
}
@After
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testBasicEcho() throws IOException, InterruptedException, ExecutionException, TimeoutException
{
// Set client timeout
final int timeout = 1000;
client.setMaxIdleTimeout(timeout);
URI wsURI = server.getWsUri().resolve("/untrusted/" + testname.getMethodName());
CompletableFuture<UntrustedWSSession> serverSessionFut = new CompletableFuture<UntrustedWSSession>()
{
@Override
public boolean complete(UntrustedWSSession session)
{
// echo back text as-well
session.getUntrustedEndpoint().setOnTextFunction((serverSession, text) -> text);
return super.complete(session);
}
};
server.registerConnectFuture(wsURI, serverSessionFut);
// Client connects
TrackingEndpoint clientSocket = new TrackingEndpoint(WebSocketBehavior.CLIENT.name());
Future<Session> clientConnectFuture = client.connect(clientSocket, wsURI);
// Server accepts connect
UntrustedWSSession serverSession = serverSessionFut.get(10, TimeUnit.SECONDS);
// client confirms connection via echo
assertThat("Client Opened", clientSocket.openLatch.await(5, TimeUnit.SECONDS), is(true));
Future<List<String>> futMessages = clientSocket.expectedMessages(1);
// client sends message
clientSocket.getRemote().sendString("Hello Echo");
List<String> messages = futMessages.get(10, TimeUnit.SECONDS);
assertThat("Messages[0]", messages.get(0), is("Hello Echo"));
// client closes
clientSocket.close(StatusCode.NORMAL, "Normal Close");
// client triggers close event on client ws-endpoint
clientSocket.assertClose("Client", StatusCode.NORMAL, containsString("Normal Close"));
// Server close event
serverSession.getUntrustedEndpoint().assertClose("Server", StatusCode.NORMAL, containsString("Normal Close"));
}
}

View File

@ -0,0 +1,55 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.client;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.hamcrest.Matcher;
@WebSocket
public class TrackingSocket
{
private Session session;
public void assertClose(int expectedStatusCode, Matcher<String> reasonMatcher)
{
}
public Session getSession()
{
return session;
}
@OnWebSocketConnect
public void onOpen(Session session)
{
this.session = session;
}
public void waitForClose(int timeout, TimeUnit unit)
{
}
public void waitForConnected(int timeout, TimeUnit unit)
{
}
}