430418 - Jetty 9.1.3 and Chrome 33 permessage-deflate do not work together

+ Updating Compress/PerMessageDeflate extensions for latest spec
  document, http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18
  Intra-frame tail 0000FFFF is now being preserved for
  permessage-deflate
This commit is contained in:
Joakim Erdfelt 2014-05-14 13:00:30 -07:00
parent a2b6c69525
commit e0e00b0aed
7 changed files with 142 additions and 23 deletions

View File

@ -43,15 +43,36 @@ public abstract class CompressExtension extends AbstractExtension
protected static final byte[] TAIL_BYTES = new byte[]{0x00, 0x00, (byte)0xFF, (byte)0xFF};
private static final Logger LOG = Log.getLogger(CompressExtension.class);
/** Never drop tail bytes 0000FFFF, from any frame type */
protected static final int TAIL_DROP_NEVER = 0;
/** Always drop tail bytes 0000FFFF, from all frame types */
protected static final int TAIL_DROP_ALWAYS = 1;
/** Only drop tail bytes 0000FFFF, from fin==true frames */
protected static final int TAIL_DROP_FIN_ONLY = 2;
/** Always set RSV flag, on all frame types */
protected static final int RSV_USE_ALWAYS = 0;
/**
* Only set RSV flag on first frame in multi-frame messages.
* <p>
* Note: this automatically means no-continuation frames have
* the RSV bit set
*/
protected static final int RSV_USE_ONLY_FIRST = 1;
private final Queue<FrameEntry> entries = new ConcurrentArrayQueue<>();
private final IteratingCallback flusher = new Flusher();
private final Deflater compressor;
private final Inflater decompressor;
private int tailDrop = TAIL_DROP_NEVER;
private int rsvUse = RSV_USE_ALWAYS;
protected CompressExtension()
{
compressor = new Deflater(Deflater.BEST_COMPRESSION, true);
decompressor = new Inflater(true);
tailDrop = getTailDropMode();
rsvUse = getRsvUseMode();
}
public Deflater getDeflater()
@ -73,6 +94,20 @@ public abstract class CompressExtension extends AbstractExtension
return true;
}
/**
* Return the mode of operation for dropping (or keeping) tail bytes in frames generated by compress (outgoing)
*
* @return either {@link #TAIL_DROP_ALWAYS}, {@link #TAIL_DROP_FIN_ONLY}, or {@link #TAIL_DROP_NEVER}
*/
abstract int getTailDropMode();
/**
* Return the mode of operation for RSV flag use in frames generate by compress (outgoing)
*
* @return either {@link #RSV_USE_ALWAYS} or {@link #RSV_USE_ONLY_FIRST}
*/
abstract int getRsvUseMode();
protected void forwardIncoming(Frame frame, ByteAccumulator accumulator)
{
DataFrame newFrame = new DataFrame(frame);
@ -301,15 +336,31 @@ public abstract class CompressExtension extends AbstractExtension
}
}
// Skip the last tail bytes bytes generated by SYNC_FLUSH.
boolean fin = frame.isFin() && finished;
// Handle tail bytes generated by SYNC_FLUSH.
if(tailDrop == TAIL_DROP_ALWAYS) {
payload = ByteBuffer.wrap(output, 0, outputLength - TAIL_BYTES.length);
LOG.debug("Compressed {}: {}->{} chunk bytes", entry, inputLength, outputLength);
} else if(tailDrop == TAIL_DROP_FIN_ONLY) {
payload = ByteBuffer.wrap(output, 0, outputLength - (fin?TAIL_BYTES.length:0));
} else {
// always include
payload = ByteBuffer.wrap(output, 0, outputLength);
}
if (LOG.isDebugEnabled())
{
LOG.debug("Compressed {}: {}->{} chunk bytes",entry,inputLength,outputLength);
}
boolean continuation = frame.getType().isContinuation() || !first;
DataFrame chunk = new DataFrame(frame, continuation);
if(rsvUse == RSV_USE_ONLY_FIRST) {
chunk.setRsv1(!continuation);
} else {
// always set
chunk.setRsv1(true);
}
chunk.setPayload(payload);
boolean fin = frame.isFin() && finished;
chunk.setFin(fin);
nextOutgoingFrame(chunk, this, entry.batchMode);

View File

