From f6a46be1944b716e75bb9e49dc0facf726f2b444 Mon Sep 17 00:00:00 2001 From: Robert Schlabbach Date: Thu, 25 Dec 2025 12:09:32 +0100 Subject: [PATCH] Add new RFC 7962-compliant Per-Message Deflate extension implementation Add a reimplementation of the WebSocket Per-Message Deflate extension written from scratch for full RFC 7962 compliance, with the following over the existing implementation: - compliant extension parameters negotiation handling - default operation with context takeover works - fragmented messages are handled correctly (either all fragments are compressed/decompressed or none) - clears RSV1 after decompressing to remove the compression mark - produces the result specified in RFC 7962 section 7.2.3.4 - produces the result specified in RFC 7962 section 7.2.3.6 - uses the extension common name registered for RFC 7962 - has an additional optional constructor parameter "maxFrameSize" to ensure the limit is not exceeded while decompressing already - has an additional API "getCompressionRatio()" to get the effective compression ratio (over all payloads compressed and decompressed) Otherwise, it is fully API compatible with the old implementation. For now, the new implementation lives side by side with the old one, and is named "WebSocketPerMessageDeflateExtension". Add RFC 7962 tests for the new implementation, to validate it produces the expected results for all examples from RFC 7962 section 7.2.3. Add a copy of the unit tests for the old implementation, which verifies the new implementation works the same, except for fixed issues and different defaults. --- .../WebSocketPerMessageDeflateExtension.java | 574 ++++++++++++++++++ ...PerMessageDeflateExtensionRFC7962Test.java | 276 +++++++++ ...bSocketPerMessageDeflateExtensionTest.java | 320 ++++++++++ 3 files changed, 1170 insertions(+) create mode 100644 src/main/java/org/java_websocket/extensions/permessage_deflate/WebSocketPerMessageDeflateExtension.java create mode 100644 src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionRFC7962Test.java create mode 100644 src/test/java/org/java_websocket/extensions/WebSocketPerMessageDeflateExtensionTest.java 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()); + } +}