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