blob: 9d73f1f72e2162d8c461ddc4948386a295c3fa2e [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.extensions.compress;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BadPayloadException;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.OpCode;
/**
* Per Message Deflate Compression extension for WebSocket.
* <p>
* Attempts to follow <a href="https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-12">draft-ietf-hybi-permessage-compression-12</a>
*/
public class PerMessageDeflateExtension extends CompressExtension
{
private static final Logger LOG = Log.getLogger(PerMessageDeflateExtension.class);
private ExtensionConfig configRequested;
private ExtensionConfig configNegotiated;
private boolean incomingContextTakeover = true;
private boolean outgoingContextTakeover = true;
private boolean incomingCompressed;
@Override
public String getName()
{
return "permessage-deflate";
}
@Override
public void incomingFrame(Frame frame)
{
// Incoming frames are always non concurrent because
// they are read and parsed with a single thread, and
// therefore there is no need for synchronization.
// This extension requires the RSV1 bit set only in the first frame.
// Subsequent continuation frames don't have RSV1 set, but are compressed.
if (frame.getType().isData())
{
incomingCompressed = frame.isRsv1();
}
if (OpCode.isControlFrame(frame.getOpCode()) || !incomingCompressed)
{
nextIncomingFrame(frame);
return;
}
ByteAccumulator accumulator = newByteAccumulator();
try
{
ByteBuffer payload = frame.getPayload();
decompress(accumulator, payload);
if (frame.isFin())
{
decompress(accumulator, TAIL_BYTES_BUF.slice());
}
forwardIncoming(frame, accumulator);
}
catch (DataFormatException e)
{
throw new BadPayloadException(e);
}
if (frame.isFin())
incomingCompressed = false;
}
@Override
protected void nextIncomingFrame(Frame frame)
{
if (frame.isFin() && !incomingContextTakeover)
{
LOG.debug("Incoming Context Reset");
decompressCount.set(0);
getInflater().reset();
}
super.nextIncomingFrame(frame);
}
@Override
protected void nextOutgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
{
if (frame.isFin() && !outgoingContextTakeover)
{
LOG.debug("Outgoing Context Reset");
getDeflater().reset();
}
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)
{
configRequested = new ExtensionConfig(config);
configNegotiated = new ExtensionConfig(config.getName());
for (String key : config.getParameterKeys())
{
key = key.trim();
switch (key)
{
case "client_max_window_bits":
case "server_max_window_bits":
{
// Not supported by Jetty
// Don't negotiate these parameters
break;
}
case "client_no_context_takeover":
{
configNegotiated.setParameter("client_no_context_takeover");
switch (getPolicy().getBehavior())
{
case CLIENT:
incomingContextTakeover = false;
break;
case SERVER:
outgoingContextTakeover = false;
break;
}
break;
}
case "server_no_context_takeover":
{
configNegotiated.setParameter("server_no_context_takeover");
switch (getPolicy().getBehavior())
{
case CLIENT:
outgoingContextTakeover = false;
break;
case SERVER:
incomingContextTakeover = false;
break;
}
break;
}
default:
{
throw new IllegalArgumentException();
}
}
}
LOG.debug("config: outgoingContextTakover={}, incomingContextTakeover={} : {}", outgoingContextTakeover, incomingContextTakeover, this);
super.setConfig(configNegotiated);
}
@Override
public String toString()
{
return String.format("%s[requested=\"%s\", negotiated=\"%s\"]",
getClass().getSimpleName(),
configRequested.getParameterizedName(),
configNegotiated.getParameterizedName());
}
}