blob: 78f925ff121da2a699becc34fea9bf6e61d5ff03 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2015 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.common.test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import java.io.IOException;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.EventQueue;
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.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.Generator;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.io.IOState;
import org.junit.Assert;
/**
* Fuzzing utility for the AB tests.
*/
public class Fuzzer implements AutoCloseable
{
public static enum CloseState
{
OPEN,
REMOTE_INITIATED,
LOCAL_INITIATED
}
public static enum SendMode
{
BULK,
PER_FRAME,
SLOW
}
public static enum DisconnectMode
{
/** Disconnect occurred after a proper close handshake */
CLEAN,
/** Disconnect occurred in a harsh manner, without a close handshake */
UNCLEAN
}
private static final int KBYTE = 1024;
private static final int MBYTE = KBYTE * KBYTE;
private static final Logger LOG = Log.getLogger(Fuzzer.class);
// Client side framing mask
protected static final byte[] MASK =
{ 0x11, 0x22, 0x33, 0x44 };
private final BlockheadClient client;
private final Generator generator;
private final String testname;
private SendMode sendMode = SendMode.BULK;
private int slowSendSegmentSize = 5;
public Fuzzer(Fuzzed testcase) throws Exception
{
WebSocketPolicy policy = WebSocketPolicy.newClientPolicy();
int bigMessageSize = 20 * MBYTE;
policy.setMaxTextMessageSize(bigMessageSize);
policy.setMaxBinaryMessageSize(bigMessageSize);
policy.setIdleTimeout(5000);
this.client = new BlockheadClient(policy,testcase.getServerURI());
this.client.setTimeout(2,TimeUnit.SECONDS);
this.generator = testcase.getLaxGenerator();
this.testname = testcase.getTestMethodName();
}
public ByteBuffer asNetworkBuffer(List<WebSocketFrame> send)
{
int buflen = 0;
for (Frame f : send)
{
buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH;
}
ByteBuffer buf = ByteBuffer.allocate(buflen);
// Generate frames
for (WebSocketFrame f : send)
{
setClientMask(f);
generator.generateWholeFrame(f,buf);
}
buf.flip();
return buf;
}
@Override
public void close() throws Exception
{
this.client.disconnect();
}
public void disconnect()
{
this.client.disconnect();
}
public void connect() throws IOException
{
if (!client.isConnected())
{
client.connect();
client.addHeader("X-TestCase: " + testname + "\r\n");
client.sendStandardRequest();
client.expectUpgradeResponse();
}
}
public void expect(List<WebSocketFrame> expect) throws Exception
{
expect(expect,10,TimeUnit.SECONDS);
}
public void expect(List<WebSocketFrame> expect, int duration, TimeUnit unit) throws Exception
{
int expectedCount = expect.size();
LOG.debug("expect() {} frame(s)",expect.size());
// Read frames
EventQueue<WebSocketFrame> frames = client.readFrames(expect.size(),duration,unit);
String prefix = "";
for (int i = 0; i < expectedCount; i++)
{
WebSocketFrame expected = expect.get(i);
WebSocketFrame actual = frames.poll();
prefix = "Frame[" + i + "]";
LOG.debug("{} {}",prefix,actual);
Assert.assertThat(prefix + ".opcode",OpCode.name(actual.getOpCode()),is(OpCode.name(expected.getOpCode())));
prefix += "/" + actual.getOpCode();
if (expected.getOpCode() == OpCode.CLOSE)
{
CloseInfo expectedClose = new CloseInfo(expected);
CloseInfo actualClose = new CloseInfo(actual);
Assert.assertThat(prefix + ".statusCode",actualClose.getStatusCode(),is(expectedClose.getStatusCode()));
}
else
{
Assert.assertThat(prefix + ".payloadLength",actual.getPayloadLength(),is(expected.getPayloadLength()));
ByteBufferAssert.assertEquals(prefix + ".payload",expected.getPayload(),actual.getPayload());
}
}
}
public void expect(WebSocketFrame expect) throws Exception
{
expect(Collections.singletonList(expect));
}
public void expectNoMoreFrames()
{
// TODO Should test for no more frames. success if connection closed.
}
public CloseState getCloseState()
{
IOState ios = client.getIOState();
if (ios.wasLocalCloseInitiated())
{
return CloseState.LOCAL_INITIATED;
}
else if (ios.wasRemoteCloseInitiated())
{
return CloseState.REMOTE_INITIATED;
}
else
{
return CloseState.OPEN;
}
}
public SendMode getSendMode()
{
return sendMode;
}
public int getSlowSendSegmentSize()
{
return slowSendSegmentSize;
}
public void send(ByteBuffer buf) throws IOException
{
Assert.assertThat("Client connected",client.isConnected(),is(true));
LOG.debug("Sending bytes {}",BufferUtil.toDetailString(buf));
if (sendMode == SendMode.SLOW)
{
client.writeRawSlowly(buf,slowSendSegmentSize);
}
else
{
client.writeRaw(buf);
}
}
public void send(ByteBuffer buf, int numBytes) throws IOException
{
client.writeRaw(buf,numBytes);
client.flush();
}
public void send(List<WebSocketFrame> send) throws IOException
{
Assert.assertThat("Client connected",client.isConnected(),is(true));
LOG.debug("[{}] Sending {} frames (mode {})",testname,send.size(),sendMode);
if ((sendMode == SendMode.BULK) || (sendMode == SendMode.SLOW))
{
int buflen = 0;
for (Frame f : send)
{
buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH;
}
ByteBuffer buf = ByteBuffer.allocate(buflen);
// Generate frames
for (WebSocketFrame f : send)
{
setClientMask(f);
buf.put(generator.generateHeaderBytes(f));
if (f.hasPayload())
{
buf.put(f.getPayload());
}
}
BufferUtil.flipToFlush(buf,0);
// Write Data Frame
switch (sendMode)
{
case BULK:
client.writeRaw(buf);
break;
case SLOW:
client.writeRawSlowly(buf,slowSendSegmentSize);
break;
default:
throw new RuntimeException("Whoops, unsupported sendMode: " + sendMode);
}
}
else if (sendMode == SendMode.PER_FRAME)
{
for (WebSocketFrame f : send)
{
f.setMask(MASK); // make sure we have mask set
// Using lax generator, generate and send
ByteBuffer fullframe = ByteBuffer.allocate(f.getPayloadLength() + Generator.MAX_HEADER_LENGTH);
BufferUtil.clearToFill(fullframe);
generator.generateWholeFrame(f,fullframe);
BufferUtil.flipToFlush(fullframe,0);
client.writeRaw(fullframe);
client.flush();
}
}
}
public void send(WebSocketFrame send) throws IOException
{
send(Collections.singletonList(send));
}
public void sendAndIgnoreBrokenPipe(List<WebSocketFrame> send) throws IOException
{
try
{
send(send);
}
catch (SocketException ignore)
{
// Potential for SocketException (Broken Pipe) here.
// But not in 100% of testing scenarios. It is a safe
// exception to ignore in this testing scenario, as the
// slow writing of the frames can result in the server
// throwing a PROTOCOL ERROR termination/close when it
// encounters the bad continuation frame above (this
// termination is the expected behavior), and this
// early socket close can propagate back to the client
// before it has a chance to finish writing out the
// remaining frame octets
Assert.assertThat("Allowed to be a broken pipe",ignore.getMessage().toLowerCase(Locale.ENGLISH),containsString("broken pipe"));
}
}
private void setClientMask(WebSocketFrame f)
{
if (LOG.isDebugEnabled())
{
f.setMask(new byte[]
{ 0x00, 0x00, 0x00, 0x00 });
}
else
{
f.setMask(MASK); // make sure we have mask set
}
}
public void setSendMode(SendMode sendMode)
{
this.sendMode = sendMode;
}
public void setSlowSendSegmentSize(int segmentSize)
{
this.slowSendSegmentSize = segmentSize;
}
}