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 
137             } else {
138                 // SYN_STREAM frames initiated by the client are HTTP requests
139                 try {
140                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
141 
142                     // Set the Stream-ID as a header
143                     SpdyHttpHeaders.setStreamId(httpRequest, streamID);
144 
145                     if (spdySynStreamFrame.isLast()) {
146                         return httpRequest;
147                     } else {
148                         // Request body will follow in a series of Data Frames
149                         messageMap.put(streamID, httpRequest);
150                     }
151                 } catch (Exception e) {
152                     // If a client sends a SYN_STREAM without all of the method, url (host and path),
153                     // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
154                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
155                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID);
156                     spdySynReplyFrame.setLast(true);
157                     SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
158                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
159                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
160                 }
161             }
162 
163         } else if (msg instanceof SpdySynReplyFrame) {
164 
165             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
166             int streamID = spdySynReplyFrame.getStreamId();
167 
168             try {
169                 HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame);
170 
171                 // Set the Stream-ID as a header
172                 SpdyHttpHeaders.setStreamId(httpResponse, streamID);
173 
174                 if (spdySynReplyFrame.isLast()) {
175                     HttpHeaders.setContentLength(httpResponse, 0);
176                     return httpResponse;
177                 } else {
178                     // Response body will follow in a series of Data Frames
179                     messageMap.put(streamID, httpResponse);
180                 }
181             } catch (Exception e) {
182                 // If a client receives a SYN_REPLY without valid status and version headers
183                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
184                 SpdyRstStreamFrame spdyRstStreamFrame =
185                     new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
186                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
187             }
188 
189         } else if (msg instanceof SpdyHeadersFrame) {
190 
191             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
192             Integer streamID = spdyHeadersFrame.getStreamId();
193             HttpMessage httpMessage = messageMap.get(streamID);
194 
195             // If message is not in map discard HEADERS frame.
196             // SpdySessionHandler should prevent this from happening.
197             if (httpMessage == null) {
198                 return null;
199             }
200 
201             for (Map.Entry<String, String> e: spdyHeadersFrame.getHeaders()) {
202                 httpMessage.addHeader(e.getKey(), e.getValue());
203             }
204 
205         } else if (msg instanceof SpdyDataFrame) {
206 
207             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
208             Integer streamID = spdyDataFrame.getStreamId();
209             HttpMessage httpMessage = messageMap.get(streamID);
210 
211             // If message is not in map discard Data Frame.
212             // SpdySessionHandler should prevent this from happening.
213             if (httpMessage == null) {
214                 return null;
215             }
216 
217             ChannelBuffer content = httpMessage.getContent();
218             if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) {
219                 messageMap.remove(streamID);
220                 throw new TooLongFrameException(
221                         "HTTP content length exceeded " + maxContentLength + " bytes.");
222             }
223 
224             if (content == ChannelBuffers.EMPTY_BUFFER) {
225                 content = ChannelBuffers.dynamicBuffer(channel.getConfig().getBufferFactory());
226                 content.writeBytes(spdyDataFrame.getData());
227                 httpMessage.setContent(content);
228             } else {
229                 content.writeBytes(spdyDataFrame.getData());
230             }
231 
232             if (spdyDataFrame.isLast()) {
233                 HttpHeaders.setContentLength(httpMessage, content.readableBytes());
234                 messageMap.remove(streamID);
235                 return httpMessage;
236             }
237 
238         } else if (msg instanceof SpdyRstStreamFrame) {
239 
240             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
241             Integer streamID = spdyRstStreamFrame.getStreamId();
242             messageMap.remove(streamID);
243         }
244 
245         return null;
246     }
247 
248     private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeaderBlock requestFrame)
249             throws Exception {
250         // Create the first line of the request from the name/value pairs
251         HttpMethod  method      = SpdyHeaders.getMethod(spdyVersion, requestFrame);
252         String      url         = SpdyHeaders.getUrl(spdyVersion, requestFrame);
253         HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame);
254         SpdyHeaders.removeMethod(spdyVersion, requestFrame);
255         SpdyHeaders.removeUrl(spdyVersion, requestFrame);
256         SpdyHeaders.removeVersion(spdyVersion, requestFrame);
257 
258         HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url);
259 
260         // Remove the scheme header
261         SpdyHeaders.removeScheme(spdyVersion, requestFrame);
262 
263         if (spdyVersion >= 3) {
264             // Replace the SPDY host header with the HTTP host header
265             String host = SpdyHeaders.getHost(requestFrame);
266             SpdyHeaders.removeHost(requestFrame);
267             HttpHeaders.setHost(httpRequest, host);
268         }
269 
270         for (Map.Entry<String, String> e: requestFrame.getHeaders()) {
271             httpRequest.addHeader(e.getKey(), e.getValue());
272         }
273 
274         // The Connection and Keep-Alive headers are no longer valid
275         HttpHeaders.setKeepAlive(httpRequest, true);
276 
277         // Transfer-Encoding header is not valid
278         httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
279 
280         return httpRequest;
281     }
282 
283     private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeaderBlock responseFrame)
284             throws Exception {
285         // Create the first line of the response from the name/value pairs
286         HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame);
287         HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame);
288         SpdyHeaders.removeStatus(spdyVersion, responseFrame);
289         SpdyHeaders.removeVersion(spdyVersion, responseFrame);
290 
291         HttpResponse httpResponse = new DefaultHttpResponse(version, status);
292         for (Map.Entry<String, String> e: responseFrame.getHeaders()) {
293             httpResponse.addHeader(e.getKey(), e.getValue());
294         }
295 
296         // The Connection and Keep-Alive headers are no longer valid
297         HttpHeaders.setKeepAlive(httpResponse, true);
298 
299         // Transfer-Encoding header is not valid
300         httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
301         httpResponse.removeHeader(HttpHeaders.Names.TRAILER);
302 
303         return httpResponse;
304     }
305 }