View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package org.jboss.netty.handler.codec.spdy;
17  
18  import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*;
19  
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import org.jboss.netty.buffer.ChannelBuffer;
24  import org.jboss.netty.buffer.ChannelBuffers;
25  import org.jboss.netty.channel.Channel;
26  import org.jboss.netty.channel.ChannelHandlerContext;
27  import org.jboss.netty.channel.Channels;
28  import org.jboss.netty.handler.codec.frame.TooLongFrameException;
29  import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
30  import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
31  import org.jboss.netty.handler.codec.http.HttpHeaders;
32  import org.jboss.netty.handler.codec.http.HttpMessage;
33  import org.jboss.netty.handler.codec.http.HttpMethod;
34  import org.jboss.netty.handler.codec.http.HttpRequest;
35  import org.jboss.netty.handler.codec.http.HttpResponse;
36  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
37  import org.jboss.netty.handler.codec.http.HttpVersion;
38  import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
39  
40  /**
41   * Decodes {@link SpdySynStreamFrame}s, {@link SpdySynReplyFrame}s,
42   * and {@link SpdyDataFrame}s into {@link HttpRequest}s and {@link HttpResponse}s.
43   */
44  public class SpdyHttpDecoder extends OneToOneDecoder {
45  
46      private final int spdyVersion;
47      private final int maxContentLength;
48      private final Map<Integer, HttpMessage> messageMap;
49  
50      /**
51       * Creates a new instance.
52       *
53       * @param spdyVersion the protocol version
54       * @param maxContentLength the maximum length of the message content.
55       *        If the length of the message content exceeds this value,
56       *        a {@link TooLongFrameException} will be raised.
57       */
58      public SpdyHttpDecoder(SpdyVersion spdyVersion, int maxContentLength) {
59          this(spdyVersion, maxContentLength, new HashMap<Integer, HttpMessage>());
60      }
61  
62      /**
63       * Creates a new instance with the specified parameters.
64       *
65       * @param spdyVersion the protocol version
66       * @param maxContentLength the maximum length of the message content.
67       *        If the length of the message content exceeds this value,
68       *        a {@link TooLongFrameException} will be raised.
69       * @param messageMap the {@link Map} used to hold partially received messages.
70       */
71      protected SpdyHttpDecoder(SpdyVersion spdyVersion, int maxContentLength, Map<Integer, HttpMessage> messageMap) {
72          if (spdyVersion == null) {
73              throw new NullPointerException("spdyVersion");
74          }
75          if (maxContentLength <= 0) {
76              throw new IllegalArgumentException(
77                      "maxContentLength must be a positive integer: " + maxContentLength);
78          }
79          this.spdyVersion = spdyVersion.getVersion();
80          this.maxContentLength = maxContentLength;
81          this.messageMap = messageMap;
82      }
83  
84      protected HttpMessage putMessage(int streamId, HttpMessage message) {
85          return messageMap.put(streamId, message);
86      }
87  
88      protected HttpMessage getMessage(int streamId) {
89          return messageMap.get(streamId);
90      }
91  
92      protected HttpMessage removeMessage(int streamId) {
93          return messageMap.remove(streamId);
94      }
95  
96      @Override
97      protected Object decode(ChannelHandlerContext ctx, Channel channel, Object msg)
98              throws Exception {
99  
100         if (msg instanceof SpdySynStreamFrame) {
101 
102             // HTTP requests/responses are mapped one-to-one to SPDY streams.
103             SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
104             int streamId = spdySynStreamFrame.getStreamId();
105 
106             if (isServerId(streamId)) {
107                 // SYN_STREAM frames initiated by the server are pushed resources
108                 int associatedToStreamId = spdySynStreamFrame.getAssociatedToStreamId();
109 
110                 // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0
111                 // it must reply with a RST_STREAM with error code INVALID_STREAM.
112                 if (associatedToStreamId == 0) {
113                     SpdyRstStreamFrame spdyRstStreamFrame =
114                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM);
115                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
116                     return null;
117                 }
118 
119                 // If a client receives a SYN_STREAM with isLast set,
120                 // reply with a RST_STREAM with error code PROTOCOL_ERROR
121                 // (we only support pushed resources divided into two header blocks).
122                 if (spdySynStreamFrame.isLast()) {
123                     SpdyRstStreamFrame spdyRstStreamFrame =
124                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
125                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
126                     return null;
127                 }
128 
129                 // If a client receives a response with a truncated header block,
130                 // reply with a RST_STREAM with error code INTERNAL_ERROR.
131                 if (spdySynStreamFrame.isTruncated()) {
132                     SpdyRstStreamFrame spdyRstStreamFrame =
133                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
134                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
135                     return null;
136                 }
137 
138                 try {
139                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
140 
141                     // Set the Stream-ID, Associated-To-Stream-ID, and Priority as headers
142                     SpdyHttpHeaders.setStreamId(httpRequest, streamId);
143                     SpdyHttpHeaders.setAssociatedToStreamId(httpRequest, associatedToStreamId);
144                     SpdyHttpHeaders.setPriority(httpRequest, spdySynStreamFrame.getPriority());
145 
146                     return httpRequest;
147 
148                 } catch (Exception e) {
149                     SpdyRstStreamFrame spdyRstStreamFrame =
150                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
151                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
152                 }
153             } else {
154                 // SYN_STREAM frames initiated by the client are HTTP requests
155 
156                 // If a client sends a request with a truncated header block, the server must
157                 // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply.
158                 if (spdySynStreamFrame.isTruncated()) {
159                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
160                     spdySynReplyFrame.setLast(true);
161                     SpdyHeaders.setStatus(spdyVersion,
162                             spdySynReplyFrame,
163                             HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE);
164                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
165                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
166                     return null;
167                 }
168 
169                 try {
170                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
171 
172                     // Set the Stream-ID as a header
173                     SpdyHttpHeaders.setStreamId(httpRequest, streamId);
174 
175                     if (spdySynStreamFrame.isLast()) {
176                         return httpRequest;
177                     } else {
178                         // Request body will follow in a series of Data Frames
179                         putMessage(streamId, httpRequest);
180                     }
181                 } catch (Exception e) {
182                     // If a client sends a SYN_STREAM without all of the method, url (host and path),
183                     // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
184                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
185                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
186                     spdySynReplyFrame.setLast(true);
187                     SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
188                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
189                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
190                 }
191             }
192 
193         } else if (msg instanceof SpdySynReplyFrame) {
194 
195             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
196             int streamId = spdySynReplyFrame.getStreamId();
197 
198             // If a client receives a SYN_REPLY with a truncated header block,
199             // reply with a RST_STREAM frame with error code INTERNAL_ERROR.
200             if (spdySynReplyFrame.isTruncated()) {
201                 SpdyRstStreamFrame spdyRstStreamFrame =
202                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
203                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
204                 return null;
205             }
206 
207             try {
208                 HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame);
209 
210                 // Set the Stream-ID as a header
211                 SpdyHttpHeaders.setStreamId(httpResponse, streamId);
212 
213                 if (spdySynReplyFrame.isLast()) {
214                     HttpHeaders.setContentLength(httpResponse, 0);
215                     return httpResponse;
216                 } else {
217                     // Response body will follow in a series of Data Frames
218                     putMessage(streamId, httpResponse);
219                 }
220             } catch (Exception e) {
221                 // If a client receives a SYN_REPLY without valid status and version headers
222                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
223                 SpdyRstStreamFrame spdyRstStreamFrame =
224                     new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
225                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
226             }
227 
228         } else if (msg instanceof SpdyHeadersFrame) {
229 
230             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
231             int streamId = spdyHeadersFrame.getStreamId();
232             HttpMessage httpMessage = getMessage(streamId);
233 
234             if (httpMessage == null) {
235                 // HEADERS frames may initiate a pushed response
236                 if (isServerId(streamId)) {
237 
238                     // If a client receives a HEADERS with a truncated header block,
239                     // reply with a RST_STREAM frame with error code INTERNAL_ERROR.
240                     if (spdyHeadersFrame.isTruncated()) {
241                         SpdyRstStreamFrame spdyRstStreamFrame =
242                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
243                         Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
244                         return null;
245                     }
246 
247                     try {
248                         httpMessage = createHttpResponse(spdyVersion, spdyHeadersFrame);
249 
250                         // Set the Stream-ID as a header
251                         SpdyHttpHeaders.setStreamId(httpMessage, streamId);
252 
253                         if (spdyHeadersFrame.isLast()) {
254                             HttpHeaders.setContentLength(httpMessage, 0);
255                             return httpMessage;
256                         } else {
257                             // Response body will follow in a series of Data Frames
258                             putMessage(streamId, httpMessage);
259                         }
260                     } catch (Exception e) {
261                         // If a client receives a HEADERS without valid status and version headers
262                         // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
263                         SpdyRstStreamFrame spdyRstStreamFrame =
264                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
265                         Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
266                         return null;
267                     }
268                 }
269                 return null;
270             }
271 
272             // Ignore trailers in a truncated HEADERS frame.
273             if (!spdyHeadersFrame.isTruncated()) {
274                 for (Map.Entry<String, String> e : spdyHeadersFrame.headers()) {
275                     httpMessage.headers().add(e.getKey(), e.getValue());
276                 }
277             }
278 
279             if (spdyHeadersFrame.isLast()) {
280                 HttpHeaders.setContentLength(httpMessage, httpMessage.getContent().readableBytes());
281                 removeMessage(streamId);
282                 return httpMessage;
283             }
284 
285         } else if (msg instanceof SpdyDataFrame) {
286 
287             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
288             int streamId = spdyDataFrame.getStreamId();
289             HttpMessage httpMessage = getMessage(streamId);
290 
291             // If message is not in map discard Data Frame.
292             if (httpMessage == null) {
293                 return null;
294             }
295 
296             ChannelBuffer content = httpMessage.getContent();
297             if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) {
298                 removeMessage(streamId);
299                 throw new TooLongFrameException(
300                         "HTTP content length exceeded " + maxContentLength + " bytes.");
301             }
302 
303             if (content == ChannelBuffers.EMPTY_BUFFER) {
304                 content = ChannelBuffers.dynamicBuffer(channel.getConfig().getBufferFactory());
305                 content.writeBytes(spdyDataFrame.getData());
306                 httpMessage.setContent(content);
307             } else {
308                 content.writeBytes(spdyDataFrame.getData());
309             }
310 
311             if (spdyDataFrame.isLast()) {
312                 HttpHeaders.setContentLength(httpMessage, content.readableBytes());
313                 removeMessage(streamId);
314                 return httpMessage;
315             }
316 
317         } else if (msg instanceof SpdyRstStreamFrame) {
318 
319             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
320             int streamId = spdyRstStreamFrame.getStreamId();
321             removeMessage(streamId);
322         }
323 
324         return null;
325     }
326 
327     private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeadersFrame requestFrame)
328             throws Exception {
329         // Create the first line of the request from the name/value pairs
330         HttpMethod  method      = SpdyHeaders.getMethod(spdyVersion, requestFrame);
331         String      url         = SpdyHeaders.getUrl(spdyVersion, requestFrame);
332         HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame);
333         SpdyHeaders.removeMethod(spdyVersion, requestFrame);
334         SpdyHeaders.removeUrl(spdyVersion, requestFrame);
335         SpdyHeaders.removeVersion(spdyVersion, requestFrame);
336 
337         HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url);
338 
339         // Remove the scheme header
340         SpdyHeaders.removeScheme(spdyVersion, requestFrame);
341 
342         // Replace the SPDY host header with the HTTP host header
343         String host = SpdyHeaders.getHost(requestFrame);
344         SpdyHeaders.removeHost(requestFrame);
345         HttpHeaders.setHost(httpRequest, host);
346 
347         for (Map.Entry<String, String> e: requestFrame.headers()) {
348             httpRequest.headers().add(e.getKey(), e.getValue());
349         }
350 
351         // The Connection and Keep-Alive headers are no longer valid
352         HttpHeaders.setKeepAlive(httpRequest, true);
353 
354         // Transfer-Encoding header is not valid
355         httpRequest.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING);
356 
357         return httpRequest;
358     }
359 
360     private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeadersFrame responseFrame)
361             throws Exception {
362         // Create the first line of the response from the name/value pairs
363         HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame);
364         HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame);
365         SpdyHeaders.removeStatus(spdyVersion, responseFrame);
366         SpdyHeaders.removeVersion(spdyVersion, responseFrame);
367 
368         HttpResponse httpResponse = new DefaultHttpResponse(version, status);
369         for (Map.Entry<String, String> e: responseFrame.headers()) {
370             httpResponse.headers().add(e.getKey(), e.getValue());
371         }
372 
373         // The Connection and Keep-Alive headers are no longer valid
374         HttpHeaders.setKeepAlive(httpResponse, true);
375 
376         // Transfer-Encoding header is not valid
377         httpResponse.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING);
378         httpResponse.headers().remove(HttpHeaders.Names.TRAILER);
379 
380         return httpResponse;
381     }
382 }