Bug 391140 - Implement x-webkit-deflate-frame extension as-used by Chrome/Safari
This commit is contained in:
parent
b18a5b05dc
commit
d23215f3f4
|
@ -230,6 +230,9 @@ public class UpgradeConnection extends AbstractConnection
|
|||
// Connect extensions
|
||||
if (extensions != null)
|
||||
{
|
||||
connection.getParser().configureFromExtensions(extensions);
|
||||
connection.getGenerator().configureFromExtensions(extensions);
|
||||
|
||||
Iterator<Extension> extIter;
|
||||
// Connect outgoings
|
||||
extIter = extensions.iterator();
|
||||
|
@ -238,23 +241,6 @@ public class UpgradeConnection extends AbstractConnection
|
|||
Extension ext = extIter.next();
|
||||
ext.setNextOutgoingFrames(outgoing);
|
||||
outgoing = ext;
|
||||
|
||||
// Handle RSV reservations
|
||||
if (ext.useRsv1())
|
||||
{
|
||||
connection.getGenerator().setRsv1InUse(true);
|
||||
connection.getParser().setRsv1InUse(true);
|
||||
}
|
||||
if (ext.useRsv2())
|
||||
{
|
||||
connection.getGenerator().setRsv2InUse(true);
|
||||
connection.getParser().setRsv2InUse(true);
|
||||
}
|
||||
if (ext.useRsv3())
|
||||
{
|
||||
connection.getGenerator().setRsv3InUse(true);
|
||||
connection.getParser().setRsv3InUse(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect incomings
|
||||
|
|
|
@ -368,6 +368,8 @@ public class BlockheadServer
|
|||
// Connect extensions
|
||||
if (!extensions.isEmpty())
|
||||
{
|
||||
generator.configureFromExtensions(extensions);
|
||||
|
||||
Iterator<Extension> extIter;
|
||||
// Connect outgoings
|
||||
extIter = extensions.iterator();
|
||||
|
@ -376,20 +378,6 @@ public class BlockheadServer
|
|||
Extension ext = extIter.next();
|
||||
ext.setNextOutgoingFrames(outgoing);
|
||||
outgoing = ext;
|
||||
|
||||
// Handle RSV reservations
|
||||
if (ext.useRsv1())
|
||||
{
|
||||
generator.setRsv1InUse(true);
|
||||
}
|
||||
if (ext.useRsv2())
|
||||
{
|
||||
generator.setRsv2InUse(true);
|
||||
}
|
||||
if (ext.useRsv3())
|
||||
{
|
||||
generator.setRsv3InUse(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect incomings
|
||||
|
|
|
@ -88,6 +88,58 @@ public abstract class Extension implements OutgoingFrames, IncomingFrames
|
|||
nextIncoming(frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate that the extension makes use of the RSV1 bit of the base websocket framing.
|
||||
* <p>
|
||||
* This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV1.
|
||||
*
|
||||
* @return true if extension uses RSV1 for its own purposes.
|
||||
*/
|
||||
public boolean isRsv1User()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate that the extension makes use of the RSV2 bit of the base websocket framing.
|
||||
* <p>
|
||||
* This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV2.
|
||||
*
|
||||
* @return true if extension uses RSV2 for its own purposes.
|
||||
*/
|
||||
public boolean isRsv2User()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate that the extension makes use of the RSV3 bit of the base websocket framing.
|
||||
* <p>
|
||||
* This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV3.
|
||||
*
|
||||
* @return true if extension uses RSV3 for its own purposes.
|
||||
*/
|
||||
public boolean isRsv3User()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate that the extension works as a decoder of TEXT Data Frames.
|
||||
* <p>
|
||||
* This is used to adjust validation during parsing/generating, as per spec TEXT Data Frames can only contain UTF8 encoded String data.
|
||||
* <p>
|
||||
* Example: a compression extension will process a compressed set of text data, the parser/generator should no longer be concerned about the validity of the
|
||||
* TEXT Data Frames as this is now the responsibility of the extension.
|
||||
*
|
||||
* @return true if extension will process TEXT Data Frames, false if extension makes no modifications of TEXT Data Frames. If false, the parser/generator is
|
||||
* now free to validate the conformance to spec of TEXT Data Frames.
|
||||
*/
|
||||
public boolean isTextDataDecoder()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for {@link #getNextIncomingFrames()#incoming(WebSocketException)}
|
||||
*
|
||||
|
@ -173,19 +225,4 @@ public abstract class Extension implements OutgoingFrames, IncomingFrames
|
|||
{
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
public boolean useRsv1()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean useRsv2()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean useRsv3()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.eclipse.jetty.websocket.core.api.Extension;
|
|||
import org.eclipse.jetty.websocket.core.api.ExtensionRegistry;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.core.extensions.deflate.WebkitDeflateFrameExtension;
|
||||
import org.eclipse.jetty.websocket.core.extensions.fragment.FragmentExtension;
|
||||
import org.eclipse.jetty.websocket.core.extensions.identity.IdentityExtension;
|
||||
import org.eclipse.jetty.websocket.core.extensions.permessage.CompressExtension;
|
||||
|
@ -50,6 +51,7 @@ public class WebSocketExtensionRegistry implements ExtensionRegistry
|
|||
|
||||
this.registry.put("identity",IdentityExtension.class);
|
||||
this.registry.put("fragment",FragmentExtension.class);
|
||||
this.registry.put("x-webkit-deflate-frame",WebkitDeflateFrameExtension.class);
|
||||
this.registry.put("permessage-compress",CompressExtension.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.core.extensions.deflate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.websocket.core.api.BadPayloadException;
|
||||
import org.eclipse.jetty.websocket.core.api.Extension;
|
||||
import org.eclipse.jetty.websocket.core.protocol.ExtensionConfig;
|
||||
import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
||||
|
||||
/**
|
||||
* Implementation of the <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-05.txt">x-webkit-deflate-frame</a> extension seen out
|
||||
* in the wild.
|
||||
*/
|
||||
public class WebkitDeflateFrameExtension extends Extension
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(WebkitDeflateFrameExtension.class);
|
||||
private static final int BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
private Deflater deflater;
|
||||
private Inflater inflater;
|
||||
|
||||
public ByteBuffer deflate(ByteBuffer data)
|
||||
{
|
||||
int length = data.remaining();
|
||||
|
||||
// prepare the uncompressed input
|
||||
deflater.reset();
|
||||
deflater.setInput(BufferUtil.toArray(data));
|
||||
deflater.finish();
|
||||
|
||||
// prepare the output buffer
|
||||
int bufsize = Math.max(BUFFER_SIZE,length * 2);
|
||||
ByteBuffer buf = getBufferPool().acquire(bufsize,false);
|
||||
BufferUtil.clearToFill(buf);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("Uncompressed length={} - {}",length,buf.position());
|
||||
}
|
||||
|
||||
while (!deflater.finished())
|
||||
{
|
||||
byte out[] = new byte[BUFFER_SIZE];
|
||||
int len = deflater.deflate(out,0,out.length,Deflater.SYNC_FLUSH);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("Deflater: finished={}, needsInput={}, len={} / input.len={}",deflater.finished(),deflater.needsInput(),len,length);
|
||||
}
|
||||
|
||||
buf.put(out,0,len);
|
||||
}
|
||||
BufferUtil.flipToFlush(buf,0);
|
||||
|
||||
/* Per the spec, it says that BFINAL 1 or 0 are allowed.
|
||||
* However, Java always uses BFINAL 1, where the browsers
|
||||
* Chrome and Safari fail to decompress when it encounters
|
||||
* BFINAL 1.
|
||||
*
|
||||
* This hack will always set BFINAL 0
|
||||
*/
|
||||
byte b0 = buf.get(0);
|
||||
if ((b0 & 1) != 0) // if BFINAL 1
|
||||
{
|
||||
buf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incoming(WebSocketFrame frame)
|
||||
{
|
||||
if (frame.isControlFrame() || !frame.isRsv1())
|
||||
{
|
||||
// Cannot modify incoming control frames or ones with RSV1 set.
|
||||
super.incoming(frame);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Decompressing Frame: {}",frame);
|
||||
|
||||
ByteBuffer data = frame.getPayload();
|
||||
try
|
||||
{
|
||||
ByteBuffer uncompressed = inflate(data);
|
||||
frame.setPayload(uncompressed);
|
||||
nextIncoming(frame);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// release original buffer (no longer needed)
|
||||
getBufferPool().release(data);
|
||||
}
|
||||
}
|
||||
|
||||
public ByteBuffer inflate(ByteBuffer data)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("inflate: {}",BufferUtil.toDetailString(data));
|
||||
LOG.debug("raw data: {}",TypeUtil.toHexString(BufferUtil.toArray(data)));
|
||||
}
|
||||
|
||||
// Set the data that is compressed to the inflater
|
||||
byte compressed[] = BufferUtil.toArray(data);
|
||||
inflater.reset();
|
||||
inflater.setInput(compressed,0,compressed.length);
|
||||
|
||||
// Establish place for inflated data
|
||||
byte buf[] = new byte[BUFFER_SIZE];
|
||||
try
|
||||
{
|
||||
int inflated = inflater.inflate(buf);
|
||||
if (inflated == 0)
|
||||
{
|
||||
throw new DataFormatException("Insufficient compressed data");
|
||||
}
|
||||
|
||||
ByteBuffer ret = ByteBuffer.wrap(buf,0,inflated);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("uncompressed={}",BufferUtil.toDetailString(ret));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
catch (DataFormatException e)
|
||||
{
|
||||
LOG.warn(e);
|
||||
throw new BadPayloadException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates use of RSV1 flag for indicating deflation is in use.
|
||||
* <p>
|
||||
* Also known as the "COMP" framing header bit
|
||||
*/
|
||||
@Override
|
||||
public boolean isRsv1User()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that this extensions is now responsible for TEXT Data Frame compliance to the WebSocket spec.
|
||||
*/
|
||||
@Override
|
||||
public boolean isTextDataDecoder()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <C> void output(C context, Callback<C> callback, WebSocketFrame frame) throws IOException
|
||||
{
|
||||
if (frame.isControlFrame())
|
||||
{
|
||||
// skip, cannot compress control frames.
|
||||
nextOutput(context,callback,frame);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer data = frame.getPayload();
|
||||
try
|
||||
{
|
||||
// deflate data
|
||||
ByteBuffer buf = deflate(data);
|
||||
frame.setPayload(buf);
|
||||
frame.setRsv1(true);
|
||||
nextOutput(context,callback,frame);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// free original data buffer
|
||||
getBufferPool().release(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfig(ExtensionConfig config)
|
||||
{
|
||||
super.setConfig(config);
|
||||
|
||||
boolean nowrap = true;
|
||||
|
||||
deflater = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
|
||||
deflater.setStrategy(Deflater.DEFAULT_STRATEGY);
|
||||
inflater = new Inflater(nowrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("DeflateFrameExtension[]");
|
||||
}
|
||||
}
|
|
@ -267,7 +267,7 @@ public class CompressExtension extends Extension
|
|||
* Indicates use of RSV1 flag for indicating deflation is in use.
|
||||
*/
|
||||
@Override
|
||||
public boolean useRsv1()
|
||||
public boolean isRsv1User()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -23,11 +23,11 @@ import java.nio.ByteBuffer;
|
|||
import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
||||
|
||||
/**
|
||||
* Binary payload validator does nothing, essentially.
|
||||
* payload validator does no validation.
|
||||
*/
|
||||
public class BinaryValidator implements PayloadProcessor
|
||||
public class NoOpValidator implements PayloadProcessor
|
||||
{
|
||||
public static final BinaryValidator INSTANCE = new BinaryValidator();
|
||||
public static final NoOpValidator INSTANCE = new NoOpValidator();
|
||||
|
||||
@Override
|
||||
public void process(ByteBuffer payload)
|
|
@ -19,11 +19,13 @@
|
|||
package org.eclipse.jetty.websocket.core.protocol;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
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.core.api.Extension;
|
||||
import org.eclipse.jetty.websocket.core.api.ProtocolException;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketBehavior;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketPolicy;
|
||||
|
@ -164,6 +166,31 @@ public class Generator
|
|||
|
||||
}
|
||||
|
||||
public void configureFromExtensions(List<? extends Extension> exts)
|
||||
{
|
||||
// default
|
||||
this.rsv1InUse = false;
|
||||
this.rsv2InUse = false;
|
||||
this.rsv3InUse = false;
|
||||
|
||||
// configure from list of extensions in use
|
||||
for(Extension ext: exts)
|
||||
{
|
||||
if (ext.isRsv1User())
|
||||
{
|
||||
this.rsv1InUse = true;
|
||||
}
|
||||
if (ext.isRsv2User())
|
||||
{
|
||||
this.rsv2InUse = true;
|
||||
}
|
||||
if (ext.isRsv3User())
|
||||
{
|
||||
this.rsv3InUse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate, into a ByteBuffer, no more than bufferSize of contents from the frame. If the frame exceeds the bufferSize, then multiple calls to
|
||||
* {@link #generate(int, WebSocketFrame)} are required to obtain each window of ByteBuffer to complete the frame.
|
||||
|
@ -368,21 +395,6 @@ public class Generator
|
|||
return rsv3InUse;
|
||||
}
|
||||
|
||||
public void setRsv1InUse(boolean rsv1InUse)
|
||||
{
|
||||
this.rsv1InUse = rsv1InUse;
|
||||
}
|
||||
|
||||
public void setRsv2InUse(boolean rsv2InUse)
|
||||
{
|
||||
this.rsv2InUse = rsv2InUse;
|
||||
}
|
||||
|
||||
public void setRsv3InUse(boolean rsv3InUse)
|
||||
{
|
||||
this.rsv3InUse = rsv3InUse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
@ -19,18 +19,20 @@
|
|||
package org.eclipse.jetty.websocket.core.protocol;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
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.core.api.Extension;
|
||||
import org.eclipse.jetty.websocket.core.api.MessageTooLargeException;
|
||||
import org.eclipse.jetty.websocket.core.api.ProtocolException;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketException;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.core.io.IncomingFrames;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.BinaryValidator;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.CloseReasonValidator;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.DeMaskProcessor;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.NoOpValidator;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.PayloadProcessor;
|
||||
import org.eclipse.jetty.websocket.core.io.payload.UTF8Validator;
|
||||
|
||||
|
@ -71,6 +73,8 @@ public class Parser
|
|||
private boolean rsv2InUse = false;
|
||||
/** Is there an extension using RSV3 */
|
||||
private boolean rsv3InUse = false;
|
||||
/** Is there an extension that processes invalid UTF8 text messages (such as compressed content) */
|
||||
private boolean isTextFrameValidated = true;
|
||||
|
||||
private static final Logger LOG = Log.getLogger(Parser.class);
|
||||
private IncomingFrames incomingFramesHandler;
|
||||
|
@ -111,6 +115,36 @@ public class Parser
|
|||
}
|
||||
}
|
||||
|
||||
public void configureFromExtensions(List<? extends Extension> exts)
|
||||
{
|
||||
// default
|
||||
this.rsv1InUse = false;
|
||||
this.rsv2InUse = false;
|
||||
this.rsv3InUse = false;
|
||||
this.isTextFrameValidated = true;
|
||||
|
||||
// configure from list of extensions in use
|
||||
for(Extension ext: exts)
|
||||
{
|
||||
if (ext.isRsv1User())
|
||||
{
|
||||
this.rsv1InUse = true;
|
||||
}
|
||||
if (ext.isRsv2User())
|
||||
{
|
||||
this.rsv2InUse = true;
|
||||
}
|
||||
if (ext.isRsv3User())
|
||||
{
|
||||
this.rsv3InUse = true;
|
||||
}
|
||||
if (ext.isTextDataDecoder())
|
||||
{
|
||||
this.isTextFrameValidated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IncomingFrames getIncomingFramesHandler()
|
||||
{
|
||||
return incomingFramesHandler;
|
||||
|
@ -262,7 +296,10 @@ public class Parser
|
|||
throw new ProtocolException("Unknown opcode: " + opc);
|
||||
}
|
||||
|
||||
LOG.debug("OpCode {}, fin={}",OpCode.name(opcode),fin);
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("OpCode {}, fin={} rsv={}{}{}",OpCode.name(opcode),fin,(rsv1?'1':'.'),(rsv2?'1':'.'),(rsv3?'1':'.'));
|
||||
}
|
||||
|
||||
/*
|
||||
* RFC 6455 Section 5.2
|
||||
|
@ -290,13 +327,20 @@ public class Parser
|
|||
switch (opcode)
|
||||
{
|
||||
case OpCode.TEXT:
|
||||
strictnessProcessor = new UTF8Validator();
|
||||
if (isTextFrameValidated)
|
||||
{
|
||||
strictnessProcessor = new UTF8Validator();
|
||||
}
|
||||
else
|
||||
{
|
||||
strictnessProcessor = NoOpValidator.INSTANCE;
|
||||
}
|
||||
break;
|
||||
case OpCode.CLOSE:
|
||||
strictnessProcessor = new CloseReasonValidator();
|
||||
break;
|
||||
default:
|
||||
strictnessProcessor = BinaryValidator.INSTANCE;
|
||||
strictnessProcessor = NoOpValidator.INSTANCE;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -539,21 +583,6 @@ public class Parser
|
|||
this.incomingFramesHandler = incoming;
|
||||
}
|
||||
|
||||
public void setRsv1InUse(boolean rsv1InUse)
|
||||
{
|
||||
this.rsv1InUse = rsv1InUse;
|
||||
}
|
||||
|
||||
public void setRsv2InUse(boolean rsv2InUse)
|
||||
{
|
||||
this.rsv2InUse = rsv2InUse;
|
||||
}
|
||||
|
||||
public void setRsv3InUse(boolean rsv3InUse)
|
||||
{
|
||||
this.rsv3InUse = rsv3InUse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
@ -23,7 +23,7 @@ import org.junit.runners.Suite;
|
|||
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses(
|
||||
{ DeflateFrameExtensionTest.class, FragmentExtensionTest.class, IdentityExtensionTest.class })
|
||||
{ CompressMessageExtensionTest.class, FragmentExtensionTest.class, IdentityExtensionTest.class, WebkitDeflateFrameExtensionTest.class })
|
||||
public class AllTests
|
||||
{
|
||||
/* nothing to do here, its all done in the annotations */
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.eclipse.jetty.websocket.core.extensions;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
@ -36,19 +38,12 @@ import org.eclipse.jetty.websocket.core.protocol.ExtensionConfig;
|
|||
import org.eclipse.jetty.websocket.core.protocol.IncomingFramesCapture;
|
||||
import org.eclipse.jetty.websocket.core.protocol.OpCode;
|
||||
import org.eclipse.jetty.websocket.core.protocol.OutgoingFramesCapture;
|
||||
import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
||||
import org.eclipse.jetty.websocket.core.protocol.OutgoingFramesCapture.Write;
|
||||
import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
|
||||
public class DeflateFrameExtensionTest
|
||||
public class CompressMessageExtensionTest
|
||||
{
|
||||
/**
|
||||
* Test a large payload (a payload length over 65535 bytes)
|
||||
|
@ -206,7 +201,7 @@ public class DeflateFrameExtensionTest
|
|||
CompressExtension ext = new CompressExtension();
|
||||
ext.setBufferPool(new MappedByteBufferPool());
|
||||
ext.setPolicy(WebSocketPolicy.newServerPolicy());
|
||||
ExtensionConfig config = ExtensionConfig.parse("x-deflate-frame;minLength=16");
|
||||
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
|
||||
ext.setConfig(config);
|
||||
|
||||
ext.setNextIncomingFrames(capture);
|
|
@ -0,0 +1,282 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.core.extensions;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.FutureCallback;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.websocket.core.ByteBufferAssert;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.core.extensions.deflate.WebkitDeflateFrameExtension;
|
||||
import org.eclipse.jetty.websocket.core.protocol.ExtensionConfig;
|
||||
import org.eclipse.jetty.websocket.core.protocol.Generator;
|
||||
import org.eclipse.jetty.websocket.core.protocol.IncomingFramesCapture;
|
||||
import org.eclipse.jetty.websocket.core.protocol.OpCode;
|
||||
import org.eclipse.jetty.websocket.core.protocol.OutgoingNetworkBytesCapture;
|
||||
import org.eclipse.jetty.websocket.core.protocol.Parser;
|
||||
import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class WebkitDeflateFrameExtensionTest
|
||||
{
|
||||
private void assertIncoming(byte[] raw, String... expectedTextDatas)
|
||||
{
|
||||
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
|
||||
IncomingFramesCapture capture = new IncomingFramesCapture();
|
||||
|
||||
WebkitDeflateFrameExtension ext = new WebkitDeflateFrameExtension();
|
||||
ext.setBufferPool(new MappedByteBufferPool());
|
||||
ext.setPolicy(policy);
|
||||
|
||||
ExtensionConfig config = ExtensionConfig.parse("x-webkit-deflate-frame");
|
||||
ext.setConfig(config);
|
||||
|
||||
ext.setNextIncomingFrames(capture);
|
||||
|
||||
Parser parser = new Parser(policy);
|
||||
parser.configureFromExtensions(Collections.singletonList(ext));
|
||||
parser.setIncomingFramesHandler(ext);
|
||||
|
||||
parser.parse(ByteBuffer.wrap(raw));
|
||||
|
||||
int len = expectedTextDatas.length;
|
||||
capture.assertFrameCount(len);
|
||||
capture.assertHasFrame(OpCode.TEXT,len);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
WebSocketFrame actual = capture.getFrames().get(i);
|
||||
String prefix = "Frame[" + i + "]";
|
||||
Assert.assertThat(prefix + ".opcode",actual.getOpCode(),is(OpCode.TEXT));
|
||||
Assert.assertThat(prefix + ".fin",actual.isFin(),is(true));
|
||||
Assert.assertThat(prefix + ".rsv1",actual.isRsv1(),is(false)); // RSV1 should be unset at this point
|
||||
Assert.assertThat(prefix + ".rsv2",actual.isRsv2(),is(false));
|
||||
Assert.assertThat(prefix + ".rsv3",actual.isRsv3(),is(false));
|
||||
|
||||
ByteBuffer expected = BufferUtil.toBuffer(expectedTextDatas[i],StringUtil.__UTF8_CHARSET);
|
||||
Assert.assertThat(prefix + ".payloadLength",actual.getPayloadLength(),is(expected.remaining()));
|
||||
ByteBufferAssert.assertEquals(prefix + ".payload",expected,actual.getPayload().slice());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertOutgoing(String text, String expectedHex) throws IOException
|
||||
{
|
||||
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
|
||||
|
||||
WebkitDeflateFrameExtension ext = new WebkitDeflateFrameExtension();
|
||||
ext.setBufferPool(new MappedByteBufferPool());
|
||||
ext.setPolicy(policy);
|
||||
|
||||
ExtensionConfig config = ExtensionConfig.parse("x-webkit-deflate-frame");
|
||||
ext.setConfig(config);
|
||||
|
||||
ByteBufferPool bufferPool = new MappedByteBufferPool();
|
||||
boolean validating = true;
|
||||
Generator generator = new Generator(policy,bufferPool,validating);
|
||||
generator.configureFromExtensions(Collections.singletonList(ext));
|
||||
|
||||
OutgoingNetworkBytesCapture capture = new OutgoingNetworkBytesCapture(generator);
|
||||
ext.setNextOutgoingFrames(capture);
|
||||
|
||||
WebSocketFrame frame = WebSocketFrame.text(text);
|
||||
ext.output(null,new FutureCallback<Void>(),frame);
|
||||
|
||||
capture.assertBytes(0,expectedHex);
|
||||
}
|
||||
|
||||
private String deflate(byte data[], int level, boolean nowrap, int strategy, int flush)
|
||||
{
|
||||
Deflater compressor = new Deflater(level,nowrap);
|
||||
compressor.setStrategy(strategy);
|
||||
|
||||
// Prime the compressor
|
||||
compressor.reset();
|
||||
compressor.setInput(data,0,data.length);
|
||||
compressor.finish();
|
||||
|
||||
byte out[] = new byte[64];
|
||||
int len = compressor.deflate(out,0,out.length,flush);
|
||||
compressor.end();
|
||||
|
||||
String ret = TypeUtil.toHexString(out,0,len);
|
||||
System.out.printf("deflate(l=%d,s=%d,f=%d,w=%-5b) => %s%n",level,strategy,flush,nowrap,ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllDeflate() throws Exception
|
||||
{
|
||||
int strategies[] = new int[] {
|
||||
Deflater.DEFAULT_STRATEGY,
|
||||
Deflater.FILTERED,
|
||||
Deflater.HUFFMAN_ONLY
|
||||
};
|
||||
int flushes[] = new int[] {
|
||||
Deflater.FULL_FLUSH,
|
||||
Deflater.NO_FLUSH,
|
||||
Deflater.SYNC_FLUSH
|
||||
};
|
||||
|
||||
byte uncompressed[] = StringUtil.getUtf8Bytes("info:");
|
||||
|
||||
for(int level = 0; level <= 9; level++)
|
||||
{
|
||||
for (int strategy : strategies)
|
||||
{
|
||||
for (int flush : flushes)
|
||||
{
|
||||
deflate(uncompressed,level,true,strategy,flush);
|
||||
deflate(uncompressed,level,false,strategy,flush);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChrome20_Hello()
|
||||
{
|
||||
// Captured from Chrome 20.x - "Hello" (sent from browser/client)
|
||||
byte rawbuf[] = TypeUtil.fromHexString("c187832b5c11716391d84a2c5c");
|
||||
assertIncoming(rawbuf,"Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChrome20_Info()
|
||||
{
|
||||
// Captured from Chrome 20.x - "info:" (sent from browser/client)
|
||||
byte rawbuf[] = TypeUtil.fromHexString("c187ca4def7f0081a4b47d4fef");
|
||||
assertIncoming(rawbuf,"info:");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeflateBasics() throws Exception
|
||||
{
|
||||
// Setup deflater basics
|
||||
boolean nowrap = true;
|
||||
Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap);
|
||||
compressor.setStrategy(Deflater.DEFAULT_STRATEGY);
|
||||
|
||||
// Text to compress
|
||||
String text = "info:";
|
||||
byte uncompressed[] = StringUtil.getUtf8Bytes(text);
|
||||
|
||||
// Prime the compressor
|
||||
compressor.reset();
|
||||
compressor.setInput(uncompressed,0,uncompressed.length);
|
||||
compressor.finish();
|
||||
|
||||
// Perform compression
|
||||
ByteBuffer outbuf = ByteBuffer.allocate(64);
|
||||
BufferUtil.clearToFill(outbuf);
|
||||
|
||||
while (!compressor.finished())
|
||||
{
|
||||
byte out[] = new byte[64];
|
||||
int len = compressor.deflate(out,0,out.length,Deflater.SYNC_FLUSH);
|
||||
if (len > 0)
|
||||
{
|
||||
System.err.printf("Compressed %,d bytes%n",len);
|
||||
outbuf.put(out,0,len);
|
||||
}
|
||||
}
|
||||
compressor.end();
|
||||
|
||||
BufferUtil.flipToFlush(outbuf,0);
|
||||
byte b0 = outbuf.get(0);
|
||||
if ((b0 & 1) != 0)
|
||||
{
|
||||
outbuf.put(0,(b0 ^= 1));
|
||||
}
|
||||
byte compressed[] = BufferUtil.toArray(outbuf);
|
||||
|
||||
String actual = TypeUtil.toHexString(compressed);
|
||||
String expected = "CaCc4bCbB70200"; // what pywebsocket produces
|
||||
// String expected = "CbCc4bCbB70200"; // what java produces
|
||||
|
||||
System.out.printf("Compressed data: %s%n",actual);
|
||||
Assert.assertThat("Compressed data",actual,is(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInflateBasics() throws Exception
|
||||
{
|
||||
// should result in "info:" text if properly inflated
|
||||
byte rawbuf[] = TypeUtil.fromHexString("CaCc4bCbB70200"); // what pywebsocket produces
|
||||
// byte rawbuf[] = TypeUtil.fromHexString("CbCc4bCbB70200"); // what java produces
|
||||
|
||||
Inflater inflater = new Inflater(true);
|
||||
inflater.reset();
|
||||
inflater.setInput(rawbuf,0,rawbuf.length);
|
||||
|
||||
byte outbuf[] = new byte[64];
|
||||
int len = inflater.inflate(outbuf);
|
||||
inflater.end();
|
||||
Assert.assertThat("Inflated length",len,greaterThan(4));
|
||||
|
||||
String actual = StringUtil.toUTF8String(outbuf,0,len);
|
||||
Assert.assertThat("Inflated text",actual,is("info:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPyWebSocketServer_Hello()
|
||||
{
|
||||
// Captured from PyWebSocket - "Hello" (echo from server)
|
||||
byte rawbuf[] = TypeUtil.fromHexString("c107f248cdc9c90700");
|
||||
assertIncoming(rawbuf, "Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPyWebSocketServer_Long()
|
||||
{
|
||||
// Captured from PyWebSocket - Long Text (echo from server)
|
||||
byte rawbuf[] = TypeUtil.fromHexString("c1421cca410a80300c44d1abccce9df7" + "f018298634d05631138ab7b7b8fdef1f" + "dc0282e2061d575a45f6f2686bab25e1"
|
||||
+ "3fb7296fa02b5885eb3b0379c394f461" + "98cafd03");
|
||||
assertIncoming(rawbuf,"It's a big enough umbrella but it's always me that ends up getting wet.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPyWebSocketServer_Medium()
|
||||
{
|
||||
// Captured from PyWebSocket - "stackoverflow" (echo from server)
|
||||
byte rawbuf[]=TypeUtil.fromHexString("c10f2a2e494ccece2f4b2d4acbc92f0700");
|
||||
assertIncoming(rawbuf, "stackoverflow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that the server generated compressed form for "Hello" is
|
||||
* consistent with what PyWebSocket creates.
|
||||
*/
|
||||
@Test
|
||||
public void testServerGeneratedHello() throws IOException
|
||||
{
|
||||
assertOutgoing("Hello", "c107f248cdc9c90700");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.core.protocol;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.websocket.core.io.OutgoingFrames;
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
* Capture outgoing network bytes.
|
||||
*/
|
||||
public class OutgoingNetworkBytesCapture implements OutgoingFrames
|
||||
{
|
||||
private final Generator generator;
|
||||
private List<ByteBuffer> captured;
|
||||
|
||||
public OutgoingNetworkBytesCapture(Generator generator)
|
||||
{
|
||||
this.generator = generator;
|
||||
this.captured = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void assertBytes(int idx, String expectedHex)
|
||||
{
|
||||
Assert.assertThat("Capture index does not exist",idx,lessThan(captured.size()));
|
||||
ByteBuffer buf = captured.get(idx);
|
||||
String actualHex = TypeUtil.toHexString(BufferUtil.toArray(buf)).toUpperCase();
|
||||
Assert.assertThat("captured[" + idx + "]",actualHex,is(expectedHex.toUpperCase()));
|
||||
}
|
||||
|
||||
public List<ByteBuffer> getCaptured()
|
||||
{
|
||||
return captured;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <C> void output(C context, Callback<C> callback, WebSocketFrame frame) throws IOException
|
||||
{
|
||||
ByteBuffer buf = generator.generate(frame);
|
||||
captured.add(buf.slice());
|
||||
callback.completed(context);
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ public abstract class WebSocketHandler extends HandlerWrapper
|
|||
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
|
||||
configurePolicy(policy);
|
||||
webSocketFactory = new WebSocketServerFactory(policy);
|
||||
addBean(webSocketFactory);
|
||||
}
|
||||
|
||||
public abstract void configure(WebSocketServerFactory factory);
|
||||
|
@ -88,6 +89,7 @@ public abstract class WebSocketHandler extends HandlerWrapper
|
|||
if (webSocketFactory.acceptWebSocket(request,response))
|
||||
{
|
||||
// We have a socket instance created
|
||||
baseRequest.setHandled(true);
|
||||
return;
|
||||
}
|
||||
// If we reach this point, it means we had an incoming request to upgrade
|
||||
|
|
|
@ -56,7 +56,6 @@ import org.eclipse.jetty.websocket.core.io.WebSocketSession;
|
|||
import org.eclipse.jetty.websocket.core.io.event.EventDriver;
|
||||
import org.eclipse.jetty.websocket.core.io.event.EventDriverFactory;
|
||||
import org.eclipse.jetty.websocket.core.protocol.ExtensionConfig;
|
||||
import org.eclipse.jetty.websocket.server.handshake.HandshakeHixie76;
|
||||
import org.eclipse.jetty.websocket.server.handshake.HandshakeRFC6455;
|
||||
|
||||
/**
|
||||
|
@ -69,7 +68,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
|
|||
private final Map<Integer, WebSocketHandshake> handshakes = new HashMap<>();
|
||||
{
|
||||
handshakes.put(HandshakeRFC6455.VERSION,new HandshakeRFC6455());
|
||||
handshakes.put(HandshakeHixie76.VERSION,new HandshakeHixie76());
|
||||
// OLD!! handshakes.put(HandshakeHixie76.VERSION,new HandshakeHixie76());
|
||||
}
|
||||
|
||||
private final Queue<WebSocketSession> sessions = new ConcurrentLinkedQueue<>();
|
||||
|
@ -364,8 +363,6 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
|
|||
Executor executor = http.getConnector().getExecutor();
|
||||
ByteBufferPool bufferPool = http.getConnector().getByteBufferPool();
|
||||
WebSocketServerConnection connection = new WebSocketServerConnection(endp,executor,scheduler,driver.getPolicy(),bufferPool,this);
|
||||
// Tell jetty about the new connection
|
||||
request.setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE,connection);
|
||||
|
||||
LOG.debug("HttpConnection: {}",http);
|
||||
LOG.debug("AsyncWebSocketConnection: {}",connection);
|
||||
|
@ -383,6 +380,9 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
|
|||
// Connect extensions
|
||||
if (extensions != null)
|
||||
{
|
||||
connection.getParser().configureFromExtensions(extensions);
|
||||
connection.getGenerator().configureFromExtensions(extensions);
|
||||
|
||||
Iterator<Extension> extIter;
|
||||
// Connect outgoings
|
||||
extIter = extensions.iterator();
|
||||
|
@ -391,23 +391,6 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
|
|||
Extension ext = extIter.next();
|
||||
ext.setNextOutgoingFrames(outgoing);
|
||||
outgoing = ext;
|
||||
|
||||
// Handle RSV reservations
|
||||
if (ext.useRsv1())
|
||||
{
|
||||
connection.getGenerator().setRsv1InUse(true);
|
||||
connection.getParser().setRsv1InUse(true);
|
||||
}
|
||||
if (ext.useRsv2())
|
||||
{
|
||||
connection.getGenerator().setRsv2InUse(true);
|
||||
connection.getParser().setRsv2InUse(true);
|
||||
}
|
||||
if (ext.useRsv3())
|
||||
{
|
||||
connection.getGenerator().setRsv3InUse(true);
|
||||
connection.getParser().setRsv3InUse(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect incomings
|
||||
|
@ -426,6 +409,9 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
|
|||
// configure connection for incoming flows
|
||||
connection.getParser().setIncomingFramesHandler(incoming);
|
||||
|
||||
// Tell jetty about the new connection
|
||||
request.setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE,connection);
|
||||
|
||||
// Process (version specific) handshake response
|
||||
LOG.debug("Handshake Response: {}",handshaker);
|
||||
handshaker.doHandshakeResponse(request,response);
|
||||
|
|
|
@ -47,7 +47,7 @@ public class ChromeTest
|
|||
{
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpgradeWithWebkitDeflateExtension() throws Exception
|
||||
{
|
||||
|
@ -59,7 +59,7 @@ public class ChromeTest
|
|||
client.connect();
|
||||
client.sendStandardRequest();
|
||||
String response = client.expectUpgradeResponse();
|
||||
Assert.assertThat("Response", response, not(containsString("x-webkit-deflate-frame")));
|
||||
Assert.assertThat("Response",response,containsString("x-webkit-deflate-frame"));
|
||||
|
||||
// Generate text frame
|
||||
String msg = "this is an echo ... cho ... ho ... o";
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.eclipse.jetty.websocket.server;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import org.eclipse.jetty.websocket.server.blockhead.BlockheadClient;
|
||||
import org.eclipse.jetty.websocket.server.examples.MyEchoServlet;
|
||||
import org.junit.AfterClass;
|
||||
|
@ -25,9 +27,6 @@ import org.junit.Assert;
|
|||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
public class WebSocketInvalidVersionTest
|
||||
{
|
||||
private static SimpleServletServer server;
|
||||
|
@ -59,7 +58,7 @@ public class WebSocketInvalidVersionTest
|
|||
client.sendStandardRequest();
|
||||
String respHeader = client.readResponseHeader();
|
||||
Assert.assertThat("Response Code",respHeader,startsWith("HTTP/1.1 400 Unsupported websocket version specification"));
|
||||
Assert.assertThat("Response Header Versions",respHeader,containsString("Sec-WebSocket-Version: 13, 0\r\n"));
|
||||
Assert.assertThat("Response Header Versions",respHeader,containsString("Sec-WebSocket-Version: 13\r\n"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
|
||||
package org.eclipse.jetty.websocket.server.blockhead;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
@ -39,6 +42,7 @@ import java.util.concurrent.TimeoutException;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
|
@ -65,12 +69,6 @@ import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame;
|
|||
import org.eclipse.jetty.websocket.server.helper.IncomingFramesCapture;
|
||||
import org.junit.Assert;
|
||||
|
||||
import static org.hamcrest.Matchers.anyOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* A simple websocket client for performing unit tests with.
|
||||
* <p>
|
||||
|
@ -219,6 +217,9 @@ public class BlockheadClient implements IncomingFrames, OutgoingFrames
|
|||
// Connect extensions
|
||||
if (extensions != null)
|
||||
{
|
||||
generator.configureFromExtensions(extensions);
|
||||
parser.configureFromExtensions(extensions);
|
||||
|
||||
Iterator<Extension> extIter;
|
||||
// Connect outgoings
|
||||
extIter = extensions.iterator();
|
||||
|
@ -227,20 +228,6 @@ public class BlockheadClient implements IncomingFrames, OutgoingFrames
|
|||
Extension ext = extIter.next();
|
||||
ext.setNextOutgoingFrames(outgoing);
|
||||
outgoing = ext;
|
||||
|
||||
// Handle RSV reservations
|
||||
if (ext.useRsv1())
|
||||
{
|
||||
generator.setRsv1InUse(true);
|
||||
}
|
||||
if (ext.useRsv2())
|
||||
{
|
||||
generator.setRsv2InUse(true);
|
||||
}
|
||||
if (ext.useRsv3())
|
||||
{
|
||||
generator.setRsv3InUse(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect incomings
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.server.browser;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.ResourceHandler;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.websocket.core.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.core.api.UpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketCreator;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketHandler;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
|
||||
|
||||
/**
|
||||
* Tool to help debug websocket circumstances reported around browsers.
|
||||
* <p>
|
||||
* Provides a server, with a few simple websocket's that can be twiddled from a browser. This helps with setting up breakpoints and whatnot to help debug our
|
||||
* websocket implementation from the context of a browser client.
|
||||
*/
|
||||
public class BrowserDebugTool implements WebSocketCreator
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(BrowserDebugTool.class);
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int port = 8080;
|
||||
|
||||
for (int i = 0; i < args.length; i++)
|
||||
{
|
||||
String a = args[i];
|
||||
if ("-p".equals(a) || "--port".equals(a))
|
||||
{
|
||||
port = Integer.parseInt(args[++i]);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BrowserDebugTool tool = new BrowserDebugTool();
|
||||
tool.setupServer(port);
|
||||
tool.runForever();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
LOG.warn(t);
|
||||
}
|
||||
}
|
||||
|
||||
private Server server;
|
||||
|
||||
@Override
|
||||
public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
|
||||
{
|
||||
LOG.debug("Creating BrowserSocket");
|
||||
|
||||
if (req.getSubProtocols() != null)
|
||||
{
|
||||
if (!req.getSubProtocols().isEmpty())
|
||||
{
|
||||
String subProtocol = req.getSubProtocols().get(0);
|
||||
resp.setAcceptedSubProtocol(subProtocol);
|
||||
}
|
||||
}
|
||||
|
||||
String ua = req.getHeader("User-Agent");
|
||||
String rexts = req.getHeader("Sec-WebSocket-Extensions");
|
||||
BrowserSocket socket = new BrowserSocket(ua,rexts);
|
||||
return socket;
|
||||
}
|
||||
|
||||
private void runForever() throws Exception
|
||||
{
|
||||
server.start();
|
||||
LOG.info("Server available.");
|
||||
server.join();
|
||||
}
|
||||
|
||||
private void setupServer(int port)
|
||||
{
|
||||
server = new Server();
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setPort(port);
|
||||
|
||||
server.addConnector(connector);
|
||||
|
||||
WebSocketHandler wsHandler = new WebSocketHandler()
|
||||
{
|
||||
@Override
|
||||
public void configure(WebSocketServerFactory factory)
|
||||
{
|
||||
LOG.debug("Configuring WebSocketServerFactory ...");
|
||||
factory.setCreator(BrowserDebugTool.this);
|
||||
}
|
||||
};
|
||||
|
||||
server.setHandler(wsHandler);
|
||||
|
||||
String resourceBase = "src/test/resources/browser-debug-tool";
|
||||
|
||||
ResourceHandler rHandler = new ResourceHandler();
|
||||
rHandler.setDirectoriesListed(true);
|
||||
rHandler.setResourceBase(resourceBase);
|
||||
wsHandler.setHandler(rHandler);
|
||||
|
||||
LOG.info("{} setup on port {}",this.getClass().getName(),port);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.server.browser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
import org.eclipse.jetty.util.FutureCallback;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.websocket.core.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.core.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.core.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.core.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.core.api.WebSocketConnection;
|
||||
|
||||
@WebSocket
|
||||
public class BrowserSocket
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(BrowserSocket.class);
|
||||
private WebSocketConnection connection;
|
||||
private final String userAgent;
|
||||
private final String requestedExtensions;
|
||||
|
||||
public BrowserSocket(String ua, String reqExts)
|
||||
{
|
||||
this.userAgent = ua;
|
||||
this.requestedExtensions = reqExts;
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(WebSocketConnection conn)
|
||||
{
|
||||
this.connection = conn;
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onDisconnect(int statusCode, String reason)
|
||||
{
|
||||
this.connection = null;
|
||||
LOG.info("Closed [{}, {}]",statusCode,reason);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onTextMessage(String message)
|
||||
{
|
||||
LOG.info("onTextMessage({})",message);
|
||||
|
||||
int idx = message.indexOf(':');
|
||||
if (idx > 0)
|
||||
{
|
||||
String key = message.substring(0,idx).toLowerCase();
|
||||
String val = message.substring(idx + 1);
|
||||
switch (key)
|
||||
{
|
||||
case "info":
|
||||
{
|
||||
if (StringUtil.isBlank(userAgent))
|
||||
{
|
||||
writeMessage("Client has no User-Agent");
|
||||
}
|
||||
else
|
||||
{
|
||||
writeMessage("Client User-Agent: " + this.userAgent);
|
||||
}
|
||||
|
||||
if (StringUtil.isBlank(requestedExtensions))
|
||||
{
|
||||
writeMessage("Client requested no Sec-WebSocket-Extensions");
|
||||
}
|
||||
else
|
||||
{
|
||||
writeMessage("Client Sec-WebSocket-Extensions: " + this.requestedExtensions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "time":
|
||||
{
|
||||
Calendar now = Calendar.getInstance();
|
||||
DateFormat sdf = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.FULL,SimpleDateFormat.FULL);
|
||||
writeMessage("Server time: %s",sdf.format(now.getTime()));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
writeMessage("key[%s] val[%s]",key,val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// echo it
|
||||
writeMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMessage(String message)
|
||||
{
|
||||
if (this.connection == null)
|
||||
{
|
||||
LOG.debug("Not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.isOpen() == false)
|
||||
{
|
||||
LOG.debug("Not open");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
connection.write(null,new FutureCallback<String>(),message);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
LOG.info(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMessage(String format, Object... args)
|
||||
{
|
||||
writeMessage(String.format(format,args));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Jetty WebSocket Browser -> Server Debug Tool</title>
|
||||
<script type="text/javascript" src="websocket.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="main.css" media="all" >
|
||||
</head>
|
||||
<body>
|
||||
jetty websocket/browser/javascript -> server debug tool #console
|
||||
<div id="console"></div>
|
||||
<div id="buttons">
|
||||
<input id="connect" class="button" type="submit" name="connect" value="connect"/>
|
||||
<input id="close" class="button" type="submit" name="close" value="close" disabled="disabled"/>
|
||||
<input id="info" class="button" type="submit" name="info" value="info" disabled="disabled"/>
|
||||
<input id="time" class="button" type="submit" name="time" value="time" disabled="disabled"/>
|
||||
<input id="hello" class="button" type="submit" name="hello" value="hello" disabled="disabled"/>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$("connect").onclick = function(event) { wstool.connect(); return false; }
|
||||
$("close").onclick = function(event) {wstool.close(); return false; }
|
||||
$("info").onclick = function(event) {wstool.write("info:"); return false; }
|
||||
$("time").onclick = function(event) {wstool.write("time:"); return false; }
|
||||
$("hello").onclick = function(event) {wstool.write("Hello"); return false; }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,29 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
div {
|
||||
border: 0px solid black;
|
||||
}
|
||||
|
||||
div#console {
|
||||
clear: both;
|
||||
width: 40em;
|
||||
height: 20em;
|
||||
overflow: auto;
|
||||
background-color: #f0f0f0;
|
||||
padding: 4px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
div#console .info {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div#console .client {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
div#console .server {
|
||||
color: magenta;
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
if (!window.WebSocket && window.MozWebSocket) {
|
||||
window.WebSocket = window.MozWebSocket;
|
||||
}
|
||||
|
||||
if (!window.WebSocket) {
|
||||
alert("WebSocket not supported by this browser");
|
||||
}
|
||||
|
||||
function $() {
|
||||
return document.getElementById(arguments[0]);
|
||||
}
|
||||
function $F() {
|
||||
return document.getElementById(arguments[0]).value;
|
||||
}
|
||||
|
||||
function getKeyCode(ev) {
|
||||
if (window.event)
|
||||
return window.event.keyCode;
|
||||
return ev.keyCode;
|
||||
}
|
||||
|
||||
var wstool = {
|
||||
connect : function() {
|
||||
var location = document.location.toString().replace('http://', 'ws://')
|
||||
.replace('https://', 'wss://');
|
||||
|
||||
wstool.info("Document URI: " + document.location);
|
||||
wstool.info("WS URI: " + location);
|
||||
|
||||
try {
|
||||
this._ws = new WebSocket(location, "tool");
|
||||
this._ws.onopen = this._onopen;
|
||||
this._ws.onmessage = this._onmessage;
|
||||
this._ws.onclose = this._onclose;
|
||||
} catch (exception) {
|
||||
wstool.info("Connect Error: " + exception);
|
||||
}
|
||||
},
|
||||
|
||||
close : function() {
|
||||
this._ws.close();
|
||||
},
|
||||
|
||||
_out : function(css, message) {
|
||||
var console = $('console');
|
||||
var spanText = document.createElement('span');
|
||||
spanText.className = 'text ' + css;
|
||||
spanText.innerHTML = message;
|
||||
var lineBreak = document.createElement('br');
|
||||
console.appendChild(spanText);
|
||||
console.appendChild(lineBreak);
|
||||
console.scrollTop = console.scrollHeight - console.clientHeight;
|
||||
},
|
||||
|
||||
info : function(message) {
|
||||
wstool._out("info", message);
|
||||
},
|
||||
|
||||
infoc : function(message) {
|
||||
wstool._out("client", "[c] " + message);
|
||||
},
|
||||
|
||||
infos : function(message) {
|
||||
wstool._out("server", "[s] " + message);
|
||||
},
|
||||
|
||||
setState : function(enabled) {
|
||||
$('connect').disabled = enabled;
|
||||
$('close').disabled = !enabled;
|
||||
$('info').disabled = !enabled;
|
||||
$('time').disabled = !enabled;
|
||||
$('hello').disabled = !enabled;
|
||||
},
|
||||
|
||||
_onopen : function() {
|
||||
wstool.setState(true);
|
||||
wstool.info("Websocket Connected");
|
||||
},
|
||||
|
||||
_send : function(message) {
|
||||
if (this._ws) {
|
||||
this._ws.send(message);
|
||||
wstool.infoc(message);
|
||||
}
|
||||
},
|
||||
|
||||
write : function(text) {
|
||||
wstool._send(text);
|
||||
},
|
||||
|
||||
_onmessage : function(m) {
|
||||
if (m.data) {
|
||||
wstool.infos(m.data);
|
||||
}
|
||||
},
|
||||
|
||||
_onclose : function(closeEvent) {
|
||||
this._ws = null;
|
||||
wstool.setState(false);
|
||||
wstool.info("Websocket Closed");
|
||||
wstool.info(" .wasClean = " + closeEvent.wasClean);
|
||||
|
||||
var codeMap = {};
|
||||
codeMap[1000] = "(NORMAL)";
|
||||
codeMap[1001] = "(ENDPOINT_GOING_AWAY)";
|
||||
codeMap[1002] = "(PROTOCOL_ERROR)";
|
||||
codeMap[1003] = "(UNSUPPORTED_DATA)";
|
||||
codeMap[1004] = "(UNUSED/RESERVED)";
|
||||
codeMap[1005] = "(INTERNAL/NO_CODE_PRESENT)";
|
||||
codeMap[1006] = "(INTERNAL/ABNORMAL_CLOSE)";
|
||||
codeMap[1007] = "(BAD_DATA)";
|
||||
codeMap[1008] = "(POLICY_VIOLATION)";
|
||||
codeMap[1009] = "(MESSAGE_TOO_BIG)";
|
||||
codeMap[1010] = "(HANDSHAKE/EXT_FAILURE)";
|
||||
codeMap[1011] = "(SERVER/UNEXPECTED_CONDITION)";
|
||||
codeMap[1015] = "(INTERNAL/TLS_ERROR)";
|
||||
var codeStr = codeMap[closeEvent.code];
|
||||
wstool.info(" .code = " + closeEvent.code + " " + codeStr);
|
||||
wstool.info(" .reason = " + closeEvent.reason);
|
||||
}
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
org.eclipse.jetty.LEVEL=WARN
|
||||
# org.eclipse.jetty.LEVEL=INFO
|
||||
# org.eclipse.jetty.websocket.LEVEL=WARN
|
||||
# org.eclipse.jetty.websocket.LEVEL=DEBUG
|
||||
# org.eclipse.jetty.websocket.io.LEVEL=DEBUG
|
||||
|
@ -17,6 +18,9 @@ org.eclipse.jetty.LEVEL=WARN
|
|||
### See the read/write traffic
|
||||
# org.eclipse.jetty.websocket.io.Frames.LEVEL=DEBUG
|
||||
|
||||
### Show state changes on BrowserDebugTool
|
||||
org.eclipse.jetty.websocket.server.browser.LEVEL=DEBUG
|
||||
|
||||
### Disabling intentional error out of RFCSocket
|
||||
org.eclipse.jetty.websocket.server.helper.RFCSocket.LEVEL=OFF
|
||||
### Disable stacks on FrameBytes.failed()
|
||||
|
|
Loading…
Reference in New Issue