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                 }
117 
118                 String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
119 
120                 // If a client receives a SYN_STREAM without a 'url' header
121                 // it must reply with a RST_STREAM with error code PROTOCOL_ERROR
122                 if (URL == null) {
123                     SpdyRstStreamFrame spdyRstStreamFrame =
124                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
125                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
126                 }
127 
128                 // If a client receives a response with a truncated header block,
129                 // reply with a RST_STREAM with error code INTERNAL_ERROR.
130                 if (spdySynStreamFrame.isTruncated()) {
131                     SpdyRstStreamFrame spdyRstStreamFrame =
132                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
133                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
134                 }
135 
136                 try {
137                     HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame);
138 
139                     // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers
140                     SpdyHttpHeaders.setStreamId(httpResponse, streamId);
141                     SpdyHttpHeaders.setAssociatedToStreamId(httpResponse, associatedToStreamId);
142                     SpdyHttpHeaders.setPriority(httpResponse, spdySynStreamFrame.getPriority());
143                     SpdyHttpHeaders.setUrl(httpResponse, URL);
144 
145                     if (spdySynStreamFrame.isLast()) {
146                         HttpHeaders.setContentLength(httpResponse, 0);
147                         return httpResponse;
148                     } else {
149                         // Response body will follow in a series of Data Frames
150                         putMessage(streamId, httpResponse);
151                     }
152                 } catch (Exception e) {
153                     SpdyRstStreamFrame spdyRstStreamFrame =
154                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
155                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
156                 }
157             } else {
158                 // SYN_STREAM frames initiated by the client are HTTP requests
159 
160                 // If a client sends a request with a truncated header block, the server must
161                 // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply.
162                 if (spdySynStreamFrame.isTruncated()) {
163                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
164                     spdySynReplyFrame.setLast(true);
165                     SpdyHeaders.setStatus(spdyVersion,
166                             spdySynReplyFrame,
167                             HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE);
168                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
169                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
170                 }
171 
172                 try {
173                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
174 
175                     // Set the Stream-ID as a header
176                     SpdyHttpHeaders.setStreamId(httpRequest, streamId);
177 
178                     if (spdySynStreamFrame.isLast()) {
179                         return httpRequest;
180                     } else {
181                         // Request body will follow in a series of Data Frames
182                         putMessage(streamId, httpRequest);
183                     }
184                 } catch (Exception e) {
185                     // If a client sends a SYN_STREAM without all of the method, url (host and path),
186                     // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
187                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
188                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
189                     spdySynReplyFrame.setLast(true);
190                     SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
191                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
192                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
193                 }
194             }
195 
196         } else if (msg instanceof SpdySynReplyFrame) {
197 
198             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
199             int streamId = spdySynReplyFrame.getStreamId();
200 
201             // If a client receives a SYN_REPLY with a truncated header block,
202             // reply with a RST_STREAM frame with error code INTERNAL_ERROR.
203             if (spdySynReplyFrame.isTruncated()) {
204                 SpdyRstStreamFrame spdyRstStreamFrame =
205                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
206                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
207             }
208 
209             try {
210                 HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame);
211 
212                 // Set the Stream-ID as a header
213                 SpdyHttpHeaders.setStreamId(httpResponse, streamId);
214 
215                 if (spdySynReplyFrame.isLast()) {
216                     HttpHeaders.setContentLength(httpResponse, 0);
217                     return httpResponse;
218                 } else {
219                     // Response body will follow in a series of Data Frames
220                     putMessage(streamId, httpResponse);
221                 }
222             } catch (Exception e) {
223                 // If a client receives a SYN_REPLY without valid status and version headers
224                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
225                 SpdyRstStreamFrame spdyRstStreamFrame =
226                     new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
227                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
228             }
229 
230         } else if (msg instanceof SpdyHeadersFrame) {
231 
232             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
233             int streamId = spdyHeadersFrame.getStreamId();
234             HttpMessage httpMessage = getMessage(streamId);
235 
236             // If message is not in map discard HEADERS frame.
237             if (httpMessage == null) {
238                 return null;
239             }
240 
241             // Ignore trailers in a truncated HEADERS frame.
242             if (!spdyHeadersFrame.isTruncated()) {
243                 for (Map.Entry<String, String> e : spdyHeadersFrame.headers()) {
244                     httpMessage.headers().add(e.getKey(), e.getValue());
245                 }
246             }
247 
248             if (spdyHeadersFrame.isLast()) {
249                 HttpHeaders.setContentLength(httpMessage, httpMessage.getContent().readableBytes());
250                 removeMessage(streamId);
251                 return httpMessage;
252             }
253 
254         } else if (msg instanceof SpdyDataFrame) {
255 
256             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
257             int streamId = spdyDataFrame.getStreamId();
258             HttpMessage httpMessage = getMessage(streamId);
259 
260             // If message is not in map discard Data Frame.
261             if (httpMessage == null) {
262                 return null;
263             }
264 
265             ChannelBuffer content = httpMessage.getContent();
266             if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) {
267                 removeMessage(streamId);
268                 throw new TooLongFrameException(
269                         "HTTP content length exceeded " + maxContentLength + " bytes.");
270             }
271 
272             if (content == ChannelBuffers.EMPTY_BUFFER) {
273                 content = ChannelBuffers.dynamicBuffer(channel.getConfig().getBufferFactory());
274                 content.writeBytes(spdyDataFrame.getData());
275                 httpMessage.setContent(content);
276             } else {
277                 content.writeBytes(spdyDataFrame.getData());
278             }
279 
280             if (spdyDataFrame.isLast()) {
281                 HttpHeaders.setContentLength(httpMessage, content.readableBytes());
282                 removeMessage(streamId);
283                 return httpMessage;
284             }
285 
286         } else if (msg instanceof SpdyRstStreamFrame) {
287 
288             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
289             int streamId = spdyRstStreamFrame.getStreamId();
290             removeMessage(streamId);
291         }
292 
293         return null;
294     }
295 
296     private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeadersFrame requestFrame)
297             throws Exception {
298         // Create the first line of the request from the name/value pairs
299         HttpMethod  method      = SpdyHeaders.getMethod(spdyVersion, requestFrame);
300         String      url         = SpdyHeaders.getUrl(spdyVersion, requestFrame);
301         HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame);
302         SpdyHeaders.removeMethod(spdyVersion, requestFrame);
303         SpdyHeaders.removeUrl(spdyVersion, requestFrame);
304         SpdyHeaders.removeVersion(spdyVersion, requestFrame);
305 
306         HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url);
307 
308         // Remove the scheme header
309         SpdyHeaders.removeScheme(spdyVersion, requestFrame);
310 
311         if (spdyVersion >= 3) {
312             // Replace the SPDY host header with the HTTP host header
313             String host = SpdyHeaders.getHost(requestFrame);
314             SpdyHeaders.removeHost(requestFrame);
315             HttpHeaders.setHost(httpRequest, host);
316         }
317 
318         for (Map.Entry<String, String> e: requestFrame.headers()) {
319             httpRequest.headers().add(e.getKey(), e.getValue());
320         }
321 
322         // The Connection and Keep-Alive headers are no longer valid
323         HttpHeaders.setKeepAlive(httpRequest, true);
324 
325         // Transfer-Encoding header is not valid
326         httpRequest.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING);
327 
328         return httpRequest;
329     }
330 
331     private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeadersFrame responseFrame)
332             throws Exception {
333         // Create the first line of the response from the name/value pairs
334         HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame);
335         HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame);
336         SpdyHeaders.removeStatus(spdyVersion, responseFrame);
337         SpdyHeaders.removeVersion(spdyVersion, responseFrame);
338 
339         HttpResponse httpResponse = new DefaultHttpResponse(version, status);
340         for (Map.Entry<String, String> e: responseFrame.headers()) {
341             httpResponse.headers().add(e.getKey(), e.getValue());
342         }
343 
344         // The Connection and Keep-Alive headers are no longer valid
345         HttpHeaders.setKeepAlive(httpResponse, true);
346 
347         // Transfer-Encoding header is not valid
348         httpResponse.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING);
349         httpResponse.headers().remove(HttpHeaders.Names.TRAILER);
350 
351         return httpResponse;
352     }
353 }