View Javadoc

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