View Javadoc

    1   /*
    2    * Copyright 2012 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 org.jboss.netty.handler.codec.spdy;
    17
    18  import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*;
    19
    20  import java.util.HashMap;
    21  import java.util.Map;
    22
    23  import org.jboss.netty.buffer.ChannelBuffer;
    24  import org.jboss.netty.buffer.ChannelBuffers;
    25  import org.jboss.netty.channel.Channel;
    26  import org.jboss.netty.channel.ChannelHandlerContext;
    27  import org.jboss.netty.channel.Channels;
    28  import org.jboss.netty.handler.codec.frame.TooLongFrameException;
    29  import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
    30  import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
    31  import org.jboss.netty.handler.codec.http.HttpHeaders;
    32  import org.jboss.netty.handler.codec.http.HttpMessage;
    33  import org.jboss.netty.handler.codec.http.HttpMethod;
    34  import org.jboss.netty.handler.codec.http.HttpRequest;
    35  import org.jboss.netty.handler.codec.http.HttpResponse;
    36  import org.jboss.netty.handler.codec.http.HttpResponseStatus;
    37  import org.jboss.netty.handler.codec.http.HttpVersion;
    38  import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
    39
    40  /**
    41   * Decodes {@link SpdySynStreamFrame}s, {@link SpdySynReplyFrame}s,
    42   * and {@link SpdyDataFrame}s into {@link HttpRequest}s and {@link HttpResponse}s.
    43   */
    44  public class SpdyHttpDecoder extends OneToOneDecoder {
    45
    46      private final int spdyVersion;
    47      private final int maxContentLength;
    48      private final Map<Integer, HttpMessage> messageMap = new HashMap<Integer, HttpMessage>();
    49
    50      /**
    51       * Creates a new instance for the SPDY/2 protocol
    52       *
    53       * @param maxContentLength the maximum length of the message content.
    54       *        If the length of the message content exceeds this value,
    55       *        a {@link TooLongFrameException} will be raised.
    56       */
    57      @Deprecated
    58      public SpdyHttpDecoder(int maxContentLength) {
    59          this(2, maxContentLength);
    60      }
    61
    62      /**
    63       * Creates a new instance.
    64       *
    65       * @param version the protocol version
    66       * @param maxContentLength the maximum length of the message content.
    67       *        If the length of the message content exceeds this value,
    68       *        a {@link TooLongFrameException} will be raised.
    69       */
    70      public SpdyHttpDecoder(int version, int maxContentLength) {
    71          super();
    72          if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) {
    73              throw new IllegalArgumentException(
    74                      "unsupported version: " + version);
    75          }
    76          if (maxContentLength <= 0) {
    77              throw new IllegalArgumentException(
    78                      "maxContentLength must be a positive integer: " + maxContentLength);
    79          }
    80          spdyVersion = version;
    81          this.maxContentLength = maxContentLength;
    82      }
    83
    84      @Override
    85      protected Object decode(ChannelHandlerContext ctx, Channel channel, Object msg)
    86              throws Exception {
    87
    88          if (msg instanceof SpdySynStreamFrame) {
    89
    90              // HTTP requests/responses are mapped one-to-one to SPDY streams.
    91              SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
    92              int streamID = spdySynStreamFrame.getStreamId();
    93
    94              if (SpdyCodecUtil.isServerId(streamID)) {
    95                  // SYN_STREAM frames initiated by the server are pushed resources
    96                  int associatedToStreamID = spdySynStreamFrame.getAssociatedToStreamId();
    97
    98                  // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0
    99                  // it must reply with a RST_STREAM with error code INVALID_STREAM
    100                 if (associatedToStreamID == 0) {
    101                     SpdyRstStreamFrame spdyRstStreamFrame =
    102                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.INVALID_STREAM);
    103                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
    104                 }
    105
    106                 String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame);
    107
    108                 // If a client receives a SYN_STREAM without a 'url' header
    109                 // it must reply with a RST_STREAM with error code PROTOCOL_ERROR
    110                 if (URL == null) {
    111                     SpdyRstStreamFrame spdyRstStreamFrame =
    112                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
    113                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
    114                 }
    115
    116                 try {
    117                     HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame);
    118
    119                     // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers
    120                     SpdyHttpHeaders.setStreamId(httpResponse, streamID);
    121                     SpdyHttpHeaders.setAssociatedToStreamId(httpResponse, associatedToStreamID);
    122                     SpdyHttpHeaders.setPriority(httpResponse, spdySynStreamFrame.getPriority());
    123                     SpdyHttpHeaders.setUrl(httpResponse, URL);
    124
    125                     if (spdySynStreamFrame.isLast()) {
    126                         HttpHeaders.setContentLength(httpResponse, 0);
    127                         return httpResponse;
    128                     } else {
    129                         // Response body will follow in a series of Data Frames
    130                         messageMap.put(streamID, httpResponse);
    131                     }
    132                 } catch (Exception e) {
    133                     SpdyRstStreamFrame spdyRstStreamFrame =
    134                         new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
    135                     Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
    136                 }
    137
    138             } else {
    139                 // SYN_STREAM frames initiated by the client are HTTP requests
    140                 try {
    141                     HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame);
    142
    143                     // Set the Stream-ID as a header
    144                     SpdyHttpHeaders.setStreamId(httpRequest, streamID);
    145
    146                     if (spdySynStreamFrame.isLast()) {
    147                         return httpRequest;
    148                     } else {
    149                         // Request body will follow in a series of Data Frames
    150                         messageMap.put(streamID, httpRequest);
    151                     }
    152                 } catch (Exception e) {
    153                     // If a client sends a SYN_STREAM without all of the method, url (host and path),
    154                     // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
    155                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
    156                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID);
    157                     spdySynReplyFrame.setLast(true);
    158                     SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST);
    159                     SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0);
    160                     Channels.write(ctx, Channels.future(channel), spdySynReplyFrame);
    161                 }
    162             }
    163
    164         } else if (msg instanceof SpdySynReplyFrame) {
    165
    166             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
    167             int streamID = spdySynReplyFrame.getStreamId();
    168
    169             try {
    170                 HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame);
    171
    172                 // Set the Stream-ID as a header
    173                 SpdyHttpHeaders.setStreamId(httpResponse, streamID);
    174
    175                 if (spdySynReplyFrame.isLast()) {
    176                     HttpHeaders.setContentLength(httpResponse, 0);
    177                     return httpResponse;
    178                 } else {
    179                     // Response body will follow in a series of Data Frames
    180                     messageMap.put(streamID, httpResponse);
    181                 }
    182             } catch (Exception e) {
    183                 // If a client receives a SYN_REPLY without valid status and version headers
    184                 // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR
    185                 SpdyRstStreamFrame spdyRstStreamFrame =
    186                     new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR);
    187                 Channels.write(ctx, Channels.future(channel), spdyRstStreamFrame);
    188             }
    189
    190         } else if (msg instanceof SpdyHeadersFrame) {
    191
    192             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
    193             Integer streamID = spdyHeadersFrame.getStreamId();
    194             HttpMessage httpMessage = messageMap.get(streamID);
    195
    196             // If message is not in map discard HEADERS frame.
    197             // SpdySessionHandler should prevent this from happening.
    198             if (httpMessage == null) {
    199                 return null;
    200             }
    201
    202             for (Map.Entry<String, String> e: spdyHeadersFrame.getHeaders()) {
    203                 httpMessage.addHeader(e.getKey(), e.getValue());
    204             }
    205
    206         } else if (msg instanceof SpdyDataFrame) {
    207
    208             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
    209             Integer streamID = spdyDataFrame.getStreamId();
    210             HttpMessage httpMessage = messageMap.get(streamID);
    211
    212             // If message is not in map discard Data Frame.
    213             // SpdySessionHandler should prevent this from happening.
    214             if (httpMessage == null) {
    215                 return null;
    216             }
    217
    218             ChannelBuffer content = httpMessage.getContent();
    219             if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) {
    220                 messageMap.remove(streamID);
    221                 throw new TooLongFrameException(
    222                         "HTTP content length exceeded " + maxContentLength + " bytes.");
    223             }
    224
    225             if (content == ChannelBuffers.EMPTY_BUFFER) {
    226                 content = ChannelBuffers.dynamicBuffer(channel.getConfig().getBufferFactory());
    227                 content.writeBytes(spdyDataFrame.getData());
    228                 httpMessage.setContent(content);
    229             } else {
    230                 content.writeBytes(spdyDataFrame.getData());
    231             }
    232
    233             if (spdyDataFrame.isLast()) {
    234                 HttpHeaders.setContentLength(httpMessage, content.readableBytes());
    235                 messageMap.remove(streamID);
    236                 return httpMessage;
    237             }
    238
    239         } else if (msg instanceof SpdyRstStreamFrame) {
    240
    241             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
    242             Integer streamID = spdyRstStreamFrame.getStreamId();
    243             messageMap.remove(streamID);
    244         }
    245
    246         return null;
    247     }
    248
    249     private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeaderBlock requestFrame)
    250             throws Exception {
    251         // Create the first line of the request from the name/value pairs
    252         HttpMethod  method      = SpdyHeaders.getMethod(spdyVersion, requestFrame);
    253         String      url         = SpdyHeaders.getUrl(spdyVersion, requestFrame);
    254         HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame);
    255         SpdyHeaders.removeMethod(spdyVersion, requestFrame);
    256         SpdyHeaders.removeUrl(spdyVersion, requestFrame);
    257         SpdyHeaders.removeVersion(spdyVersion, requestFrame);
    258
    259         HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url);
    260
    261         // Remove the scheme header
    262         SpdyHeaders.removeScheme(spdyVersion, requestFrame);
    263
    264         if (spdyVersion >= 3) {
    265             // Replace the SPDY host header with the HTTP host header
    266             String host = SpdyHeaders.getHost(requestFrame);
    267             SpdyHeaders.removeHost(requestFrame);
    268             HttpHeaders.setHost(httpRequest, host);
    269         }
    270
    271         for (Map.Entry<String, String> e: requestFrame.getHeaders()) {
    272             httpRequest.addHeader(e.getKey(), e.getValue());
    273         }
    274
    275         // The Connection and Keep-Alive headers are no longer valid
    276         HttpHeaders.setKeepAlive(httpRequest, true);
    277
    278         // Transfer-Encoding header is not valid
    279         httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
    280
    281         return httpRequest;
    282     }
    283
    284     private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeaderBlock responseFrame)
    285             throws Exception {
    286         // Create the first line of the response from the name/value pairs
    287         HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame);
    288         HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame);
    289         SpdyHeaders.removeStatus(spdyVersion, responseFrame);
    290         SpdyHeaders.removeVersion(spdyVersion, responseFrame);
    291
    292         HttpResponse httpResponse = new DefaultHttpResponse(version, status);
    293         for (Map.Entry<String, String> e: responseFrame.getHeaders()) {
    294             httpResponse.addHeader(e.getKey(), e.getValue());
    295         }
    296
    297         // The Connection and Keep-Alive headers are no longer valid
    298         HttpHeaders.setKeepAlive(httpResponse, true);
    299
    300         // Transfer-Encoding header is not valid
    301         httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
    302         httpResponse.removeHeader(HttpHeaders.Names.TRAILER);
    303
    304         return httpResponse;
    305     }
    306 }