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