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