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