View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.spdy.parser;
20  
21  import java.nio.ByteBuffer;
22  import java.nio.charset.StandardCharsets;
23  import java.util.zip.ZipException;
24  
25  import org.eclipse.jetty.spdy.CompressionDictionary;
26  import org.eclipse.jetty.spdy.CompressionFactory;
27  import org.eclipse.jetty.spdy.SessionException;
28  import org.eclipse.jetty.spdy.StreamException;
29  import org.eclipse.jetty.spdy.api.SPDY;
30  import org.eclipse.jetty.spdy.api.SessionStatus;
31  import org.eclipse.jetty.spdy.api.StreamStatus;
32  
33  public abstract class HeadersBlockParser
34  {
35      private final CompressionFactory.Decompressor decompressor;
36      private byte[] data;
37      private boolean needsDictionary = true;
38  
39      protected HeadersBlockParser(CompressionFactory.Decompressor decompressor)
40      {
41          this.decompressor = decompressor;
42      }
43  
44      public boolean parse(int streamId, short version, int length, ByteBuffer buffer)
45      {
46          // Need to be sure that all the compressed data has arrived
47          // Because SPDY uses SYNC_FLUSH mode, and the Java API
48          // does not expose when decompression is finished with this mode
49          // (but only when using NO_FLUSH), then we need to
50          // accumulate the compressed bytes until we have all of them
51  
52          boolean accumulated = accumulate(length, buffer);
53          if (!accumulated)
54              return false;
55  
56          byte[] compressedHeaders = data;
57          data = null;
58          ByteBuffer decompressedHeaders = decompress(version, compressedHeaders);
59  
60          // We know the decoded bytes contain the full headers,
61          // so optimize instead of looping byte by byte
62          int count = readCount(version, decompressedHeaders);
63          for (int i = 0; i < count; ++i)
64          {
65              int nameLength = readNameLength(version, decompressedHeaders);
66              if (nameLength == 0)
67                  throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid header name length");
68              byte[] nameBytes = new byte[nameLength];
69              decompressedHeaders.get(nameBytes);
70              String name = new String(nameBytes, StandardCharsets.ISO_8859_1);
71  
72              int valueLength = readValueLength(version, decompressedHeaders);
73              if (valueLength == 0)
74                  throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid header value length");
75              byte[] valueBytes = new byte[valueLength];
76              decompressedHeaders.get(valueBytes);
77              String value = new String(valueBytes, StandardCharsets.ISO_8859_1);
78              // Multi valued headers are separate by NUL
79              String[] values = value.split("\u0000");
80              // Check if there are multiple NULs (section 2.6.9)
81              for (String v : values)
82                  if (v.length() == 0)
83                      throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid multi valued header");
84  
85              onHeader(name, values);
86          }
87  
88          return true;
89      }
90  
91      private boolean accumulate(int length, ByteBuffer buffer)
92      {
93          int remaining = buffer.remaining();
94          if (data == null)
95          {
96              if (remaining < length)
97              {
98                  data = new byte[remaining];
99                  buffer.get(data);
100                 return false;
101             }
102             else
103             {
104                 data = new byte[length];
105                 buffer.get(data);
106                 return true;
107             }
108         }
109         else
110         {
111             int accumulated = data.length;
112             int needed = length - accumulated;
113             if (remaining < needed)
114             {
115                 byte[] local = new byte[accumulated + remaining];
116                 System.arraycopy(data, 0, local, 0, accumulated);
117                 buffer.get(local, accumulated, remaining);
118                 data = local;
119                 return false;
120             }
121             else
122             {
123                 byte[] local = new byte[length];
124                 System.arraycopy(data, 0, local, 0, accumulated);
125                 buffer.get(local, accumulated, needed);
126                 data = local;
127                 return true;
128             }
129         }
130     }
131 
132     private int readCount(int version, ByteBuffer buffer)
133     {
134         switch (version)
135         {
136             case SPDY.V2:
137                 return buffer.getShort();
138             case SPDY.V3:
139                 return buffer.getInt();
140             default:
141                 throw new IllegalStateException();
142         }
143     }
144 
145     private int readNameLength(int version, ByteBuffer buffer)
146     {
147         return readCount(version, buffer);
148     }
149 
150     private int readValueLength(int version, ByteBuffer buffer)
151     {
152         return readCount(version, buffer);
153     }
154 
155     protected abstract void onHeader(String name, String[] values);
156 
157     private ByteBuffer decompress(short version, byte[] compressed)
158     {
159         // Differently from compression, decompression always happens
160         // non-concurrently because we read and parse with a single
161         // thread, and therefore there is no need for synchronization.
162 
163         try
164         {
165             byte[] decompressed = null;
166             byte[] buffer = new byte[compressed.length * 2];
167             decompressor.setInput(compressed);
168 
169             while (true)
170             {
171                 int count = decompressor.decompress(buffer);
172                 if (count == 0)
173                 {
174                     if (decompressed != null)
175                     {
176                         return ByteBuffer.wrap(decompressed);
177                     }
178                     else if (needsDictionary)
179                     {
180                         decompressor.setDictionary(CompressionDictionary.get(version));
181                         needsDictionary = false;
182                     }
183                     else
184                     {
185                         throw new IllegalStateException();
186                     }
187                 }
188                 else
189                 {
190                     if (count < buffer.length)
191                     {
192                         if (decompressed == null)
193                         {
194                             // Only one pass was needed to decompress
195                             return ByteBuffer.wrap(buffer, 0, count);
196                         }
197                         else
198                         {
199                             // Last pass needed to decompress, merge decompressed bytes
200                             byte[] result = new byte[decompressed.length + count];
201                             System.arraycopy(decompressed, 0, result, 0, decompressed.length);
202                             System.arraycopy(buffer, 0, result, decompressed.length, count);
203                             return ByteBuffer.wrap(result);
204                         }
205                     }
206                     else
207                     {
208                         if (decompressed == null)
209                         {
210                             decompressed = buffer;
211                             buffer = new byte[buffer.length];
212                         }
213                         else
214                         {
215                             byte[] result = new byte[decompressed.length + buffer.length];
216                             System.arraycopy(decompressed, 0, result, 0, decompressed.length);
217                             System.arraycopy(buffer, 0, result, decompressed.length, buffer.length);
218                             decompressed = result;
219                         }
220                     }
221                 }
222             }
223         }
224         catch (ZipException x)
225         {
226             // We had a compression problem, and since the compression context
227             // is per-connection, we need to tear down the connection
228             throw new SessionException(SessionStatus.PROTOCOL_ERROR, x);
229         }
230     }
231 }