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         super(SpdyFrame.class);
135         spdyVersion = ObjectUtil.checkNotNull(version, "version").version();
136         this.maxContentLength = checkPositive(maxContentLength, "maxContentLength");
137         this.messageMap = messageMap;
138         this.headersFactory = headersFactory;
139         this.trailersFactory = trailersFactory;
140     }
141 
142     @Override
143     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
144         // Release any outstanding messages from the map
145         for (Map.Entry<Integer, FullHttpMessage> entry : messageMap.entrySet()) {
146             ReferenceCountUtil.safeRelease(entry.getValue());
147         }
148         messageMap.clear();
149         super.handlerRemoved(ctx);
150     }
151 
152     protected FullHttpMessage putMessage(int streamId, FullHttpMessage message) {
153         return messageMap.put(streamId, message);
154     }
155 
156     protected FullHttpMessage getMessage(int streamId) {
157         return messageMap.get(streamId);
158     }
159 
160     protected FullHttpMessage removeMessage(int streamId) {
161         return messageMap.remove(streamId);
162     }
163 
164     @Override
165     protected void decode(ChannelHandlerContext ctx, SpdyFrame msg, List<Object> out)
166             throws Exception {
167         if (msg instanceof SpdySynStreamFrame) {
168 
169             // HTTP requests/responses are mapped one-to-one to SPDY streams.
170             SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
171             int streamId = spdySynStreamFrame.streamId();
172 
173             if (SpdyCodecUtil.isServerId(streamId)) {
174                 // SYN_STREAM frames initiated by the server are pushed resources
175                 int associatedToStreamId = spdySynStreamFrame.associatedStreamId();
176 
177                 // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0
178                 // it must reply with a RST_STREAM with error code INVALID_STREAM.
179                 if (associatedToStreamId == 0) {
180                     SpdyRstStreamFrame spdyRstStreamFrame =
181                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM);
182                     ctx.writeAndFlush(spdyRstStreamFrame);
183                     return;
184                 }
185 
186                 // If a client receives a SYN_STREAM with isLast set,
187                 // reply with a RST_STREAM with error code PROTOCOL_ERROR
188                 // (we only support pushed resources divided into two header blocks).
189                 if (spdySynStreamFrame.isLast()) {
190                     SpdyRstStreamFrame spdyRstStreamFrame =
191                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
192                     ctx.writeAndFlush(spdyRstStreamFrame);
193                     return;
194                 }
195 
196                 // If a client receives a response with a truncated header block,
197                 // reply with a RST_STREAM with error code INTERNAL_ERROR.
198                 if (spdySynStreamFrame.isTruncated()) {
199                     SpdyRstStreamFrame spdyRstStreamFrame =
200                     new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
201                     ctx.writeAndFlush(spdyRstStreamFrame);
202                     return;
203                 }
204 
205                 FullHttpRequest httpRequestWithEntity = null;
206                 try {
207                     httpRequestWithEntity = createHttpRequest(spdySynStreamFrame, ctx.alloc());
208 
209                     // Set the Stream-ID, Associated-To-Stream-ID, and Priority as headers
210                     httpRequestWithEntity.headers().setInt(Names.STREAM_ID, streamId);
211                     httpRequestWithEntity.headers().setInt(Names.ASSOCIATED_TO_STREAM_ID, associatedToStreamId);
212                     httpRequestWithEntity.headers().setInt(Names.PRIORITY, spdySynStreamFrame.priority());
213 
214                     out.add(httpRequestWithEntity);
215                     httpRequestWithEntity = null;
216                 } catch (Throwable ignored) {
217                     if (httpRequestWithEntity != null) {
218                         httpRequestWithEntity.release();
219                     }
220                     SpdyRstStreamFrame spdyRstStreamFrame =
221                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
222                     ctx.writeAndFlush(spdyRstStreamFrame);
223                 }
224             } else {
225                 // SYN_STREAM frames initiated by the client are HTTP requests
226 
227                 // If a client sends a request with a truncated header block, the server must
228                 // reply with an HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply.
229                 if (spdySynStreamFrame.isTruncated()) {
230                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
231                     spdySynReplyFrame.setLast(true);
232                     SpdyHeaders frameHeaders = spdySynReplyFrame.headers();
233                     frameHeaders.setInt(STATUS, HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.code());
234                     frameHeaders.setObject(VERSION, HttpVersion.HTTP_1_0);
235                     ctx.writeAndFlush(spdySynReplyFrame);
236                     return;
237                 }
238 
239                 FullHttpRequest httpRequestWithEntity = null;
240                 try {
241                     httpRequestWithEntity = createHttpRequest(spdySynStreamFrame, ctx.alloc());
242 
243                     // Set the Stream-ID as a header
244                     httpRequestWithEntity.headers().setInt(Names.STREAM_ID, streamId);
245 
246                     if (spdySynStreamFrame.isLast()) {
247                         out.add(httpRequestWithEntity);
248                         httpRequestWithEntity = null;
249                     } else {
250                         // Request body will follow in a series of Data Frames
251                         FullHttpMessage old = putMessage(streamId, httpRequestWithEntity);
252                         httpRequestWithEntity = null;
253                         if (old != null) {
254                             old.release();
255                         }
256                     }
257                 } catch (Throwable t) {
258                     if (httpRequestWithEntity != null) {
259                         httpRequestWithEntity.release();
260                     }
261                     // If a client sends a SYN_STREAM without all of the getMethod, url (host and path),
262                     // scheme, and version headers the server must reply with an HTTP 400 BAD REQUEST reply.
263                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
264                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
265                     spdySynReplyFrame.setLast(true);
266                     SpdyHeaders frameHeaders = spdySynReplyFrame.headers();
267                     frameHeaders.setInt(STATUS, HttpResponseStatus.BAD_REQUEST.code());
268                     frameHeaders.setObject(VERSION, HttpVersion.HTTP_1_0);
269                     ctx.writeAndFlush(spdySynReplyFrame);
270                 }
271             }
272 
273         } else if (msg instanceof SpdySynReplyFrame) {
274 
275             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
276             int streamId = spdySynReplyFrame.streamId();
277 
278             // If a client receives a SYN_REPLY with a truncated header block,
279             // reply with a RST_STREAM frame with error code INTERNAL_ERROR.
280             if (spdySynReplyFrame.isTruncated()) {
281                 SpdyRstStreamFrame spdyRstStreamFrame =
282                         new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
283                 ctx.writeAndFlush(spdyRstStreamFrame);
284                 return;
285             }
286 
287             FullHttpResponse httpResponseWithEntity = null;
288             try {
289                 httpResponseWithEntity = createHttpResponse(spdySynReplyFrame, ctx.alloc());
290 
291                 // Set the Stream-ID as a header
292                 httpResponseWithEntity.headers().setInt(Names.STREAM_ID, streamId);
293 
294                 if (spdySynReplyFrame.isLast()) {
295                     HttpUtil.setContentLength(httpResponseWithEntity, 0);
296                     out.add(httpResponseWithEntity);
297                     httpResponseWithEntity = null;
298                 } else {
299                     // Response body will follow in a series of Data Frames
300                     FullHttpMessage old = putMessage(streamId, httpResponseWithEntity);
301                     httpResponseWithEntity = null;
302                     if (old != null) {
303                         old.release();
304                     }
305                 }
306             } catch (Throwable t) {
307                 if (httpResponseWithEntity != null) {
308                     httpResponseWithEntity.release();
309                 }
310                 // If a client receives a SYN_REPLY without valid getStatus and version headers
311                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
312                 SpdyRstStreamFrame spdyRstStreamFrame =
313                     new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
314                 ctx.writeAndFlush(spdyRstStreamFrame);
315             }
316 
317         } else if (msg instanceof SpdyHeadersFrame) {
318 
319             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
320             int streamId = spdyHeadersFrame.streamId();
321             FullHttpMessage fullHttpMessage = getMessage(streamId);
322 
323             if (fullHttpMessage == null) {
324                 // HEADERS frames may initiate a pushed response
325                 if (SpdyCodecUtil.isServerId(streamId)) {
326 
327                     // If a client receives a HEADERS with a truncated header block,
328                     // reply with a RST_STREAM frame with error code INTERNAL_ERROR.
329                     if (spdyHeadersFrame.isTruncated()) {
330                         SpdyRstStreamFrame spdyRstStreamFrame =
331                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INTERNAL_ERROR);
332                         ctx.writeAndFlush(spdyRstStreamFrame);
333                         return;
334                     }
335 
336                     try {
337                         fullHttpMessage = createHttpResponse(spdyHeadersFrame, ctx.alloc());
338 
339                         // Set the Stream-ID as a header
340                         fullHttpMessage.headers().setInt(Names.STREAM_ID, streamId);
341 
342                         if (spdyHeadersFrame.isLast()) {
343                             HttpUtil.setContentLength(fullHttpMessage, 0);
344                             out.add(fullHttpMessage);
345                             fullHttpMessage = null;
346                         } else {
347                             // Response body will follow in a series of Data Frames
348                             FullHttpMessage old = putMessage(streamId, fullHttpMessage);
349                             fullHttpMessage = null;
350                             if (old != null) {
351                                 old.release();
352                             }
353                         }
354                     } catch (Throwable t) {
355                         if (fullHttpMessage != null) {
356                             fullHttpMessage.release();
357                         }
358                         // If a client receives a SYN_REPLY without valid getStatus and version headers
359                         // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
360                         SpdyRstStreamFrame spdyRstStreamFrame =
361                             new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR);
362                         ctx.writeAndFlush(spdyRstStreamFrame);
363                     }
364                 }
365                 return;
366             }
367 
368             // Ignore trailers in a truncated HEADERS frame.
369             if (!spdyHeadersFrame.isTruncated()) {
370                 for (Map.Entry<CharSequence, CharSequence> e: spdyHeadersFrame.headers()) {
371                     fullHttpMessage.headers().add(e.getKey(), e.getValue());
372                 }
373             }
374 
375             if (spdyHeadersFrame.isLast()) {
376                 HttpUtil.setContentLength(fullHttpMessage, fullHttpMessage.content().readableBytes());
377                 FullHttpMessage removed = removeMessage(streamId);
378                 if (removed != null && removed != fullHttpMessage) {
379                     removed.release();
380                 }
381                 out.add(fullHttpMessage);
382             }
383 
384         } else if (msg instanceof SpdyDataFrame) {
385 
386             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
387             int streamId = spdyDataFrame.streamId();
388             FullHttpMessage fullHttpMessage = getMessage(streamId);
389 
390             // If message is not in map discard Data Frame.
391             if (fullHttpMessage == null) {
392                 return;
393             }
394 
395             ByteBuf content = fullHttpMessage.content();
396             if (content.readableBytes() > maxContentLength - spdyDataFrame.content().readableBytes()) {
397                 FullHttpMessage removed = removeMessage(streamId);
398                 if (removed != null && removed != fullHttpMessage) {
399                     removed.release();
400                 }
401                 fullHttpMessage.release();
402                 throw new TooLongFrameException(
403                         "HTTP content length exceeded " + maxContentLength + " bytes: "
404                                 + spdyDataFrame.content().readableBytes());
405             }
406 
407             ByteBuf spdyDataFrameData = spdyDataFrame.content();
408             int spdyDataFrameDataLen = spdyDataFrameData.readableBytes();
409             content.writeBytes(spdyDataFrameData, spdyDataFrameData.readerIndex(), spdyDataFrameDataLen);
410 
411             if (spdyDataFrame.isLast()) {
412                 HttpUtil.setContentLength(fullHttpMessage, content.readableBytes());
413                 FullHttpMessage removed = removeMessage(streamId);
414                 if (removed != null && removed != fullHttpMessage) {
415                     removed.release();
416                 }
417                 out.add(fullHttpMessage);
418             }
419 
420         } else if (msg instanceof SpdyRstStreamFrame) {
421 
422             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
423             int streamId = spdyRstStreamFrame.streamId();
424             FullHttpMessage removed = removeMessage(streamId);
425             if (removed != null) {
426                 removed.release();
427             }
428         }
429     }
430 
431     private static FullHttpRequest createHttpRequest(SpdyHeadersFrame requestFrame, ByteBufAllocator alloc)
432        throws Exception {
433         // Create the first line of the request from the name/value pairs
434         SpdyHeaders headers     = requestFrame.headers();
435         HttpMethod  method      = HttpMethod.valueOf(headers.getAsString(METHOD));
436         String      url         = headers.getAsString(PATH);
437         HttpVersion httpVersion = HttpVersion.valueOf(headers.getAsString(VERSION));
438         headers.remove(METHOD);
439         headers.remove(PATH);
440         headers.remove(VERSION);
441 
442         boolean release = true;
443         ByteBuf buffer = alloc.buffer();
444         try {
445             FullHttpRequest req = new DefaultFullHttpRequest(httpVersion, method, url, buffer);
446 
447             // Remove the scheme header
448             headers.remove(SCHEME);
449 
450             // Replace the SPDY host header with the HTTP host header
451             CharSequence host = headers.get(HOST);
452             headers.remove(HOST);
453             req.headers().set(HttpHeaderNames.HOST, host);
454 
455             for (Map.Entry<CharSequence, CharSequence> e : requestFrame.headers()) {
456                 req.headers().add(e.getKey(), e.getValue());
457             }
458 
459             // The Connection and Keep-Alive headers are no longer valid
460             HttpUtil.setKeepAlive(req, true);
461 
462             // Transfer-Encoding header is not valid
463             req.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
464             release = false;
465             return req;
466         } finally {
467             if (release) {
468                 buffer.release();
469             }
470         }
471     }
472 
473     private FullHttpResponse createHttpResponse(SpdyHeadersFrame responseFrame, ByteBufAllocator alloc)
474             throws Exception {
475 
476         // Create the first line of the response from the name/value pairs
477         SpdyHeaders headers = responseFrame.headers();
478         HttpResponseStatus status = HttpResponseStatus.parseLine(headers.get(STATUS));
479         HttpVersion version = HttpVersion.valueOf(headers.getAsString(VERSION));
480         headers.remove(STATUS);
481         headers.remove(VERSION);
482 
483         boolean release = true;
484         ByteBuf buffer = alloc.buffer();
485         try {
486             FullHttpResponse res = new DefaultFullHttpResponse(
487                     version, status, buffer, headersFactory, trailersFactory);
488             for (Map.Entry<CharSequence, CharSequence> e: responseFrame.headers()) {
489                 res.headers().add(e.getKey(), e.getValue());
490             }
491 
492             // The Connection and Keep-Alive headers are no longer valid
493             HttpUtil.setKeepAlive(res, true);
494 
495             // Transfer-Encoding header is not valid
496             res.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
497             res.headers().remove(HttpHeaderNames.TRAILER);
498 
499             release = false;
500             return res;
501         } finally {
502             if (release) {
503                 buffer.release();
504             }
505         }
506     }
507 }