/*******************************************************************************
* Copyright (c) 2011 Intalio, Inc.
* ======================================================================
* 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;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutionException;
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.AtomicLong;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
public class WebSocketClientTest
{
private WebSocketClientFactory _factory = new WebSocketClientFactory();
private ServerSocket _server;
private int _serverPort;
@Before
public void startServer() throws Exception
{
_server = new ServerSocket();
_server.bind(null);
_serverPort = _server.getLocalPort();
_factory.start();
}
@After
public void stopServer() throws Exception
{
if(_server != null) {
_server.close();
}
_factory.stop();
}
@Test
public void testMessageBiggerThanBufferSize() throws Exception
{
QueuedThreadPool threadPool = new QueuedThreadPool();
int bufferSize = 512;
WebSocketClientFactory factory = new WebSocketClientFactory(threadPool, new ZeroMaskGen(), bufferSize);
threadPool.start();
factory.start();
WebSocketClient client = new WebSocketClient(factory);
final CountDownLatch openLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
WebSocket.OnTextMessage websocket = new WebSocket.OnTextMessage()
{
public void onOpen(Connection connection)
{
openLatch.countDown();
}
public void onMessage(String data)
{
// System.out.println("data = " + data);
dataLatch.countDown();
}
public void onClose(int closeCode, String message)
{
}
};
client.open(new URI("ws://127.0.0.1:" + _serverPort + "/"), websocket);
Socket socket = _server.accept();
accept(socket);
Assert.assertTrue(openLatch.await(1, TimeUnit.SECONDS));
OutputStream serverOutput = socket.getOutputStream();
int length = bufferSize + bufferSize / 2;
serverOutput.write(0x80 | 0x01); // FIN + TEXT
serverOutput.write(0x7E); // No MASK and 2 bytes length
serverOutput.write(length >> 8); // first length byte
serverOutput.write(length & 0xFF); // second length byte
for (int i = 0; i < length; ++i)
serverOutput.write('x');
serverOutput.flush();
Assert.assertTrue(dataLatch.await(1000, TimeUnit.SECONDS));
factory.stop();
threadPool.stop();
}
@Test
public void testBadURL() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
boolean bad=false;
final AtomicBoolean open = new AtomicBoolean();
try
{
client.open(new URI("http://localhost:8080"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{}
});
Assert.fail();
}
catch(IllegalArgumentException e)
{
bad=true;
}
Assert.assertTrue(bad);
Assert.assertFalse(open.get());
}
@Test
public void testAsyncConnectionRefused() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:1"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
}
});
Throwable error=null;
try
{
future.get(1,TimeUnit.SECONDS);
Assert.fail();
}
catch(ExecutionException e)
{
error=e.getCause();
}
Assert.assertFalse(open.get());
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,close.get());
Assert.assertTrue(error instanceof ConnectException);
}
@Test
public void testConnectionNotAccepted() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
}
});
Throwable error=null;
try
{
future.get(250,TimeUnit.MILLISECONDS);
Assert.fail();
}
catch(TimeoutException e)
{
error=e;
}
Assert.assertFalse(open.get());
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,close.get());
Assert.assertTrue(error instanceof TimeoutException);
}
@Test
public void testConnectionTimeout() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
}
});
Assert.assertNotNull(_server.accept());
Throwable error=null;
try
{
future.get(250,TimeUnit.MILLISECONDS);
Assert.fail();
}
catch(TimeoutException e)
{
error=e;
}
Assert.assertFalse(open.get());
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,close.get());
Assert.assertTrue(error instanceof TimeoutException);
}
@Test
public void testBadHandshake() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
}
});
Socket connection = _server.accept();
respondToClient(connection, "HTTP/1.1 404 NOT FOUND\r\n\r\n");
Throwable error=null;
try
{
future.get(250,TimeUnit.MILLISECONDS);
Assert.fail();
}
catch(ExecutionException e)
{
error=e.getCause();
}
Assert.assertFalse(open.get());
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,close.get());
Assert.assertTrue(error instanceof IOException);
Assert.assertTrue(error.getMessage().indexOf("404 NOT FOUND")>0);
}
@Test
public void testBadUpgrade() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
}
});
Socket connection = _server.accept();
respondToClient(connection,
"HTTP/1.1 101 Upgrade\r\n" +
"Sec-WebSocket-Accept: rubbish\r\n" +
"\r\n" );
Throwable error=null;
try
{
future.get(250,TimeUnit.MILLISECONDS);
Assert.fail();
}
catch(ExecutionException e)
{
error=e.getCause();
}
Assert.assertFalse(open.get());
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,close.get());
Assert.assertTrue(error instanceof IOException);
Assert.assertTrue(error.getMessage().indexOf("Bad Sec-WebSocket-Accept")>=0);
}
@Test
public void testUpgradeThenTCPClose() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
final CountDownLatch _latch = new CountDownLatch(1);
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
_latch.countDown();
}
});
Socket socket = _server.accept();
accept(socket);
WebSocket.Connection connection = future.get(250,TimeUnit.MILLISECONDS);
Assert.assertNotNull(connection);
Assert.assertTrue(open.get());
Assert.assertEquals(0,close.get());
socket.close();
_latch.await(10,TimeUnit.SECONDS);
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,close.get());
}
@Test
public void testIdle() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
client.setMaxIdleTime(500);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
final CountDownLatch _latch = new CountDownLatch(1);
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
_latch.countDown();
}
});
Socket socket = _server.accept();
accept(socket);
WebSocket.Connection connection = future.get(250,TimeUnit.MILLISECONDS);
Assert.assertNotNull(connection);
Assert.assertTrue(open.get());
Assert.assertEquals(0,close.get());
long start=System.currentTimeMillis();
_latch.await(10,TimeUnit.SECONDS);
Assert.assertTrue(System.currentTimeMillis()-start<5000);
Assert.assertEquals(WebSocketConnectionRFC6455.CLOSE_NORMAL,close.get());
}
@Test
public void testNotIdle() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
client.setMaxIdleTime(500);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
final CountDownLatch _latch = new CountDownLatch(1);
final BlockingQueue<String> queue = new BlockingArrayQueue<String>();
final StringBuilder closeMessage = new StringBuilder();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket.OnTextMessage()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
closeMessage.append(message);
_latch.countDown();
}
public void onMessage(String data)
{
queue.add(data);
}
});
Socket socket = _server.accept();
accept(socket);
WebSocket.Connection connection = future.get(250,TimeUnit.MILLISECONDS);
Assert.assertNotNull(connection);
Assert.assertTrue(open.get());
Assert.assertEquals(0,close.get());
// Send some messages client to server
byte[] recv = new byte[1024];
int len=-1;
for (int i=0;i<10;i++)
{
Thread.sleep(250);
connection.sendMessage("Hello");
len=socket.getInputStream().read(recv,0,recv.length);
Assert.assertTrue(len>0);
}
// Send some messages server to client
byte[] send = new byte[] { (byte)0x81, (byte) 0x02, (byte)'H', (byte)'i'};
for (int i=0;i<10;i++)
{
Thread.sleep(250);
socket.getOutputStream().write(send,0,send.length);
socket.getOutputStream().flush();
Assert.assertEquals("Hi",queue.poll(1,TimeUnit.SECONDS));
}
// Close with code
long start=System.currentTimeMillis();
socket.getOutputStream().write(new byte[]{(byte)0x88, (byte) 0x02, (byte)4, (byte)87 },0,4);
socket.getOutputStream().flush();
_latch.await(10,TimeUnit.SECONDS);
Assert.assertTrue(System.currentTimeMillis()-start<5000);
Assert.assertEquals(1002,close.get());
Assert.assertEquals("Invalid close code 1111", closeMessage.toString());
}
@Test
public void testBlockSending() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
client.setMaxIdleTime(10000);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
final CountDownLatch _latch = new CountDownLatch(1);
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket.OnTextMessage()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
_latch.countDown();
}
public void onMessage(String data)
{
}
});
final Socket socket = _server.accept();
accept(socket);
WebSocket.Connection connection = future.get(250,TimeUnit.MILLISECONDS);
Assert.assertNotNull(connection);
Assert.assertTrue(open.get());
Assert.assertEquals(0,close.get());
final int messages=200000;
final AtomicLong totalB=new AtomicLong();
Thread consumer = new Thread()
{
@Override
public void run()
{
// Thread.sleep is for artificially poor performance reader needed for this testcase.
try
{
Thread.sleep(200);
byte[] recv = new byte[32*1024];
int len=0;
while (len>=0)
{
totalB.addAndGet(len);
len=socket.getInputStream().read(recv,0,recv.length);
Thread.sleep(10);
}
}
catch(InterruptedException e)
{
return;
}
catch(Exception e)
{
e.printStackTrace();
}
}
};
consumer.start();
// Send lots of messages client to server
long start=System.currentTimeMillis();
String mesg="This is a test message to send";
for (int i=0;i<messages;i++)
{
connection.sendMessage(mesg);
}
// Duration for the write phase
long writeDur = (System.currentTimeMillis() - start);
// wait for consumer to complete
while (totalB.get()<messages*(mesg.length()+6L))
{
Thread.sleep(10);
}
Assert.assertThat("write duration", writeDur, greaterThan(1000L)); // writing was blocked
Assert.assertEquals(messages*(mesg.length()+6L),totalB.get());
consumer.interrupt();
}
@Test
public void testBlockReceiving() throws Exception
{
WebSocketClient client = new WebSocketClient(_factory);
client.setMaxIdleTime(60000);
final AtomicBoolean open = new AtomicBoolean();
final AtomicInteger close = new AtomicInteger();
final CountDownLatch _latch = new CountDownLatch(1);
final StringBuilder closeMessage = new StringBuilder();
final Exchanger<String> exchanger = new Exchanger<String>();
Future<WebSocket.Connection> future=client.open(new URI("ws://127.0.0.1:"+_serverPort+"/"),new WebSocket.OnTextMessage()
{
public void onOpen(Connection connection)
{
open.set(true);
}
public void onClose(int closeCode, String message)
{
close.set(closeCode);
closeMessage.append(message);
_latch.countDown();
}
public void onMessage(String data)
{
try
{
exchanger.exchange(data);
}
catch (InterruptedException e)
{
// e.printStackTrace();
}
}
});
Socket socket = _server.accept();
socket.setSoTimeout(60000);
accept(socket);
WebSocket.Connection connection = future.get(250,TimeUnit.MILLISECONDS);
Assert.assertNotNull(connection);
Assert.assertTrue(open.get());
Assert.assertEquals(0,close.get());
// define some messages to send server to client
byte[] send = new byte[] { (byte)0x81, (byte) 0x05,
(byte)'H', (byte)'e', (byte)'l', (byte)'l',(byte)'o' };
final int messages=100000;
final AtomicInteger m = new AtomicInteger();
// Set up a consumer of received messages that waits a while before consuming
Thread consumer = new Thread()
{
@Override
public void run()
{
try
{
Thread.sleep(200);
while (m.get() < messages)
{
String msg = exchanger.exchange(null);
if ("Hello".equals(msg))
{
m.incrementAndGet();
}
else
{
throw new IllegalStateException("exchanged " + msg);
}
if (m.get() % 1000 == 0)
{
// Artificially slow reader
Thread.sleep(10);
}
}
}
catch(InterruptedException e)
{
return;
}
catch(Exception e)
{
e.printStackTrace();
}
}
};
consumer.start();
long start=System.currentTimeMillis();
for (int i=0;i<messages;i++)
{
socket.getOutputStream().write(send,0,send.length);
socket.getOutputStream().flush();
}
while(consumer.isAlive())
{
Thread.sleep(10);
}
// Duration of the read operation.
long readDur = (System.currentTimeMillis() - start);
Assert.assertThat("read duration", readDur, greaterThan(1000L)); // reading was blocked
Assert.assertEquals(m.get(),messages);
// Close with code
start=System.currentTimeMillis();
socket.getOutputStream().write(new byte[]{(byte)0x88, (byte) 0x02, (byte)4, (byte)87 },0,4);
socket.getOutputStream().flush();
_latch.await(10,TimeUnit.SECONDS);
Assert.assertTrue(System.currentTimeMillis()-start<5000);
Assert.assertEquals(1002,close.get());
Assert.assertEquals("Invalid close code 1111", closeMessage.toString());
}
@Test
public void testURIWithDefaultPort() throws Exception
{
URI uri = new URI("ws://localhost");
InetSocketAddress addr = WebSocketClient.toSocketAddress(uri);
Assert.assertThat("URI (" + uri + ").host", addr.getHostName(), is("localhost"));
Assert.assertThat("URI (" + uri + ").port", addr.getPort(), is(80));
}
@Test
public void testURIWithDefaultWSSPort() throws Exception
{
URI uri = new URI("wss://localhost");
InetSocketAddress addr = WebSocketClient.toSocketAddress(uri);
Assert.assertThat("URI (" + uri + ").host", addr.getHostName(), is("localhost"));
Assert.assertThat("URI (" + uri + ").port", addr.getPort(), is(443));
}
private void respondToClient(Socket connection, String serverResponse) throws IOException
{
InputStream in = null;
InputStreamReader isr = null;
BufferedReader buf = null;
OutputStream out = null;
try {
in = connection.getInputStream();
isr = new InputStreamReader(in);
buf = new BufferedReader(isr);
String line;
while((line = buf.readLine())!=null)
{
// System.err.println(line);
if(line.length() == 0)
{
// Got the "\r\n" line.
break;
}
}
// System.out.println("[Server-Out] " + serverResponse);
out = connection.getOutputStream();
out.write(serverResponse.getBytes());
out.flush();
}
finally
{
IO.close(buf);
IO.close(isr);
IO.close(in);
IO.close(out);
}
}
private void accept(Socket connection) throws IOException
{
String key="not sent";
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
for (String line=in.readLine();line!=null;line=in.readLine())
{
if (line.length()==0)
break;
if (line.startsWith("Sec-WebSocket-Key:"))
key=line.substring(18).trim();
}
connection.getOutputStream().write((
"HTTP/1.1 101 Upgrade\r\n" +
"Sec-WebSocket-Accept: "+ WebSocketConnectionRFC6455.hashKey(key) +"\r\n" +
"\r\n").getBytes());
}
}