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