diff --git a/src/main/java/org/java_websocket/extensions/permessage_deflate/WebSocketPerMessageDeflateExtension.java b/src/main/java/org/java_websocket/extensions/permessage_deflate/WebSocketPerMessageDeflateExtension.java new file mode 100644 index 00000000..a73ff478 --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/permessage_deflate/WebSocketPerMessageDeflateExtension.java @@ -0,0 +1,574 @@ +/* + * TooTallNate - Java-WebSocket + * + * MIT License + * + * Copyright (C) 2025 Robert Schlabbach + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.java_websocket.extensions.permessage_deflate; + +import static java.util.zip.Deflater.DEFAULT_COMPRESSION; +import static java.util.zip.Deflater.NO_FLUSH; +import static java.util.zip.Deflater.SYNC_FLUSH; +import static org.java_websocket.extensions.ExtensionRequestData.parseExtensionRequest; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.extensions.CompressionExtension; +import org.java_websocket.extensions.ExtensionRequestData; +import org.java_websocket.extensions.IExtension; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.ContinuousFrame; +import org.java_websocket.framing.DataFrame; +import org.java_websocket.framing.Framedata; + +/** RFC 7692 WebSocket Per-Message Deflate Extension implementation */ +public class WebSocketPerMessageDeflateExtension extends CompressionExtension { + // RFC 7692 extension common name and identifier + public static final String EXTENSION_COMMON_NAME = "WebSocket Per-Message Deflate"; + public static final String EXTENSION_IDENTIFIER = "permessage-deflate"; + + // RFC 7692 extension parameters + public static final String PARAMETER_CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover"; + public static final String PARAMETER_SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover"; + public static final String PARAMETER_CLIENT_MAX_WINDOW_BITS = "client_max_window_bits"; + public static final int MINIMUM_CLIENT_MAX_WINDOW_BITS = 8; + public static final int MAXIMUM_CLIENT_MAX_WINDOW_BITS = 15; + public static final String PARAMETER_SERVER_MAX_WINDOW_BITS = "server_max_window_bits"; + public static final int MINIMUM_SERVER_MAX_WINDOW_BITS = 8; + public static final int MAXIMUM_SERVER_MAX_WINDOW_BITS = 15; + + // RFC 7692 extension parameter defaults + public static boolean DEFAULT_CLIENT_NO_CONTEXT_TAKEOVER = false; + public static boolean DEFAULT_SERVER_NO_CONTEXT_TAKEOVER = false; + public static int DEFAULT_CLIENT_MAX_WINDOW_BITS = MAXIMUM_CLIENT_MAX_WINDOW_BITS; + public static int DEFAULT_SERVER_MAX_WINDOW_BITS = MAXIMUM_SERVER_MAX_WINDOW_BITS; + public static int DEFAULT_COMPRESSION_THRESHOLD = 64; + + // RFC 7692 tail end to be removed from compressed data and appended when decompressing + public static final byte[] EMPTY_DEFLATE_BLOCK = + new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff}; + + // RFC 7692 empty uncompressed DEFLATE block to be used when out of uncompressed data + public static final byte[] EMPTY_UNCOMPRESSED_DEFLATE_BLOCK = new byte[] {0x00}; + + private static final int TRANSFER_CHUNK_SIZE = 8192; + + private final int compressionLevel; + private final int maxFrameSize; + private final Deflater compressor; + private final Inflater decompressor; + + private int compressionThreshold; + private boolean clientNoContextTakeover; + private boolean serverNoContextTakeover; + private int clientMaxWindowBits; + private int serverMaxWindowBits; + + private boolean isCompressorResetRequired; + private boolean isDecompressorResetAllowed; + private boolean isCompressing; + private boolean isDecompressing; + private long compressedBytes; + private long decompressedBytes; + + public WebSocketPerMessageDeflateExtension() { + this(DEFAULT_COMPRESSION); + } + + public WebSocketPerMessageDeflateExtension(int compressionLevel) { + this(compressionLevel, Integer.MAX_VALUE); + } + + public WebSocketPerMessageDeflateExtension(int compressionLevel, int maxFrameSize) { + this.compressionLevel = compressionLevel; + this.maxFrameSize = maxFrameSize; + compressor = new Deflater(compressionLevel, true); + decompressor = new Inflater(true); + compressionThreshold = DEFAULT_COMPRESSION_THRESHOLD; + clientNoContextTakeover = DEFAULT_CLIENT_NO_CONTEXT_TAKEOVER; + serverNoContextTakeover = DEFAULT_SERVER_NO_CONTEXT_TAKEOVER; + clientMaxWindowBits = DEFAULT_CLIENT_MAX_WINDOW_BITS; + serverMaxWindowBits = DEFAULT_SERVER_MAX_WINDOW_BITS; + isCompressorResetRequired = false; + isDecompressorResetAllowed = false; + isCompressing = false; + isDecompressing = false; + compressedBytes = 0; + decompressedBytes = 0; + } + + public int getCompressionLevel() { + return compressionLevel; + } + + public int getMaxFrameSize() { + return maxFrameSize; + } + + public int getThreshold() { + return compressionThreshold; + } + + public void setThreshold(int threshold) { + this.compressionThreshold = threshold; + } + + public boolean isClientNoContextTakeover() { + return clientNoContextTakeover; + } + + public void setClientNoContextTakeover(boolean clientNoContextTakeover) { + this.clientNoContextTakeover = clientNoContextTakeover; + } + + public boolean isServerNoContextTakeover() { + return serverNoContextTakeover; + } + + public void setServerNoContextTakeover(boolean serverNoContextTakeover) { + this.serverNoContextTakeover = serverNoContextTakeover; + } + + /** + * Returns the overall compression ratio of all incoming and outgoing payloads which were + * compressed. + * + *

Values below 1 mean the compression is effective, the lower, the better. If you get values + * above 1, look into increasing the compression level or the threshold. If that does not help, + * consider not using this extension. + * + * @return the overall compression ratio of all incoming and outgoing payloads + */ + public double getCompressionRatio() { + double decompressed = decompressedBytes; + return decompressed > 0 ? compressedBytes / decompressed : 1; + } + + @Override + public void isFrameValid(Framedata inputFrame) throws InvalidDataException { + // RFC 7692: RSV1 may only be set for the first fragment of a message + if (inputFrame instanceof ContinuousFrame + && (inputFrame.isRSV1() || inputFrame.isRSV2() || inputFrame.isRSV3())) { + throw new InvalidFrameException("Continuous frame cannot have RSV1, RSV2 or RSV3 set"); + } + super.isFrameValid(inputFrame); + } + + @Override + public void decodeFrame(Framedata inputFrame) throws InvalidDataException { + // RFC 7692: PMCEs operate only on data messages. + if (!(inputFrame instanceof DataFrame)) { + return; + } + + // decompression is only applicable if it was started on the first fragment + if (!isDecompressing && inputFrame instanceof ContinuousFrame) { + return; + } + + // check the RFC 7962 compression marker RSV1 whether to start decompressing + if (inputFrame.isRSV1()) { + isDecompressing = true; + } + + if (!isDecompressing) { + return; + } + + // decompress the frame payload + DataFrame dataFrame = (DataFrame) inputFrame; + ByteBuffer payload = dataFrame.getPayloadData(); + compressedBytes += payload.remaining(); + byte[] decompressed = decompress(payload, dataFrame.isFin()); + decompressedBytes += decompressed.length; + dataFrame.setPayload(ByteBuffer.wrap(decompressed)); + + // payload is no longer compressed, clear the RFC 7962 compression marker RSV1 + if (!(dataFrame instanceof ContinuousFrame)) { + dataFrame.setRSV1(false); + } + + // stop decompressing after the final fragment + if (dataFrame.isFin()) { + isDecompressing = false; + // RFC 7692: If the "agreed parameters" contain the "client|server_no_context_takeover" + // extension parameter, the server|client MAY decompress each new message with an empty + // LZ77 sliding window. + if (isDecompressorResetAllowed) { + decompressor.reset(); + } + } + } + + private byte[] decompress(ByteBuffer buffer, boolean isFinal) throws InvalidDataException { + ByteArrayOutputStream decompressed = new ByteArrayOutputStream(); + try { + decompress(buffer, decompressed); + // RFC 7962: Append empty deflate block to the tail end of the payload of the message + if (isFinal) { + decompress(ByteBuffer.wrap(EMPTY_DEFLATE_BLOCK), decompressed); + } + } catch (DataFormatException e) { + throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, e.getMessage()); + } + return decompressed.toByteArray(); + } + + private void decompress(ByteBuffer buffer, ByteArrayOutputStream decompressed) + throws DataFormatException { + if (buffer.hasArray()) { + decompressor.setInput( + buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + } else { + byte[] input = new byte[buffer.remaining()]; + buffer.duplicate().get(input); + decompressor.setInput(input); + } + byte[] chunk = new byte[TRANSFER_CHUNK_SIZE]; + while (!decompressor.finished()) { + int length = decompressor.inflate(chunk); + if (length > 0) { + decompressed.write(chunk, 0, length); + if (maxFrameSize > 0 && maxFrameSize < decompressed.size()) { + throw new DataFormatException( + "Inflated frame size exceeds limit of " + maxFrameSize + " bytes"); + } + } else { + break; + } + } + } + + @Override + public void encodeFrame(Framedata inputFrame) { + // RFC 7692: PMCEs operate only on data messages. + if (!(inputFrame instanceof DataFrame)) { + return; + } + + // compression is only applicable if it was started on the first fragment + if (!isCompressing && inputFrame instanceof ContinuousFrame) { + return; + } + + // check the threshold whether to start compressing + if (inputFrame.getPayloadData().remaining() >= compressionThreshold) { + isCompressing = true; + } + + if (!isCompressing) { + return; + } + + // compress the frame payload + DataFrame dataFrame = (DataFrame) inputFrame; + ByteBuffer payload = dataFrame.getPayloadData(); + decompressedBytes += payload.remaining(); + byte[] compressed = compress(payload, dataFrame.isFin()); + compressedBytes += compressed.length; + dataFrame.setPayload(ByteBuffer.wrap(compressed)); + + // payload is compressed now, set the RFC 7962 compression marker RSV1 + if (!(dataFrame instanceof ContinuousFrame)) { + dataFrame.setRSV1(true); + } + + // stop compressing after the final fragment + if (dataFrame.isFin()) { + isCompressing = false; + // RFC 7692: If the "agreed parameters" contain the "client|server_no_context_takeover" + // extension parameter, the client|server MUST start compressing each new message with an + // empty LZ77 sliding window. + if (isCompressorResetRequired) { + compressor.reset(); + } + } + } + + private byte[] compress(ByteBuffer buffer, boolean isFinal) { + // RFC 7962: Generate an empty fragment if the buffer for uncompressed data buffer is empty. + if (!buffer.hasRemaining() && isFinal) { + return EMPTY_UNCOMPRESSED_DEFLATE_BLOCK; + } + if (buffer.hasArray()) { + compressor.setInput( + buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + } else { + byte[] input = new byte[buffer.remaining()]; + buffer.duplicate().get(input); + compressor.setInput(input); + } + // RFC 7962 prefers the compressor output not to have the BFINAL bit set, so instead of calling + // finish(), deflate with NO_FLUSH until the input is exhausted, then deflate with SYNC_FLUSH + // until the output runs dry. + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + byte[] chunk = new byte[TRANSFER_CHUNK_SIZE]; + while (!compressor.needsInput()) { + int length = compressor.deflate(chunk, 0, chunk.length, NO_FLUSH); + if (length > 0) { + compressed.write(chunk, 0, length); + } else { + break; + } + } + while (!compressor.finished()) { + int length = compressor.deflate(chunk, 0, chunk.length, SYNC_FLUSH); + if (length > 0) { + compressed.write(chunk, 0, length); + } else { + break; + } + } + return isFinal + ? removeTail(compressed.toByteArray(), EMPTY_DEFLATE_BLOCK) + : compressed.toByteArray(); + } + + private byte[] removeTail(byte[] input, byte[] tail) { + return hasTail(input, tail) ? Arrays.copyOf(input, input.length - tail.length) : input; + } + + private boolean hasTail(byte[] input, byte[] tail) { + int offset = input.length - tail.length; + if (offset < 0) { + return false; + } + for (int i = 0; i < tail.length; i++) { + if (input[offset + i] != tail[i]) { + return false; + } + } + return true; + } + + @Override + public boolean acceptProvidedExtensionAsServer(String inputExtension) { + for (String extensionRequest : inputExtension.split(",")) { + ExtensionRequestData extensionRequestData = parseExtensionRequest(extensionRequest); + if (EXTENSION_IDENTIFIER.equalsIgnoreCase(extensionRequestData.getExtensionName()) + && acceptExtensionParametersAsServer(extensionRequestData)) { + // extension offer with acceptable extension parameters found + return true; + } + } + return false; + } + + private boolean acceptExtensionParametersAsServer(ExtensionRequestData extensionRequestData) { + // initialize extension negotiation offer parameters + boolean offerClientNoContextTakeover = false; + boolean offerServerNoContextTakeover = false; + Optional offerClientMaxWindowBits = Optional.empty(); + Optional offerServerMaxWindowBits = Optional.empty(); + + // scan through the parameters in the extension negotiation offer + for (Map.Entry parameter : + extensionRequestData.getExtensionParameters().entrySet()) { + if (PARAMETER_CLIENT_NO_CONTEXT_TAKEOVER.equalsIgnoreCase(parameter.getKey())) { + offerClientNoContextTakeover = true; + } else if (PARAMETER_SERVER_NO_CONTEXT_TAKEOVER.equalsIgnoreCase(parameter.getKey())) { + offerServerNoContextTakeover = true; + } else if (PARAMETER_CLIENT_MAX_WINDOW_BITS.equalsIgnoreCase(parameter.getKey())) { + // RFC 7692: This parameter may have no value to only indicate support for it + if (parameter.getValue().isEmpty()) { + offerClientMaxWindowBits = Optional.of(clientMaxWindowBits); + } else { + try { + offerClientMaxWindowBits = Optional.of(Integer.parseInt(parameter.getValue())); + if (offerClientMaxWindowBits.get() < MINIMUM_CLIENT_MAX_WINDOW_BITS + || offerClientMaxWindowBits.get() > MAXIMUM_CLIENT_MAX_WINDOW_BITS) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } + } else if (PARAMETER_SERVER_MAX_WINDOW_BITS.equalsIgnoreCase(parameter.getKey())) { + // RFC 7692: This parameter must always have a value + try { + offerServerMaxWindowBits = Optional.of(Integer.parseInt(parameter.getValue())); + if (offerServerMaxWindowBits.get() < MINIMUM_SERVER_MAX_WINDOW_BITS + || offerServerMaxWindowBits.get() > MAXIMUM_SERVER_MAX_WINDOW_BITS) { + return false; + } + // The Java Deflater class only supports the default maximum window bits (15) + if (offerServerMaxWindowBits.get() != DEFAULT_SERVER_MAX_WINDOW_BITS) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else { + // RFC 7692: A server MUST decline an extension negotiation offer for this extension + // if the negotiation offer contains an extension parameter not defined for use in an + // offer. + return false; + } + } + + // merge accepted extension parameters with local configuration + clientNoContextTakeover |= offerClientNoContextTakeover; + serverNoContextTakeover |= offerServerNoContextTakeover; + clientMaxWindowBits = offerClientMaxWindowBits.orElse(clientMaxWindowBits); + serverMaxWindowBits = offerServerMaxWindowBits.orElse(serverMaxWindowBits); + + // RFC 7692: The extension parameters with the "server_" prefix are used by the server to + // configure its compressor. The extension parameters with the "client_" prefix are used by + // the server to configure its decompressor. + isCompressorResetRequired = serverNoContextTakeover; + isDecompressorResetAllowed = clientNoContextTakeover; + return true; + } + + @Override + public boolean acceptProvidedExtensionAsClient(String inputExtension) { + for (String extensionRequest : inputExtension.split(",")) { + ExtensionRequestData extensionRequestData = parseExtensionRequest(extensionRequest); + if (EXTENSION_IDENTIFIER.equalsIgnoreCase(extensionRequestData.getExtensionName())) { + return acceptExtensionParametersAsClient(extensionRequestData); + } + } + return false; + } + + private boolean acceptExtensionParametersAsClient(ExtensionRequestData extensionRequestData) { + // initialize extension negotiation response parameters + boolean responseClientNoContextTakeover = false; + boolean responseServerNoContextTakeover = false; + Optional responseClientMaxWindowBits = Optional.empty(); + Optional responseServerMaxWindowBits = Optional.empty(); + + // scan through the parameters in the extension negotiation response + for (Map.Entry parameter : + extensionRequestData.getExtensionParameters().entrySet()) { + if (PARAMETER_CLIENT_NO_CONTEXT_TAKEOVER.equalsIgnoreCase(parameter.getKey())) { + responseClientNoContextTakeover = true; + } else if (PARAMETER_SERVER_NO_CONTEXT_TAKEOVER.equalsIgnoreCase(parameter.getKey())) { + responseServerNoContextTakeover = true; + } else if (PARAMETER_CLIENT_MAX_WINDOW_BITS.equalsIgnoreCase(parameter.getKey())) { + try { + responseClientMaxWindowBits = Optional.of(Integer.parseInt(parameter.getValue())); + if (responseClientMaxWindowBits.get() < MINIMUM_CLIENT_MAX_WINDOW_BITS + || responseClientMaxWindowBits.get() > MAXIMUM_CLIENT_MAX_WINDOW_BITS) { + return false; + } + // The Java Deflater class only supports the default maximum window bits (15) + if (responseClientMaxWindowBits.get() != DEFAULT_CLIENT_MAX_WINDOW_BITS) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else if (PARAMETER_SERVER_MAX_WINDOW_BITS.equalsIgnoreCase(parameter.getKey())) { + try { + responseServerMaxWindowBits = Optional.of(Integer.parseInt(parameter.getValue())); + if (responseServerMaxWindowBits.get() < MINIMUM_SERVER_MAX_WINDOW_BITS + || responseServerMaxWindowBits.get() > MAXIMUM_SERVER_MAX_WINDOW_BITS) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else { + // RFC 7692: A client MUST _Fail the WebSocket Connection_ if the peer server accepted an + // extension negotiation offer for this extension with an extension negotiation response + // that contains an extension parameter not defined for use in a response. + return false; + } + } + + // merge accepted extension parameters with local configuration + clientNoContextTakeover |= responseClientNoContextTakeover; + // the server_no_context_takeover parameter MUST NOT be merged with the local setting! + // if the server does not return this parameter, it must not be used. + serverNoContextTakeover = responseServerNoContextTakeover; + clientMaxWindowBits = responseClientMaxWindowBits.orElse(clientMaxWindowBits); + serverMaxWindowBits = responseServerMaxWindowBits.orElse(serverMaxWindowBits); + + // RFC 7692: The extension parameters with the "client_" prefix are used by the client to + // configure its compressor. The extension parameters with the "server_" prefix are used by + // the client to configure its decompressor. + isCompressorResetRequired = clientNoContextTakeover; + isDecompressorResetAllowed = serverNoContextTakeover; + return true; + } + + @Override + public String getProvidedExtensionAsClient() { + return EXTENSION_IDENTIFIER + + (clientNoContextTakeover ? "; " + PARAMETER_CLIENT_NO_CONTEXT_TAKEOVER : "") + + (serverNoContextTakeover ? "; " + PARAMETER_SERVER_NO_CONTEXT_TAKEOVER : "") + + (clientMaxWindowBits != DEFAULT_CLIENT_MAX_WINDOW_BITS + ? "; " + PARAMETER_CLIENT_MAX_WINDOW_BITS + "=" + clientMaxWindowBits + : "") + + (serverMaxWindowBits != DEFAULT_SERVER_MAX_WINDOW_BITS + ? "; " + PARAMETER_SERVER_MAX_WINDOW_BITS + "=" + serverMaxWindowBits + : ""); + } + + @Override + public String getProvidedExtensionAsServer() { + return EXTENSION_IDENTIFIER + + (clientNoContextTakeover ? "; " + PARAMETER_CLIENT_NO_CONTEXT_TAKEOVER : "") + + (serverNoContextTakeover ? "; " + PARAMETER_SERVER_NO_CONTEXT_TAKEOVER : "") + + (clientMaxWindowBits != DEFAULT_CLIENT_MAX_WINDOW_BITS + ? "; " + PARAMETER_CLIENT_MAX_WINDOW_BITS + "=" + clientMaxWindowBits + : "") + + (serverMaxWindowBits != DEFAULT_SERVER_MAX_WINDOW_BITS + ? "; " + PARAMETER_SERVER_MAX_WINDOW_BITS + "=" + serverMaxWindowBits + : ""); + } + + @Override + public IExtension copyInstance() { + WebSocketPerMessageDeflateExtension clone = + new WebSocketPerMessageDeflateExtension(getCompressionLevel(), getMaxFrameSize()); + clone.setClientNoContextTakeover(isClientNoContextTakeover()); + clone.setServerNoContextTakeover(isServerNoContextTakeover()); + clone.clientMaxWindowBits = clientMaxWindowBits; + clone.serverMaxWindowBits = serverMaxWindowBits; + clone.setThreshold(getThreshold()); + return clone; + } + + @Override + public void reset() { + isCompressing = false; + isDecompressing = false; + compressedBytes = 0; + decompressedBytes = 0; + } + + @Override + public String toString() { + return EXTENSION_COMMON_NAME; + } +} diff --git a/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionRFC7962Test.java b/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionRFC7962Test.java new file mode 100644 index 00000000..199e548e --- /dev/null +++ b/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionRFC7962Test.java @@ -0,0 +1,276 @@ +/* + * TooTallNate - Java-WebSocket + * + * MIT License + * + * Copyright (C) 2025 Robert Schlabbach + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.java_websocket.extensions; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.extensions.permessage_deflate.WebSocketPerMessageDeflateExtension; +import org.java_websocket.framing.*; +import org.java_websocket.handshake.HandshakeImpl1Client; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** RFC 7692 WebSocket Per-Message Deflate Extension Tests */ +public class WebSocketPerMessageDeflateExtensionRFC7962Test { + // RFC 7692 Section 7.2.3.1 A Message Compressed Using One Compressed Deflate Block + private static final String RFC_7962_TEST_MESSAGE_TEXT = "Hello"; + private static final byte[] RFC_7962_TEST_MESSAGE_COMPRESSED = + new byte[] { + (byte) 0xc1, 0x07, (byte) 0xf2, 0x48, (byte) 0xcd, (byte) 0xc9, (byte) 0xc9, 0x07, 0x00 + }; + private static final byte[] RFC_7962_TEST_MESSAGE_FRAGMENTS = + new byte[] { + // first frame: + 0x41, + 0x03, + (byte) 0xf2, + 0x48, + (byte) 0xcd, + // second frame: + (byte) 0x80, + 0x04, + (byte) 0xc9, + (byte) 0xc9, + 0x07, + 0x00 + }; + // RFC 7692 Section 7.2.3.2 Sharing LZ77 Sliding Window + private static final byte[] RFC_7962_TEST_PAYLOAD_COMPRESSED = + new byte[] {(byte) 0xf2, 0x48, (byte) 0xcd, (byte) 0xc9, (byte) 0xc9, 0x07, 0x00}; + private static final byte[] RFC_7962_TEST_PAYLOAD_COMPRESSED_AGAIN = + new byte[] {(byte) 0xf2, 0x00, 0x11, 0x00, 0x00}; + // RFC 7692 Section 7.2.3.3 DEFLATE Block with No Compression + private static final byte[] RFC_7962_TEST_MESSAGE_NO_COMPRESSION = + new byte[] { + (byte) 0xc1, + 0x0b, + 0x00, + 0x05, + 0x00, + (byte) 0xfa, + (byte) 0xff, + 0x48, + 0x65, + 0x6c, + 0x6c, + 0x6f, + 0x00 + }; + // RFC 7692 Section 7.2.3.4 DEFLATE Block with "BFINAL" Set to 1 + private static final byte[] RFC_7962_TEST_PAYLOAD_COMPRESSED_BFINAL = + new byte[] {(byte) 0xf3, 0x48, (byte) 0xcd, (byte) 0xc9, (byte) 0xc9, 0x07, 0x00, 0x00}; + // RFC 7692 Section 7.2.3.5 Two DEFLATE Blocks in One Message + private static final byte[] RFC_7962_TEST_PAYLOAD_TWO_DEFLATE_BLOCKS = + new byte[] { + (byte) 0xf2, + 0x48, + 0x05, + 0x00, + 0x00, + 0x00, + (byte) 0xff, + (byte) 0xff, + (byte) 0xca, + (byte) 0xc9, + (byte) 0xc9, + 0x07, + 0x00 + }; + // RFC 7692 Section 7.2.3.6 Compressed Empty Fragment + private static final byte[] RFC_7962_TEST_PAYLOAD_COMPRESSED_EMPTY_FRAGMENT = new byte[] {0x00}; + + private WebSocketPerMessageDeflateExtension extension; + private Draft_6455 draft; + + @BeforeEach + public void setUp() throws Exception { + extension = new WebSocketPerMessageDeflateExtension(); + extension.setThreshold(0); + setupDraft(); + } + + private void setupDraft() throws InvalidHandshakeException { + draft = new Draft_6455(extension); + HandshakeImpl1Client handshake = new HandshakeImpl1Client(); + handshake.setResourceDescriptor("/"); + handshake.put("Host", "localhost"); + handshake.put("Connection", "Upgrade"); + handshake.put("Upgrade", "websocket"); + handshake.put("Sec-WebSocket-Version", "13"); + handshake.put("Sec-WebSocket-Extensions", extension.getProvidedExtensionAsClient()); + draft.acceptHandshakeAsServer(handshake); + } + + @Test + public void testRFC7962Section7231MessageCompression() { + Framedata frame = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + byte[] frameBytes = draft.createBinaryFrame(frame).array(); + assertArrayEquals(RFC_7962_TEST_MESSAGE_COMPRESSED, frameBytes); + } + + @Test + public void testRFC7962Section7231FragmentsDecompression() throws InvalidDataException { + List frames = draft.translateFrame(ByteBuffer.wrap(RFC_7962_TEST_MESSAGE_FRAGMENTS)); + assertEquals(2, frames.size()); + assertInstanceOf(DataFrame.class, frames.get(0)); + assertFalse(frames.get(0) instanceof ContinuousFrame); + assertFalse(frames.get(0).isFin()); + assertFalse(frames.get(0).isRSV1()); + assertFalse(frames.get(0).isRSV2()); + assertFalse(frames.get(0).isRSV3()); + assertInstanceOf(ContinuousFrame.class, frames.get(1)); + assertTrue(frames.get(1).isFin()); + assertFalse(frames.get(1).isRSV1()); + assertFalse(frames.get(1).isRSV2()); + assertFalse(frames.get(1).isRSV3()); + assertEquals(RFC_7962_TEST_MESSAGE_TEXT, framesPayloadToString(frames)); + } + + @Test + public void testRFC7962Section7232CompressionWithNoContextTakeover() + throws InvalidHandshakeException { + extension.setServerNoContextTakeover(true); + setupDraft(); + Framedata frame1 = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + extension.encodeFrame(frame1); + assertArrayEquals(RFC_7962_TEST_PAYLOAD_COMPRESSED, getPayload(frame1)); + Framedata frame2 = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + extension.encodeFrame(frame2); + assertArrayEquals(RFC_7962_TEST_PAYLOAD_COMPRESSED, getPayload(frame2)); + } + + @Test + public void testRFC7962Section7232CompressionWithContextTakeover() { + Framedata frame1 = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + extension.encodeFrame(frame1); + assertArrayEquals(RFC_7962_TEST_PAYLOAD_COMPRESSED, getPayload(frame1)); + Framedata frame2 = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + extension.encodeFrame(frame2); + assertArrayEquals(RFC_7962_TEST_PAYLOAD_COMPRESSED_AGAIN, getPayload(frame2)); + } + + @Test + public void testRFC7962Section7233DeflateBlockWithNoCompression() throws InvalidDataException { + List frames = + draft.translateFrame(ByteBuffer.wrap(RFC_7962_TEST_MESSAGE_NO_COMPRESSION)); + assertEquals(1, frames.size()); + assertInstanceOf(DataFrame.class, frames.get(0)); + assertFalse(frames.get(0) instanceof ContinuousFrame); + assertTrue(frames.get(0).isFin()); + assertFalse(frames.get(0).isRSV1()); + assertFalse(frames.get(0).isRSV2()); + assertFalse(frames.get(0).isRSV3()); + assertEquals(RFC_7962_TEST_MESSAGE_TEXT, framesPayloadToString(frames)); + } + + @Test + public void testRFC7962Section7234DeflateBlockWithBFINAL() throws InvalidDataException { + Framedata frame = buildCompressedFrame(RFC_7962_TEST_PAYLOAD_COMPRESSED_BFINAL); + extension.decodeFrame(frame); + assertTrue(frame.isFin()); + assertFalse(frame.isRSV1()); + assertFalse(frame.isRSV2()); + assertFalse(frame.isRSV3()); + assertEquals(RFC_7962_TEST_MESSAGE_TEXT, framePayloadToString(frame)); + } + + @Test + public void testRFC7962Section7235TwoDeflateBlocksInOneMessage() throws InvalidDataException { + Framedata frame = buildCompressedFrame(RFC_7962_TEST_PAYLOAD_TWO_DEFLATE_BLOCKS); + extension.decodeFrame(frame); + assertTrue(frame.isFin()); + assertFalse(frame.isRSV1()); + assertFalse(frame.isRSV2()); + assertFalse(frame.isRSV3()); + assertEquals(RFC_7962_TEST_MESSAGE_TEXT, framePayloadToString(frame)); + } + + @Test + public void testRFC7962Section7236GeneratingAnEmptyFragment() throws InvalidDataException { + DataFrame frame1 = buildMessageFrame(RFC_7962_TEST_MESSAGE_TEXT); + frame1.setFin(false); + DataFrame frame2 = new ContinuousFrame(); + frame2.setFin(true); + extension.encodeFrame(frame1); + extension.encodeFrame(frame2); + assertArrayEquals(RFC_7962_TEST_PAYLOAD_COMPRESSED_EMPTY_FRAGMENT, getPayload(frame2)); + extension.decodeFrame(frame1); + extension.decodeFrame(frame2); + List frames = new ArrayList<>(2); + frames.add(frame1); + frames.add(frame2); + assertEquals(RFC_7962_TEST_MESSAGE_TEXT, framesPayloadToString(frames)); + } + + private DataFrame buildMessageFrame(String message) { + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message.getBytes())); + frame.setFin(true); + return frame; + } + + private DataFrame buildCompressedFrame(byte[] payload) { + DataFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(payload)); + frame.setRSV1(true); + frame.setFin(true); + return frame; + } + + private String framePayloadToString(Framedata frame) { + return framesPayloadToString(Collections.singletonList(frame)); + } + + private String framesPayloadToString(List frames) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + for (Framedata frame : frames) { + output.write(getPayload(frame)); + } + return output.toString(); + } catch (IOException e) { + return null; + } + } + + private byte[] getPayload(Framedata frame) { + ByteBuffer buffer = frame.getPayloadData(); + byte[] payload = new byte[buffer.remaining()]; + System.arraycopy( + buffer.array(), buffer.arrayOffset() + buffer.position(), payload, 0, buffer.remaining()); + return payload; + } +} diff --git a/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionTest.java b/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionTest.java new file mode 100644 index 00000000..6dd8779e --- /dev/null +++ b/src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionTest.java @@ -0,0 +1,320 @@ +package org.java_websocket.extensions; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.zip.Deflater; + +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.extensions.permessage_deflate.WebSocketPerMessageDeflateExtension; +import org.java_websocket.framing.ContinuousFrame; +import org.java_websocket.framing.TextFrame; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class WebSocketPerMessageDeflateExtensionTest { + + @Test + public void testDecodeFrame() throws InvalidDataException { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + deflateExtension.setThreshold(0); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.encodeFrame(frame); + assertTrue(frame.isRSV1()); + deflateExtension.decodeFrame(frame); + assertArrayEquals(message, frame.getPayloadData().array()); + } + @Test + public void testDecodeFrameIfRSVIsNotSet() throws InvalidDataException { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.decodeFrame(frame); + assertArrayEquals(message, frame.getPayloadData().array()); + assertFalse(frame.isRSV1()); + } + + @Test + public void testDecodeFrameNoCompression() throws InvalidDataException { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(Deflater.NO_COMPRESSION); + deflateExtension.setThreshold(0); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.encodeFrame(frame); + byte[] payloadArray = frame.getPayloadData().array(); + assertArrayEquals(message, Arrays.copyOfRange(payloadArray, 5, payloadArray.length - 1)); + assertTrue(frame.isRSV1()); + deflateExtension.decodeFrame(frame); + assertArrayEquals(message, frame.getPayloadData().array()); + } + + @Test + public void testDecodeFrameBestSpeedCompression() throws InvalidDataException { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(Deflater.BEST_SPEED); + deflateExtension.setThreshold(0); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + + Deflater localDeflater = new Deflater(Deflater.BEST_SPEED,true); + localDeflater.setInput(ByteBuffer.wrap(message).array()); + byte[] buffer = new byte[1024]; + int bytesCompressed = localDeflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH); + + deflateExtension.encodeFrame(frame); + byte[] payloadArray = frame.getPayloadData().array(); + assertArrayEquals(Arrays.copyOfRange(buffer, 0, bytesCompressed - 4), payloadArray); + assertTrue(frame.isRSV1()); + deflateExtension.decodeFrame(frame); + assertArrayEquals(message, frame.getPayloadData().array()); + } + + @Test + public void testDecodeFrameBestCompression() throws InvalidDataException { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(Deflater.BEST_COMPRESSION); + deflateExtension.setThreshold(0); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + + Deflater localDeflater = new Deflater(Deflater.BEST_COMPRESSION,true); + localDeflater.setInput(ByteBuffer.wrap(message).array()); + byte[] buffer = new byte[1024]; + int bytesCompressed = localDeflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH); + + deflateExtension.encodeFrame(frame); + byte[] payloadArray = frame.getPayloadData().array(); + assertArrayEquals(Arrays.copyOfRange(buffer, 0, bytesCompressed - 4), payloadArray); + assertTrue(frame.isRSV1()); + deflateExtension.decodeFrame(frame); + assertArrayEquals(message, frame.getPayloadData().array()); + } + + + @Test + public void testEncodeFrame() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + deflateExtension.setThreshold(0); + String str = "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text" + + "This is a highly compressable text"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.encodeFrame(frame); + assertTrue(message.length > frame.getPayloadData().array().length); + } + @Test + public void testEncodeFrameBelowThreshold() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + deflateExtension.setThreshold(11); + String str = "Hello World"; + byte[] message = str.getBytes(); + TextFrame frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.encodeFrame(frame); + // Message length is equal to the threshold --> encode + assertTrue(frame.isRSV1()); + str = "Hello Worl"; + message = str.getBytes(); + frame = new TextFrame(); + frame.setPayload(ByteBuffer.wrap(message)); + deflateExtension.encodeFrame(frame); + // Message length is below to the threshold --> do NOT encode + assertFalse(frame.isRSV1()); + } + + @Test + public void testAcceptProvidedExtensionAsServer() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertTrue(deflateExtension.acceptProvidedExtensionAsServer("permessage-deflate")); + assertTrue(deflateExtension + .acceptProvidedExtensionAsServer("some-other-extension, permessage-deflate")); + assertFalse(deflateExtension.acceptProvidedExtensionAsServer("wrong-permessage-deflate")); + } + + @Test + public void testAcceptProvidedExtensionAsClient() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertTrue(deflateExtension.acceptProvidedExtensionAsClient("permessage-deflate")); + assertTrue(deflateExtension + .acceptProvidedExtensionAsClient("some-other-extension, permessage-deflate")); + assertFalse(deflateExtension.acceptProvidedExtensionAsClient("wrong-permessage-deflate")); + } + + @Test + public void testIsFrameValid() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + TextFrame frame = new TextFrame(); + try { + deflateExtension.isFrameValid(frame); + } catch (Exception e) { + fail("RSV1 is optional and should therefore not fail"); + } + frame.setRSV1(true); + try { + deflateExtension.isFrameValid(frame); + } catch (Exception e) { + fail("Frame is valid."); + } + frame.setRSV2(true); + try { + deflateExtension.isFrameValid(frame); + fail("Only RSV1 bit must be set."); + } catch (Exception e) { + // + } + ContinuousFrame contFrame = new ContinuousFrame(); + contFrame.setRSV1(true); + try { + deflateExtension.isFrameValid(contFrame); + fail("RSV1 must only be set for first fragments.Continuous frames can't have RSV1 bit set."); + } catch (Exception e) { + // + } + contFrame.setRSV1(false); + try { + deflateExtension.isFrameValid(contFrame); + } catch (Exception e) { + fail("Continuous frame is valid."); + } + } + + @Test + public void testGetProvidedExtensionAsClient() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertEquals("permessage-deflate", deflateExtension.getProvidedExtensionAsClient()); + deflateExtension.setClientNoContextTakeover(true); + assertEquals("permessage-deflate; client_no_context_takeover", + deflateExtension.getProvidedExtensionAsClient()); + deflateExtension.setServerNoContextTakeover(true); + assertEquals("permessage-deflate; client_no_context_takeover; server_no_context_takeover", + deflateExtension.getProvidedExtensionAsClient()); + deflateExtension.setClientNoContextTakeover(false); + assertEquals("permessage-deflate; server_no_context_takeover", + deflateExtension.getProvidedExtensionAsClient()); + } + + @Test + public void testGetProvidedExtensionAsServer() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertEquals("permessage-deflate", deflateExtension.getProvidedExtensionAsServer()); + deflateExtension.setClientNoContextTakeover(true); + assertEquals("permessage-deflate; client_no_context_takeover", + deflateExtension.getProvidedExtensionAsServer()); + deflateExtension.setServerNoContextTakeover(true); + assertEquals("permessage-deflate; client_no_context_takeover; server_no_context_takeover", + deflateExtension.getProvidedExtensionAsServer()); + deflateExtension.setClientNoContextTakeover(false); + assertEquals("permessage-deflate; server_no_context_takeover", + deflateExtension.getProvidedExtensionAsServer()); + } + + @Test + public void testToString() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertEquals("WebSocket Per-Message Deflate", deflateExtension.toString()); + } + + @Test + public void testIsServerNoContextTakeover() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertFalse(deflateExtension.isServerNoContextTakeover()); + } + + @Test + public void testSetServerNoContextTakeover() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + deflateExtension.setServerNoContextTakeover(true); + assertTrue(deflateExtension.isServerNoContextTakeover()); + } + + @Test + public void testIsClientNoContextTakeover() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertFalse(deflateExtension.isClientNoContextTakeover()); + } + + @Test + public void testSetClientNoContextTakeover() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + deflateExtension.setClientNoContextTakeover(true); + assertTrue(deflateExtension.isClientNoContextTakeover()); + } + + @Test + public void testCopyInstance() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + WebSocketPerMessageDeflateExtension newDeflateExtension = (WebSocketPerMessageDeflateExtension)deflateExtension.copyInstance(); + assertEquals("WebSocket Per-Message Deflate", newDeflateExtension.toString()); + // Also check the values + assertEquals(deflateExtension.getThreshold(), newDeflateExtension.getThreshold()); + assertEquals(deflateExtension.isClientNoContextTakeover(), newDeflateExtension.isClientNoContextTakeover()); + assertEquals(deflateExtension.isServerNoContextTakeover(), newDeflateExtension.isServerNoContextTakeover()); + assertEquals(deflateExtension.getCompressionLevel(), newDeflateExtension.getCompressionLevel()); + + + deflateExtension = new WebSocketPerMessageDeflateExtension(Deflater.BEST_COMPRESSION); + deflateExtension.setThreshold(512); + deflateExtension.setServerNoContextTakeover(false); + deflateExtension.setClientNoContextTakeover(true); + newDeflateExtension = (WebSocketPerMessageDeflateExtension)deflateExtension.copyInstance(); + + assertEquals(deflateExtension.getThreshold(), newDeflateExtension.getThreshold()); + assertEquals(deflateExtension.isClientNoContextTakeover(), newDeflateExtension.isClientNoContextTakeover()); + assertEquals(deflateExtension.isServerNoContextTakeover(), newDeflateExtension.isServerNoContextTakeover()); + assertEquals(deflateExtension.getCompressionLevel(), newDeflateExtension.getCompressionLevel()); + + + deflateExtension = new WebSocketPerMessageDeflateExtension(Deflater.NO_COMPRESSION); + deflateExtension.setThreshold(64); + deflateExtension.setServerNoContextTakeover(true); + deflateExtension.setClientNoContextTakeover(false); + newDeflateExtension = (WebSocketPerMessageDeflateExtension)deflateExtension.copyInstance(); + + assertEquals(deflateExtension.getThreshold(), newDeflateExtension.getThreshold()); + assertEquals(deflateExtension.isClientNoContextTakeover(), newDeflateExtension.isClientNoContextTakeover()); + assertEquals(deflateExtension.isServerNoContextTakeover(), newDeflateExtension.isServerNoContextTakeover()); + assertEquals(deflateExtension.getCompressionLevel(), newDeflateExtension.getCompressionLevel()); + } + + @Test + public void testDefaults() { + WebSocketPerMessageDeflateExtension deflateExtension = new WebSocketPerMessageDeflateExtension(); + assertFalse(deflateExtension.isClientNoContextTakeover()); + assertFalse(deflateExtension.isServerNoContextTakeover()); + assertEquals(64, deflateExtension.getThreshold()); + assertEquals(Deflater.DEFAULT_COMPRESSION, deflateExtension.getCompressionLevel()); + } +}