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