@ -36,6 +36,18 @@ public class DeflateFrameExtension extends CompressExtension
return "deflate-frame";
}
@Override
int getRsvUseMode()
{
return RSV_USE_ALWAYS;
}
@Override
int getTailDropMode()
{
return TAIL_DROP_ALWAYS;
}
@Override
public void incomingFrame(Frame frame)
{

View File

@ -103,6 +103,18 @@ public class PerMessageDeflateExtension extends CompressExtension
super.nextOutgoingFrame(frame, callback, batchMode);
}
@Override
int getRsvUseMode()
{
return RSV_USE_ONLY_FIRST;
}
@Override
int getTailDropMode()
{
return TAIL_DROP_FIN_ONLY;
}
@Override
public void setConfig(final ExtensionConfig config)
{

View File

@ -23,6 +23,7 @@ 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.common.extensions.compress.PerMessageDeflateExtension;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
@ -84,20 +85,14 @@ public class BrowserDebugTool implements WebSocketCreator
String ua = req.getHeader("User-Agent");
String rexts = req.getHeader("Sec-WebSocket-Extensions");
LOG.debug("User-Agent: {}", ua);
LOG.debug("Sec-WebSocket-Extensions (Request) : {}", rexts);
LOG.debug("User-Agent: {}",ua);
LOG.debug("Sec-WebSocket-Extensions (Request) : {}",rexts);
return new BrowserSocket(ua,rexts);
}
public void start() throws Exception
public int getPort()
{
server.start();
LOG.info("Server available on port {}", getPort());
}
public void stop() throws Exception
{
server.stop();
return connector.getLocalPort();
}
public void prepare(int port)
@ -116,6 +111,7 @@ public class BrowserDebugTool implements WebSocketCreator
// factory.getExtensionFactory().unregister("deflate-frame");
// factory.getExtensionFactory().unregister("permessage-deflate");
factory.getExtensionFactory().register("permessage-deflate",PerMessageDeflateExtension.class);
// factory.getExtensionFactory().unregister("x-webkit-deflate-frame");
// Setup the desired Socket to use for all incoming upgrade requests
@ -123,6 +119,9 @@ public class BrowserDebugTool implements WebSocketCreator
// Set the timeout
factory.getPolicy().setIdleTimeout(30000);
// Set top end message size
factory.getPolicy().setMaxTextMessageSize(15 * 1024 * 1024);
}
};
@ -138,8 +137,14 @@ public class BrowserDebugTool implements WebSocketCreator
LOG.info("{} setup on port {}",this.getClass().getName(),port);
}
public int getPort()
public void start() throws Exception
{
return connector.getLocalPort();
server.start();
LOG.info("Server available on port {}",getPort());
}
public void stop() throws Exception
{
server.stop();
}
}

View File

@ -108,8 +108,16 @@ public class BrowserSocket
@OnWebSocketMessage
public void onTextMessage(String message)
{
if (message.length() > 300)
{
int len = message.length();
LOG.info("onTextMessage({} ... {}) size:{}",message.substring(0,15),message.substring(len - 15,len).replaceAll("[\r\n]*",""),len);
}
else
{
LOG.info("onTextMessage({})",message);
}
int idx = message.indexOf(':');
if (idx > 0)

View File

@ -17,6 +17,10 @@
<input id="hello" class="button" type="submit" name="hello" value="hello" disabled="disabled"/>
<input id="there" class="button" type="submit" name="there" value="there" disabled="disabled"/>
<input id="json" class="button" type="submit" name="json" value="json" disabled="disabled"/>
<input id="send10k" class="button" type="submit" name="send10k" value="send10k" disabled="disabled"/>
<input id="send100k" class="button" type="submit" name="send100k" value="send100k" disabled="disabled"/>
<input id="send1000k" class="button" type="submit" name="send1000k" value="send1000k" disabled="disabled"/>
<input id="send10m" class="button" type="submit" name="send10m" value="send10m" disabled="disabled"/>
</div>
<script type="text/javascript">
$("connect").onclick = function(event) { wstool.connect(); return false; }
@ -32,6 +36,21 @@
+ " 12 Sep 2013 19:42:30 GMT\"},{\"channel\":\"/chat/demo\",\"data\":{\"user\":\"ch\",\"membership\":\"join\",\"chat\":\"ch"
+ " has joined\"},\"id\":\"4\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ " 12 Sep 2013 19:42:30 GMT\"}]"); return false; }
$("send10k").onclick = function(event) {wstool.write(randomString( 10*1024)); return false;}
$("send100k").onclick = function(event) {wstool.write(randomString( 100*1024)); return false;}
$("send1000k").onclick = function(event) {wstool.write(randomString(1000*1024)); return false;}
$("send10m").onclick = function(event) {wstool.write(randomString( 10*1024*1024)); return false;}
function randomString(len, charSet) {
charSet = charSet || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{} ":;<>,.()[]';
var randomString = '';
var charLen = charSet.length;
for (var i = 0; i < len; i++) {
var randomPoz = Math.floor(Math.random() * charLen);
randomString += charSet.substring(randomPoz,randomPoz+1);
}
return randomString;
}
</script>
</body>
</html>

View File

@ -59,12 +59,20 @@ var wstool = {
},
infoc : function(message) {
if(message.length > 300) {
wstool._out("client", "[c] [big message: " + message.length + " characters]");
} else {
wstool._out("client", "[c] " + message);
}
},
infos : function(message) {
this._scount++;
if(message.length > 300) {
wstool._out("server", "[s" + this._scount + "] [big message: " + message.length + " characters]");
} else {
wstool._out("server", "[s" + this._scount + "] " + message);
}
},
setState : function(enabled) {
@ -77,6 +85,10 @@ var wstool = {
$('hello').disabled = !enabled;
$('there').disabled = !enabled;
$('json').disabled = !enabled;
$('send10k').disabled = !enabled;
$('send100k').disabled = !enabled;
$('send1000k').disabled = !enabled;
$('send10m').disabled = !enabled;
},
_onopen : function() {