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