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