com.neovisionaries.ws.client.PerMessageDeflateExtension Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nv-websocket-client Show documentation
Show all versions of nv-websocket-client Show documentation
WebSocket client implementation in Java.
/*
* Copyright (C) 2015-2016 Neo Visionaries Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.neovisionaries.ws.client;
import java.util.Map;
/**
* Per-Message Deflate Extension (7. The "permessage-deflate" Extension in
* RFC 7692).
*
* @see 7. The "permessage-deflate" Extension in RFC 7692
*/
class PerMessageDeflateExtension extends PerMessageCompressionExtension
{
private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits";
private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits";
private static final byte[] COMPRESSION_TERMINATOR = { (byte)0x00, (byte)0x00, (byte)0xFF, (byte)0xFF };
private static final int MIN_BITS = 8;
private static final int MAX_BITS = 15;
private static final int MIN_WINDOW_SIZE = 256;
private static final int MAX_WINDOW_SIZE = 32768;
private static final int INCOMING_SLIDING_WINDOW_MARGIN = 1024;
private boolean mServerNoContextTakeover;
private boolean mClientNoContextTakeover;
private int mServerWindowSize = MAX_WINDOW_SIZE;
private int mClientWindowSize = MAX_WINDOW_SIZE;
private int mIncomingSlidingWindowBufferSize;
private ByteArray mIncomingSlidingWindow;
public PerMessageDeflateExtension()
{
super(WebSocketExtension.PERMESSAGE_DEFLATE);
}
public PerMessageDeflateExtension(String name)
{
super(name);
}
@Override
void validate() throws WebSocketException
{
// For each parameter
for (Map.Entry entry : getParameters().entrySet())
{
validateParameter(entry.getKey(), entry.getValue());
}
mIncomingSlidingWindowBufferSize = mServerWindowSize + INCOMING_SLIDING_WINDOW_MARGIN;
}
public boolean isServerNoContextTakeover()
{
return mServerNoContextTakeover;
}
public boolean isClientNoContextTakeover()
{
return mClientNoContextTakeover;
}
public int getServerWindowSize()
{
return mServerWindowSize;
}
public int getClientWindowSize()
{
return mClientWindowSize;
}
private void validateParameter(String key, String value) throws WebSocketException
{
if (SERVER_NO_CONTEXT_TAKEOVER.equals(key))
{
mServerNoContextTakeover = true;
}
else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(key))
{
mClientNoContextTakeover = true;
}
else if (SERVER_MAX_WINDOW_BITS.equals(key))
{
mServerWindowSize = computeWindowSize(key, value);
}
else if (CLIENT_MAX_WINDOW_BITS.equals(key))
{
mClientWindowSize = computeWindowSize(key, value);
}
else
{
// permessage-deflate extension contains an unsupported parameter.
throw new WebSocketException(
WebSocketError.PERMESSAGE_DEFLATE_UNSUPPORTED_PARAMETER,
"permessage-deflate extension contains an unsupported parameter: " + key);
}
}
private int computeWindowSize(String key, String value) throws WebSocketException
{
int bits = extractMaxWindowBits(key, value);
int size = MIN_WINDOW_SIZE;
for (int i = MIN_BITS; i < bits; ++i)
{
size *= 2;
}
return size;
}
private int extractMaxWindowBits(String key, String value) throws WebSocketException
{
int bits = parseMaxWindowBits(value);
if (bits < 0)
{
String message = String.format(
"The value of %s parameter of permessage-deflate extension is invalid: %s",
key, value);
throw new WebSocketException(
WebSocketError.PERMESSAGE_DEFLATE_INVALID_MAX_WINDOW_BITS, message);
}
return bits;
}
private int parseMaxWindowBits(String value)
{
if (value == null)
{
return -1;
}
int bits;
try
{
bits = Integer.parseInt(value);
}
catch (NumberFormatException e)
{
return -1;
}
if (bits < MIN_BITS || MAX_BITS < bits)
{
return -1;
}
return bits;
}
@Override
protected byte[] decompress(byte[] compressed) throws WebSocketException
{
// Append 0x00, 0x00, 0xFF and 0xFF.
//
// From RFC 7692, 7.2.2. Decompression
//
// An endpoint uses the following algorithm to decompress a message.
//
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of
// the payload of the message.
//
// 2. Decompress the resulting data using DEFLATE.
//
//
// From RFC 1979, 2.1. Packet Format, Data, The 3rd paragraph:
//
// The basic format of the compressed data is precisely described by
// the 'Deflate' Compressed Data Format Specification[3]. Each
// transmitted packet must begin at a 'deflate' block boundary, to
// ensure synchronization when incompressible data resets the
// transmitter's state; to ensure this, each transmitted packet must
// be terminated with a zero-length 'deflate' non-compressed block
// (BTYPE of 00). This means that the last four bytes of the
// compressed format must be 0x00 0x00 0xFF 0xFF. These bytes MUST
// be removed before transmission; the receiver can reinsert them if
// required by the implementation.
//
int inputLen = compressed.length + COMPRESSION_TERMINATOR.length;
// Wrap the compressed byte array with ByteArray.
ByteArray input = new ByteArray(inputLen);
input.put(compressed);
input.put(COMPRESSION_TERMINATOR);
if (mIncomingSlidingWindow == null)
{
mIncomingSlidingWindow = new ByteArray(mIncomingSlidingWindowBufferSize);
}
// The size of the sliding window before decompression.
int outPos = mIncomingSlidingWindow.length();
try
{
// Decompress.
DeflateDecompressor.decompress(input, mIncomingSlidingWindow);
}
catch (Exception e)
{
// Failed to decompress the message.
throw new WebSocketException(
WebSocketError.DECOMPRESSION_ERROR,
String.format("Failed to decompress the message: %s", e.getMessage()), e);
}
byte[] output = mIncomingSlidingWindow.toBytes(outPos);
// Shrink the size of the incoming sliding window.
mIncomingSlidingWindow.shrink(mIncomingSlidingWindowBufferSize);
if (mServerNoContextTakeover)
{
// No need to remember the message for the next decompression.
mIncomingSlidingWindow.clear();
}
return output;
}
@Override
protected byte[] compress(byte[] plain) throws WebSocketException
{
if (canCompress(plain) == false)
{
// Compression should not be performed.
return plain;
}
// From RFC 7692, 7.2.1. Compression
//
// An endpoint uses the following algorithm to compress a message.
//
// 1. Compress all the octets of the payload of the message using
// DEFLATE.
//
// 2. If the resulting data does not end with an empty DEFLATE block
// with no compression (the "BTYPE" bits are set to 00), append an
// empty DEFLATE block with no compression to the tail end.
//
// 3. Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end.
// After this step, the last octet of the compressed data contains
// (possibly part of) the DEFLATE header bits with the "BTYPE" bits
// set to 00.
try
{
// Compress.
byte[] compressed = DeflateCompressor.compress(plain);
// Adjust the compressed data to comply with RFC 7692.
return adjustCompressedData(compressed);
}
catch (Exception e)
{
// Failed to compress the message.
throw new WebSocketException(
WebSocketError.COMPRESSION_ERROR,
String.format("Failed to compress the message: %s", e.getMessage()), e);
}
}
private boolean canCompress(byte[] plain)
{
// The current compression implementation (DeflateCompressor)
// cannot control the size of the internal sliding window on
// the client side.
//
// Therefore, compression should not be performed if there is
// a possibility that Huffman codes in compressed data may
// refer to bigger distances than the agreed sliding window
// size (which is computed based on client_max_window_bits).
//
// From RFC 7692, 7.2.1. Compression
//
// If the "agreed parameters" contain the "client_max_window_bits"
// extension parameter with a value of w, the client MUST NOT use
// an LZ77 sliding window longer than the w-th power of 2 bytes
// to compress messages to send.
//
// If the agreed sliding window size is the maximum value allowed
// by the DEFLATE specification, the size of the internal sliding
// window of the compressor does not have to be cared about.
if (mClientWindowSize == MAX_WINDOW_SIZE)
{
// Can be compressed.
return true;
}
// Otherwise, considering the fact that the current implementation
// does not support context takeover on the client side, it can be
// said that Huffman codes in compressed data will not refer to
// bigger distances than the agreed sliding window size if the size
// of the original plain data is less than the agreed sliding window
// size.
if (plain.length < mClientWindowSize)
{
// Can be compressed.
return true;
}
// Cannot exclude the possibility that Huffman codes in compressed
// data may refer to bigger distances than the agreed sliding window
// size. Therefore, compression should not be performed.
return false;
}
private static byte[] adjustCompressedData(byte[] compressed) throws FormatException
{
// Wrap the compressed data with ByteArray. '+1' here is for 3 bits,
// '000', that may be appended at the bottom of this method.
ByteArray data = new ByteArray(compressed.length + 1);
data.put(compressed);
// The data is compressed on a bit basis, so use a bit index.
int[] bitIndex = new int[1];
// The flag to indicate whether the last block in the original
// compressed data is an empty block with no compression.
boolean[] hasEmptyBlock = new boolean[1];
// Skip all blocks one by one until the end.
// skipBlock() returns false if no more block exists.
while (skipBlock(data, bitIndex, hasEmptyBlock));
// If the last block is an empty block with no compression.
if (hasEmptyBlock[0])
{
// In this case, it is enough to drop the last four bytes
// (0x00 0x00 0xFF 0xFF).
return data.toBytes(0, ((bitIndex[0] - 1) / 8) + 1 - 4);
}
// Append 3 bits, '000'.
//
// The first bit is BFINAL and its value is '0'. Note that '1'
// is not used here although the block being appended is the
// last block. It's because some server-side implementations
// fail to inflate compressed data with BFINAL=1.
//
// The second and the third bits are '00' and it means NO
// COMPRESSION.
appendEmptyBlock(data, bitIndex);
// Convert the ByteArray to byte[].
return data.toBytes(0, ((bitIndex[0] - 1) / 8) + 1);
}
private static void appendEmptyBlock(ByteArray data, int[] bitIndex)
{
int shift = bitIndex[0] % 8;
// ? = used (0 or 1), x = unused (= 0).
//
// | Current | After 3 bits are appended
// ----------+----------+---------------------------
// shift = 1 | xxxxxxx? | xxxx000?
// shift = 2 | xxxxxx?? | xxx000??
// shift = 3 | xxxxx??? | xx000???
// shift = 4 | xxxx???? | x000????
// shift = 5 | xxx????? | 000?????
// shift = 6 | xx?????? | 00?????? xxxxxxx0
// shift = 7 | x??????? | 0??????? xxxxxx00
// shift = 0 | ???????? | ???????? xxxxx000
switch (shift)
{
case 6:
case 7:
case 0:
data.put(0);
}
// Update the bit index for the 3 bits.
bitIndex[0] += 3;
}
private static boolean skipBlock(
ByteArray input, int[] bitIndex, boolean[] hasEmptyBlock) throws FormatException
{
// Each block has a block header which consists of 3 bits.
// See 3.2.3. of RFC 1951.
// The first bit indicates whether the block is the last one or not.
boolean last = input.readBit(bitIndex);
if (last)
{
// Clear the BFINAL bit because some server-side implementations
// fail to inflate compressed data with BFINAL=1.
input.clearBit(bitIndex[0] - 1);
}
// The combination of the second and the third bits indicate the
// compression type of the block. Compression types are as follows:
//
// 00: No compression.
// 01: Compressed with fixed Huffman codes
// 10: Compressed with dynamic Huffman codes
// 11: Reserved (error)
//
int type = input.readBits(bitIndex, 2);
// This flag becomes true if skipPlainBlock() is called and it returns 0.
boolean plain0 = false;
switch (type)
{
// No compression
case 0:
// Skip the plain block. skipPlainBlock() returns the data length.
plain0 = (skipPlainBlock(input, bitIndex) == 0);
break;
// Compressed with fixed Huffman codes
case 1:
skipFixedBlock(input, bitIndex);
break;
// Compressed with dynamic Huffman codes
case 2:
skipDynamicBlock(input, bitIndex);
break;
// Bad format
default:
// Bad compression type at the bit index.
String message = String.format(
"[%s] Bad compression type '11' at the bit index '%d'.",
PerMessageDeflateExtension.class.getSimpleName(), bitIndex[0]);
throw new FormatException(message);
}
// If no more data are available.
if (input.length() <= (bitIndex[0] / 8))
{
// Last even if the BFINAL bit is false.
last = true;
}
if (last && plain0)
{
// The last block is an empty block with no compression.
hasEmptyBlock[0] = true;
}
// Return true if this block is not the last one.
return !last;
}
private static int skipPlainBlock(ByteArray input, int[] bitIndex)
{
// 3.2.4 Non-compressed blocks (BTYPE=00)
// Skip any remaining bits in current partially processed byte.
int bi = (bitIndex[0] + 7) & ~7;
// Data copy is performed on a byte basis, so convert the bit index
// to a byte index.
int index = bi / 8;
// LEN: 2 bytes. The data length.
int len = (input.get(index) & 0xFF) + (input.get(index + 1) & 0xFF) * 256;
// NLEN: 2 bytes. The one's complement of LEN.
// Skip LEN and NLEN.
index += 4;
// Make the bitIndex point to the bit next to
// the end of the copied data.
bitIndex[0] = (index + len) * 8;
return len;
}
private static void skipFixedBlock(ByteArray input, int[] bitIndex) throws FormatException
{
// 3.2.6 Compression with fixed Huffman codes (BTYPE=01)
// Inflate the compressed data using the pre-defined
// conversion tables. The specification says in 3.2.2
// as follows.
//
// The only differences between the two compressed
// cases is how the Huffman codes for the literal/
// length and distance alphabets are defined.
//
// The "two compressed cases" in the above sentence are
// "fixed Huffman codes" and "dynamic Huffman codes".
skipData(input, bitIndex,
FixedLiteralLengthHuffman.getInstance(),
FixedDistanceHuffman.getInstance());
}
private static void skipDynamicBlock(ByteArray input, int[] bitIndex) throws FormatException
{
// 3.2.7 Compression with dynamic Huffman codes (BTYPE=10)
// Read 2 tables. One is a table to convert "code value of literal/length
// alphabet" into "literal/length symbol". The other is a table to convert
// "code value of distance alphabet" into "distance symbol".
Huffman[] tables = new Huffman[2];
DeflateUtil.readDynamicTables(input, bitIndex, tables);
skipData(input, bitIndex, tables[0], tables[1]);
}
private static void skipData(
ByteArray input, int[] bitIndex,
Huffman literalLengthHuffman, Huffman distanceHuffman) throws FormatException
{
// 3.2.5 Compressed blocks (length and distance codes)
while (true)
{
// Read a literal/length symbol from the input.
int literalLength = literalLengthHuffman.readSym(input, bitIndex);
// Symbol value '256' indicates the end.
if (literalLength == 256)
{
// End of this data.
break;
}
// Symbol values from 0 to 255 represent literal values.
if (0 <= literalLength && literalLength <= 255)
{
// Output as is.
continue;
}
// Symbol values from 257 to 285 represent pairs.
// Depending on symbol values, some extra bits in the input may be
// consumed to compute the length.
DeflateUtil.readLength(input, bitIndex, literalLength);
// Read the distance from the input.
DeflateUtil.readDistance(input, bitIndex, distanceHuffman);
}
}
}