1+ package org .java_websocket .extensions .permessage_deflate ;
2+
3+ import org .java_websocket .enums .Opcode ;
4+ import org .java_websocket .exceptions .InvalidDataException ;
5+ import org .java_websocket .exceptions .InvalidFrameException ;
6+ import org .java_websocket .extensions .CompressionExtension ;
7+ import org .java_websocket .extensions .IExtension ;
8+ import org .java_websocket .framing .*;
9+
10+ import java .io .ByteArrayOutputStream ;
11+ import java .nio .ByteBuffer ;
12+ import java .util .zip .DataFormatException ;
13+ import java .util .zip .Deflater ;
14+ import java .util .zip .Inflater ;
15+
16+ public class PerMessageDeflateExtension extends CompressionExtension {
17+
18+ // Name of the extension as registered by IETF https://tools.ietf.org/html/rfc7692#section-9.
19+ private static final String EXTENSION_REGISTERED_NAME = "permessage-deflate" ;
20+
21+ // Below values are defined for convenience. They are not used in the compression/decompression phase.
22+ // They may be needed during the extension-negotiation offer in the future.
23+ private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover" ;
24+ private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover" ;
25+ private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits" ;
26+ private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits" ;
27+ private static final boolean serverNoContextTakeover = true ;
28+ private static final boolean clientNoContextTakeover = true ;
29+ private static final int serverMaxWindowBits = 1 << 15 ;
30+ private static final int clientMaxWindowBits = 1 << 15 ;
31+
32+ private static final byte [] TAIL_BYTES = {0x00 , 0x00 , (byte )0xFF , (byte )0xFF };
33+ private static final int BUFFER_SIZE = 1 << 10 ;
34+
35+ /*
36+ An endpoint uses the following algorithm to decompress a message.
37+ 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
38+ payload of the message.
39+ 2. Decompress the resulting data using DEFLATE.
40+ See, https://tools.ietf.org/html/rfc7692#section-7.2.2
41+ */
42+ @ Override
43+ public void decodeFrame (Framedata inputFrame ) throws InvalidDataException {
44+ // Only DataFrames can be decompressed.
45+ if (!(inputFrame instanceof DataFrame ))
46+ return ;
47+
48+ // RSV1 bit must be set only for the first frame.
49+ if (inputFrame .getOpcode () == Opcode .CONTINUOUS && inputFrame .isRSV1 ())
50+ throw new InvalidDataException (CloseFrame .POLICY_VALIDATION , "RSV1 bit can only be set for the first frame." );
51+
52+ // Decompressed output buffer.
53+ ByteArrayOutputStream output = new ByteArrayOutputStream ();
54+ Inflater inflater = new Inflater (true );
55+ try {
56+ decompress (inflater , inputFrame .getPayloadData ().array (), output );
57+ // Decompress 4 bytes of 0x00 0x00 0xff 0xff as if they were appended to the end of message.
58+ if (inputFrame .isFin ())
59+ decompress (inflater , TAIL_BYTES , output );
60+ } catch (DataFormatException e ) {
61+ throw new InvalidDataException (CloseFrame .POLICY_VALIDATION , "Given data format couldn't be decompressed." );
62+ }finally {
63+ inflater .end ();
64+ }
65+
66+ // Set frames payload to the new decompressed data.
67+ ((FramedataImpl1 ) inputFrame ).setPayload (ByteBuffer .wrap (output .toByteArray ()));
68+ }
69+
70+ private void decompress (Inflater inflater , byte [] data , ByteArrayOutputStream outputBuffer ) throws DataFormatException {
71+ inflater .setInput (data );
72+ byte [] buffer = new byte [BUFFER_SIZE ];
73+
74+ int bytesInflated ;
75+ while ((bytesInflated = inflater .inflate (buffer )) > 0 ){
76+ outputBuffer .write (buffer , 0 , bytesInflated );
77+ }
78+ }
79+
80+ @ Override
81+ public void encodeFrame (Framedata inputFrame ) {
82+ // Only DataFrames can be decompressed.
83+ if (!(inputFrame instanceof DataFrame ))
84+ return ;
85+
86+ // Only the first frame's RSV1 must be set.
87+ if (!(inputFrame instanceof ContinuousFrame ))
88+ ((DataFrame ) inputFrame ).setRSV1 (true );
89+
90+ Deflater deflater = new Deflater (Deflater .DEFAULT_COMPRESSION , true );
91+ deflater .setInput (inputFrame .getPayloadData ().array ());
92+ deflater .finish ();
93+
94+ // Compressed output buffer.
95+ ByteArrayOutputStream output = new ByteArrayOutputStream ();
96+ // Temporary buffer to hold compressed output.
97+ byte [] buffer = new byte [1024 ];
98+ int bytesCompressed ;
99+ while ((bytesCompressed = deflater .deflate (buffer )) > 0 ) {
100+ output .write (buffer , 0 , bytesCompressed );
101+ }
102+ deflater .end ();
103+
104+ byte outputBytes [] = output .toByteArray ();
105+ int outputLength = outputBytes .length ;
106+ /*
107+ https://tools.ietf.org/html/rfc7692#section-7.2.1 states that if the final fragment's compressed
108+ payload ends with 0x00 0x00 0xff 0xff, they should be removed.
109+ To simulate removal, we just pass 4 bytes less to the new payload
110+ if the frame is final and outputBytes ends with 0x00 0x00 0xff 0xff.
111+ */
112+ if (inputFrame .isFin () && endsWithTail (outputBytes ))
113+ outputLength -= TAIL_BYTES .length ;
114+
115+ // Set frames payload to the new compressed data.
116+ ((FramedataImpl1 ) inputFrame ).setPayload (ByteBuffer .wrap (outputBytes , 0 , outputLength ));
117+ }
118+
119+ private boolean endsWithTail (byte [] data ){
120+ if (data .length < 4 )
121+ return false ;
122+
123+ int length = data .length ;
124+ for (int i = 0 ; i <= TAIL_BYTES .length ; i --){
125+ if (TAIL_BYTES [i ] != data [length - TAIL_BYTES .length + i ])
126+ return false ;
127+ }
128+
129+ return true ;
130+ }
131+
132+ @ Override
133+ public boolean acceptProvidedExtensionAsServer (String inputExtension ) {
134+ String [] requestedExtensions = inputExtension .split ("," );
135+ for (String extension : requestedExtensions )
136+ if (EXTENSION_REGISTERED_NAME .equalsIgnoreCase (extension .trim ()))
137+ return true ;
138+
139+ return false ;
140+ }
141+
142+ @ Override
143+ public boolean acceptProvidedExtensionAsClient (String inputExtension ) {
144+ String [] requestedExtensions = inputExtension .split ("," );
145+ for (String extension : requestedExtensions )
146+ if (EXTENSION_REGISTERED_NAME .equalsIgnoreCase (extension .trim ()))
147+ return true ;
148+
149+ return false ;
150+ }
151+
152+ @ Override
153+ public String getProvidedExtensionAsClient () {
154+ return EXTENSION_REGISTERED_NAME ;
155+ }
156+
157+ @ Override
158+ public String getProvidedExtensionAsServer () {
159+ return EXTENSION_REGISTERED_NAME ;
160+ }
161+
162+ @ Override
163+ public IExtension copyInstance () {
164+ return new PerMessageDeflateExtension ();
165+ }
166+
167+ /**
168+ * This extension requires the RSV1 bit to be set only for the first frame.
169+ * If the frame is type is CONTINUOUS, RSV1 bit must be unset.
170+ */
171+ @ Override
172+ public void isFrameValid (Framedata inputFrame ) throws InvalidDataException {
173+ if ((inputFrame instanceof TextFrame || inputFrame instanceof BinaryFrame ) && !inputFrame .isRSV1 ())
174+ throw new InvalidFrameException ("RSV1 bit must be set for DataFrames." );
175+ if ((inputFrame instanceof ContinuousFrame ) && (inputFrame .isRSV1 () || inputFrame .isRSV2 () || inputFrame .isRSV3 ()))
176+ throw new InvalidFrameException ( "bad rsv RSV1: " + inputFrame .isRSV1 () + " RSV2: " + inputFrame .isRSV2 () + " RSV3: " + inputFrame .isRSV3 () );
177+ super .isFrameValid (inputFrame );
178+ }
179+
180+ @ Override
181+ public String toString () {
182+ return "PerMessageDeflateExtension" ;
183+ }
184+
185+ }
0 commit comments