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