View Javadoc

1   /*
2    * Copyright 2012 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 = new HashMap<Integer, HttpMessage>();
49  
50      /**
51       * Creates a new instance for the SPDY/2 protocol
52       *
53       * @param maxContentLength the maximum length of the message content.
54       *        If the length of the message content exceeds this value,
55       *        a {@link TooLongFrameException} will be raised.
56       */
57      @Deprecated
58      public SpdyHttpDecoder(int maxContentLength) {
59          this(2, maxContentLength);
60      }
61  
62      /**
63       * Creates a new instance.
64       *
65       * @param version 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       */
70      public SpdyHttpDecoder(int version, int maxContentLength) {
71          if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) {
72              throw new IllegalArgumentException(
73                      "unsupported version: " + version);
74          }
75          if (maxContentLength <= 0) {
76              throw new IllegalArgumentException(
77                      "maxContentLength must be a positive integer: " + maxContentLength);
78          }
79          spdyVersion = version;
80          this.maxContentLength = maxContentLength;
81      }
82  
83      @Override
84      protected Object decode(ChannelHandlerContext ctx, Channel channel, Object msg)
85              throws Exception {
86  
87          if (msg instanceof SpdySynStreamFrame) {
88  
89              // HTTP requests/responses are mapped one-to-one to SPDY streams.
90              SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
91              int streamID = spdySynStreamFrame.getStreamId();
92  
93              if (isServerId(streamID)) {
94                  // SYN_STREAM frames initiated by the server are pushed resources
95                  int associatedToStreamID = spdySynStreamFrame.getAssociatedToStreamId();
96  
97                  // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0
98                  // it must reply with a RST_STREAM with error code INVALID_STREAM
99                  if (associatedToStreamID == 0) {
100                     SpdyRstStreamFrame spdyRstStreamFrame =
101                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.INVALID_STREAM);
102                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
103                 }
104 
105                 String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
106 
107                 // If a client receives a SYN_STREAM without a 'url' header
108                 // it must reply with a RST_STREAM with error code PROTOCOL_ERROR
109                 if (URL == null) {
110                     SpdyRstStreamFrame spdyRstStreamFrame =
111                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
112                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
113                 }
114 
115                 try {
116                     HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame);
117 
118                     // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers
119                     SpdyHttpHeaders.setStreamId(httpResponse, streamID);
120                     SpdyHttpHeaders.setAssociatedToStreamId(httpResponse, associatedToStreamID);
121                     SpdyHttpHeaders.setPriority(httpResponse, spdySynStreamFrame.getPriority());
122                     SpdyHttpHeaders.setUrl(httpResponse, URL);
123 
124                     if (spdySynStreamFrame.isLast()) {
125                         HttpHeaders.setContentLength(httpResponse, 0);
126                         return httpResponse;
127                     } else {
128                         // Response body will follow in a series of Data Frames
129                         messageMap.put(streamID, httpResponse);
130                     }
131                 } catch (Exception e) {
132                     SpdyRstStreamFrame spdyRstStreamFrame =
133                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
134                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
135                 }
136             } else {
137                 // SYN_STREAM frames initiated by the client are HTTP requests
138                 try {
139                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
140 
141                     // Set the Stream-ID as a header
142                     SpdyHttpHeaders.setStreamId(httpRequest, streamID);
143 
144                     if (spdySynStreamFrame.isLast()) {
145                         return httpRequest;
146                     } else {
147                         // Request body will follow in a series of Data Frames
148                         messageMap.put(streamID, httpRequest);
149                     }
150                 } catch (Exception e) {
151                     // If a client sends a SYN_STREAM without all of the method, url (host and path),
152                     // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
153                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
154                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID);
155                     spdySynReplyFrame.setLast(true);
156                     SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
157                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
158                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
159                 }
160             }
161 
162         } else if (msg instanceof SpdySynReplyFrame) {
163 
164             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
165             int streamID = spdySynReplyFrame.getStreamId();
166 
167             try {
168                 HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame);
169 
170                 // Set the Stream-ID as a header
171                 SpdyHttpHeaders.setStreamId(httpResponse, streamID);
172 
173                 if (spdySynReplyFrame.isLast()) {
174                     HttpHeaders.setContentLength(httpResponse, 0);
175                     return httpResponse;
176                 } else {
177                     // Response body will follow in a series of Data Frames
178                     messageMap.put(streamID, httpResponse);
179                 }
180             } catch (Exception e) {
181                 // If a client receives a SYN_REPLY without valid status and version headers
182                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
183                 SpdyRstStreamFrame spdyRstStreamFrame =
184                     new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
185                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
186             }
187 
188         } else if (msg instanceof SpdyHeadersFrame) {
189 
190             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
191             Integer streamID = spdyHeadersFrame.getStreamId();
192             HttpMessage httpMessage = messageMap.get(streamID);
193 
194             // If message is not in map discard HEADERS frame.
195             // SpdySessionHandler should prevent this from happening.
196             if (httpMessage == null) {
197                 return null;
198             }
199 
200             for (Map.Entry<String, String> e: spdyHeadersFrame.getHeaders()) {
201                 httpMessage.addHeader(e.getKey(), e.getValue());
202             }
203 
204         } else if (msg instanceof SpdyDataFrame) {
205 
206             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
207             Integer streamID = spdyDataFrame.getStreamId();
208             HttpMessage httpMessage = messageMap.get(streamID);
209 
210             // If message is not in map discard Data Frame.
211             // SpdySessionHandler should prevent this from happening.
212             if (httpMessage == null) {
213                 return null;
214             }
215 
216             ChannelBuffer content = httpMessage.getContent();
217             if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) {
218                 messageMap.remove(streamID);
219                 throw new TooLongFrameException(
220                         "HTTP content length exceeded " + maxContentLength + " bytes.");
221             }
222 
223             if (content == ChannelBuffers.EMPTY_BUFFER) {
224                 content = ChannelBuffers.dynamicBuffer(channel.getConfig().getBufferFactory());
225                 content.writeBytes(spdyDataFrame.getData());
226                 httpMessage.setContent(content);
227             } else {
228                 content.writeBytes(spdyDataFrame.getData());
229             }
230 
231             if (spdyDataFrame.isLast()) {
232                 HttpHeaders.setContentLength(httpMessage, content.readableBytes());
233                 messageMap.remove(streamID);
234                 return httpMessage;
235             }
236 
237         } else if (msg instanceof SpdyRstStreamFrame) {
238 
239             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
240             Integer streamID = spdyRstStreamFrame.getStreamId();
241             messageMap.remove(streamID);
242         }
243 
244         return null;
245     }
246 
247     private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeaderBlock requestFrame)
248             throws Exception {
249         // Create the first line of the request from the name/value pairs
250         HttpMethod  method      = SpdyHeaders.getMethod(spdyVersion, requestFrame);
251         String      url         = SpdyHeaders.getUrl(spdyVersion, requestFrame);
252         HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame);
253         SpdyHeaders.removeMethod(spdyVersion, requestFrame);
254         SpdyHeaders.removeUrl(spdyVersion, requestFrame);
255         SpdyHeaders.removeVersion(spdyVersion, requestFrame);
256 
257         HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url);
258 
259         // Remove the scheme header
260         SpdyHeaders.removeScheme(spdyVersion, requestFrame);
261 
262         if (spdyVersion >= 3) {
263             // Replace the SPDY host header with the HTTP host header
264             String host = SpdyHeaders.getHost(requestFrame);
265             SpdyHeaders.removeHost(requestFrame);
266             HttpHeaders.setHost(httpRequest, host);
267         }
268 
269         for (Map.Entry<String, String> e: requestFrame.getHeaders()) {
270             httpRequest.addHeader(e.getKey(), e.getValue());
271         }
272 
273         // The Connection and Keep-Alive headers are no longer valid
274         HttpHeaders.setKeepAlive(httpRequest, true);
275 
276         // Transfer-Encoding header is not valid
277         httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
278 
279         return httpRequest;
280     }
281 
282     private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeaderBlock responseFrame)
283             throws Exception {
284         // Create the first line of the response from the name/value pairs
285         HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame);
286         HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame);
287         SpdyHeaders.removeStatus(spdyVersion, responseFrame);
288         SpdyHeaders.removeVersion(spdyVersion, responseFrame);
289 
290         HttpResponse httpResponse = new DefaultHttpResponse(version, status);
291         for (Map.Entry<String, String> e: responseFrame.getHeaders()) {
292             httpResponse.addHeader(e.getKey(), e.getValue());
293         }
294 
295         // The Connection and Keep-Alive headers are no longer valid
296         HttpHeaders.setKeepAlive(httpResponse, true);
297 
298         // Transfer-Encoding header is not valid
299         httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
300         httpResponse.removeHeader(HttpHeaders.Names.TRAILER);
301 
302         return httpResponse;
303     }
304 }