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