View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.websocket.common.extensions.compress;
20  
21  import java.nio.ByteBuffer;
22  import java.util.zip.DataFormatException;
23  
24  import org.eclipse.jetty.util.log.Log;
25  import org.eclipse.jetty.util.log.Logger;
26  import org.eclipse.jetty.websocket.api.BadPayloadException;
27  import org.eclipse.jetty.websocket.api.BatchMode;
28  import org.eclipse.jetty.websocket.api.WriteCallback;
29  import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
30  import org.eclipse.jetty.websocket.api.extensions.Frame;
31  import org.eclipse.jetty.websocket.common.OpCode;
32  
33  /**
34   * Per Message Deflate Compression extension for WebSocket.
35   * <p>
36   * Attempts to follow <a href="https://tools.ietf.org/html/rfc7692">Compression Extensions for WebSocket</a>
37   */
38  public class PerMessageDeflateExtension extends CompressExtension
39  {
40      private static final Logger LOG = Log.getLogger(PerMessageDeflateExtension.class);
41  
42      private ExtensionConfig configRequested;
43      private ExtensionConfig configNegotiated;
44      private boolean incomingContextTakeover = true;
45      private boolean outgoingContextTakeover = true;
46      private boolean incomingCompressed;
47  
48      @Override
49      public String getName()
50      {
51          return "permessage-deflate";
52      }
53  
54      @Override
55      public void incomingFrame(Frame frame)
56      {
57          // Incoming frames are always non concurrent because
58          // they are read and parsed with a single thread, and
59          // therefore there is no need for synchronization.
60  
61          // This extension requires the RSV1 bit set only in the first frame.
62          // Subsequent continuation frames don't have RSV1 set, but are compressed.
63          if (frame.getType().isData())
64          {
65              incomingCompressed = frame.isRsv1();
66          }
67  
68          if (OpCode.isControlFrame(frame.getOpCode()) || !incomingCompressed)
69          {
70              nextIncomingFrame(frame);
71              return;
72          }
73          
74          ByteAccumulator accumulator = newByteAccumulator();
75          
76          try 
77          {
78              ByteBuffer payload = frame.getPayload();
79              decompress(accumulator, payload);
80              if (frame.isFin())
81              {
82                  decompress(accumulator, TAIL_BYTES_BUF.slice());
83              }
84              
85              forwardIncoming(frame, accumulator);
86          }
87          catch (DataFormatException e)
88          {
89              throw new BadPayloadException(e);
90          }
91  
92          if (frame.isFin())
93              incomingCompressed = false;
94      }
95  
96      @Override
97      protected void nextIncomingFrame(Frame frame)
98      {
99          if (frame.isFin() && !incomingContextTakeover)
100         {
101             LOG.debug("Incoming Context Reset");
102             decompressCount.set(0);
103             getInflater().reset();
104         }
105         super.nextIncomingFrame(frame);
106     }
107 
108     @Override
109     protected void nextOutgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
110     {
111         if (frame.isFin() && !outgoingContextTakeover)
112         {
113             LOG.debug("Outgoing Context Reset");
114             getDeflater().reset();
115         }
116         super.nextOutgoingFrame(frame, callback, batchMode);
117     }
118     
119     @Override
120     int getRsvUseMode()
121     {
122         return RSV_USE_ONLY_FIRST;
123     }
124     
125     @Override
126     int getTailDropMode()
127     {
128         return TAIL_DROP_FIN_ONLY;
129     }
130 
131     @Override
132     public void setConfig(final ExtensionConfig config)
133     {
134         configRequested = new ExtensionConfig(config);
135         configNegotiated = new ExtensionConfig(config.getName());
136         
137         for (String key : config.getParameterKeys())
138         {
139             key = key.trim();
140             switch (key)
141             {
142                 case "client_max_window_bits":
143                 case "server_max_window_bits":
144                 {
145                     // Not supported by Jetty
146                     // Don't negotiate these parameters
147                     break;
148                 }
149                 case "client_no_context_takeover":
150                 {
151                     configNegotiated.setParameter("client_no_context_takeover");
152                     switch (getPolicy().getBehavior())
153                     {
154                         case CLIENT:
155                             incomingContextTakeover = false;
156                             break;
157                         case SERVER:
158                             outgoingContextTakeover = false;
159                             break;
160                     }
161                     break;
162                 }
163                 case "server_no_context_takeover":
164                 {
165                     configNegotiated.setParameter("server_no_context_takeover");
166                     switch (getPolicy().getBehavior())
167                     {
168                         case CLIENT:
169                             outgoingContextTakeover = false;
170                             break;
171                         case SERVER:
172                             incomingContextTakeover = false;
173                             break;
174                     }
175                     break;
176                 }
177                 default:
178                 {
179                     throw new IllegalArgumentException();
180                 }
181             }
182         }
183         
184         LOG.debug("config: outgoingContextTakover={}, incomingContextTakeover={} : {}", outgoingContextTakeover, incomingContextTakeover, this);
185 
186         super.setConfig(configNegotiated);
187     }
188 
189     @Override
190     public String toString()
191     {
192         return String.format("%s[requested=\"%s\", negotiated=\"%s\"]",
193                 getClass().getSimpleName(),
194                 configRequested.getParameterizedName(),
195                 configNegotiated.getParameterizedName());
196     }
197 }