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.List;
    21  import java.util.Map;
    22
    23  import org.jboss.netty.channel.ChannelDownstreamHandler;
    24  import org.jboss.netty.channel.ChannelEvent;
    25  import org.jboss.netty.channel.ChannelFuture;
    26  import org.jboss.netty.channel.ChannelFutureListener;
    27  import org.jboss.netty.channel.ChannelHandlerContext;
    28  import org.jboss.netty.channel.Channels;
    29  import org.jboss.netty.channel.MessageEvent;
    30  import org.jboss.netty.handler.codec.http.HttpChunk;
    31  import org.jboss.netty.handler.codec.http.HttpChunkTrailer;
    32  import org.jboss.netty.handler.codec.http.HttpHeaders;
    33  import org.jboss.netty.handler.codec.http.HttpMessage;
    34  import org.jboss.netty.handler.codec.http.HttpRequest;
    35  import org.jboss.netty.handler.codec.http.HttpResponse;
    36
    37  /**
    38   * Encodes {@link HttpRequest}s, {@link HttpResponse}s, and {@link HttpChunk}s
    39   * into {@link SpdySynStreamFrame}s and {@link SpdySynReplyFrame}s.
    40   *
    41   * <h3>Request Annotations</h3>
    42   *
    43   * SPDY specific headers must be added to {@link HttpRequest}s:
    44   * <table border=1>
    45   * <tr>
    46   * <th>Header Name</th><th>Header Value</th>
    47   * </tr>
    48   * <tr>
    49   * <td>{@code "X-SPDY-Stream-ID"}</td>
    50   * <td>The Stream-ID for this request.
    51   * Stream-IDs must be odd, positive integers, and must increase monotonically.</td>
    52   * </tr>
    53   * <tr>
    54   * <td>{@code "X-SPDY-Priority"}</td>
    55   * <td>The priority value for this request.
    56   * The priority should be between 0 and 7 inclusive.
    57   * 0 represents the highest priority and 7 represents the lowest.
    58   * This header is optional and defaults to 0.</td>
    59   * </tr>
    60   * </table>
    61   *
    62   * <h3>Response Annotations</h3>
    63   *
    64   * SPDY specific headers must be added to {@link HttpResponse}s:
    65   * <table border=1>
    66   * <tr>
    67   * <th>Header Name</th><th>Header Value</th>
    68   * </tr>
    69   * <tr>
    70   * <td>{@code "X-SPDY-Stream-ID"}</td>
    71   * <td>The Stream-ID of the request corresponding to this response.</td>
    72   * </tr>
    73   * </table>
    74   *
    75   * <h3>Pushed Resource Annotations</h3>
    76   *
    77   * SPDY specific headers must be added to pushed {@link HttpResponse}s:
    78   * <table border=1>
    79   * <tr>
    80   * <th>Header Name</th><th>Header Value</th>
    81   * </tr>
    82   * <tr>
    83   * <td>{@code "X-SPDY-Stream-ID"}</td>
    84   * <td>The Stream-ID for this resource.
    85   * Stream-IDs must be even, positive integers, and must increase monotonically.</td>
    86   * </tr>
    87   * <tr>
    88   * <td>{@code "X-SPDY-Associated-To-Stream-ID"}</td>
    89   * <td>The Stream-ID of the request that initiated this pushed resource.</td>
    90   * </tr>
    91   * <tr>
    92   * <td>{@code "X-SPDY-Priority"}</td>
    93   * <td>The priority value for this resource.
    94   * The priority should be between 0 and 7 inclusive.
    95   * 0 represents the highest priority and 7 represents the lowest.
    96   * This header is optional and defaults to 0.</td>
    97   * </tr>
    98   * <tr>
    99   * <td>{@code "X-SPDY-URL"}</td>
    100  * <td>The absolute path for the resource being pushed.</td>
    101  * </tr>
    102  * </table>
    103  *
    104  * <h3>Required Annotations</h3>
    105  *
    106  * SPDY requires that all Requests and Pushed Resources contain
    107  * an HTTP "Host" header.
    108  *
    109  * <h3>Optional Annotations</h3>
    110  *
    111  * Requests and Pushed Resources must contain a SPDY scheme header.
    112  * This can be set via the {@code "X-SPDY-Scheme"} header but otherwise
    113  * defaults to "https" as that is the most common SPDY deployment.
    114  *
    115  * <h3>Chunked Content</h3>
    116  *
    117  * This encoder associates all {@link HttpChunk}s that it receives
    118  * with the most recently received 'chunked' {@link HttpRequest}
    119  * or {@link HttpResponse}.
    120  *
    121  * <h3>Pushed Resources</h3>
    122  *
    123  * All pushed resources should be sent before sending the response
    124  * that corresponds to the initial request.
    125  */
    126 public class SpdyHttpEncoder implements ChannelDownstreamHandler {
    127
    128     private final int spdyVersion;
    129     private volatile int currentStreamID;
    130
    131     /**
    132      * Creates a new instance for the SPDY/2 protocol
    133      */
    134      @Deprecated
    135     public SpdyHttpEncoder() {
    136          this(2);
    137     }
    138
    139     /**
    140      * Creates a new instance.
    141      *
    142      * @param version the protocol version
    143      */
    144     public SpdyHttpEncoder(int version) {
    145         if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) {
    146             throw new IllegalArgumentException(
    147                     "unsupported version: " + version);
    148         }
    149         spdyVersion = version;
    150     }
    151
    152
    153     public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt)
    154             throws Exception {
    155         if (!(evt instanceof MessageEvent)) {
    156             ctx.sendDownstream(evt);
    157             return;
    158         }
    159
    160         MessageEvent e = (MessageEvent) evt;
    161         Object msg = e.getMessage();
    162
    163         if (msg instanceof HttpRequest) {
    164
    165             HttpRequest httpRequest = (HttpRequest) msg;
    166             SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpRequest);
    167             int streamID = spdySynStreamFrame.getStreamId();
    168             ChannelFuture future = getContentFuture(ctx, e, streamID, httpRequest);
    169             Channels.write(ctx, future, spdySynStreamFrame, e.getRemoteAddress());
    170
    171         } else if (msg instanceof HttpResponse) {
    172
    173             HttpResponse httpResponse = (HttpResponse) msg;
    174             if (httpResponse.containsHeader(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID)) {
    175                 SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpResponse);
    176                 int streamID = spdySynStreamFrame.getStreamId();
    177                 ChannelFuture future = getContentFuture(ctx, e, streamID, httpResponse);
    178                 Channels.write(ctx, future, spdySynStreamFrame, e.getRemoteAddress());
    179             } else {
    180                 SpdySynReplyFrame spdySynReplyFrame = createSynReplyFrame(httpResponse);
    181                 int streamID = spdySynReplyFrame.getStreamId();
    182                 ChannelFuture future = getContentFuture(ctx, e, streamID, httpResponse);
    183                 Channels.write(ctx, future, spdySynReplyFrame, e.getRemoteAddress());
    184             }
    185
    186         } else if (msg instanceof HttpChunk) {
    187
    188             HttpChunk chunk = (HttpChunk) msg;
    189             SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(currentStreamID);
    190             spdyDataFrame.setData(chunk.getContent());
    191             spdyDataFrame.setLast(chunk.isLast());
    192
    193             if (chunk instanceof HttpChunkTrailer) {
    194                 HttpChunkTrailer trailer = (HttpChunkTrailer) chunk;
    195                 List<Map.Entry<String, String>> trailers = trailer.getHeaders();
    196                 if (trailers.isEmpty()) {
    197                     Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress());
    198                 } else {
    199                     // Create SPDY HEADERS frame out of trailers
    200                     SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(currentStreamID);
    201                     for (Map.Entry<String, String> entry: trailers) {
    202                         spdyHeadersFrame.addHeader(entry.getKey(), entry.getValue());
    203                     }
    204
    205                     // Write HEADERS frame and append Data Frame
    206                     ChannelFuture future = Channels.future(e.getChannel());
    207                     future.addListener(new SpdyFrameWriter(ctx, e, spdyDataFrame));
    208                     Channels.write(ctx, future, spdyHeadersFrame, e.getRemoteAddress());
    209                 }
    210             } else {
    211                 Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress());
    212             }
    213         } else {
    214             // Unknown message type
    215             ctx.sendDownstream(evt);
    216         }
    217     }
    218
    219     private ChannelFuture getContentFuture(
    220             ChannelHandlerContext ctx, MessageEvent e, int streamID, HttpMessage httpMessage) {
    221         if (httpMessage.getContent().readableBytes() == 0) {
    222             return e.getFuture();
    223         }
    224
    225         // Create SPDY Data Frame out of message content
    226         SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID);
    227         spdyDataFrame.setData(httpMessage.getContent());
    228         spdyDataFrame.setLast(true);
    229
    230         // Create new future and add listener
    231         ChannelFuture future = Channels.future(e.getChannel());
    232         future.addListener(new SpdyFrameWriter(ctx, e, spdyDataFrame));
    233
    234         return future;
    235     }
    236
    237     private class SpdyFrameWriter implements ChannelFutureListener {
    238
    239         private final ChannelHandlerContext ctx;
    240         private final MessageEvent e;
    241         private final Object spdyFrame;
    242
    243         SpdyFrameWriter(ChannelHandlerContext ctx, MessageEvent e, Object spdyFrame) {
    244             this.ctx = ctx;
    245             this.e = e;
    246             this.spdyFrame = spdyFrame;
    247         }
    248
    249         public void operationComplete(ChannelFuture future) throws Exception {
    250             if (future.isSuccess()) {
    251                 Channels.write(ctx, e.getFuture(), spdyFrame, e.getRemoteAddress());
    252             } else if (future.isCancelled()) {
    253                 e.getFuture().cancel();
    254             } else {
    255                 e.getFuture().setFailure(future.getCause());
    256             }
    257         }
    258     }
    259
    260     private SpdySynStreamFrame createSynStreamFrame(HttpMessage httpMessage)
    261             throws Exception {
    262         boolean chunked = httpMessage.isChunked();
    263
    264         // Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers
    265         int streamID = SpdyHttpHeaders.getStreamId(httpMessage);
    266         int associatedToStreamID = SpdyHttpHeaders.getAssociatedToStreamId(httpMessage);
    267         byte priority = SpdyHttpHeaders.getPriority(httpMessage);
    268         String URL = SpdyHttpHeaders.getUrl(httpMessage);
    269         String scheme = SpdyHttpHeaders.getScheme(httpMessage);
    270         SpdyHttpHeaders.removeStreamId(httpMessage);
    271         SpdyHttpHeaders.removeAssociatedToStreamId(httpMessage);
    272         SpdyHttpHeaders.removePriority(httpMessage);
    273         SpdyHttpHeaders.removeUrl(httpMessage);
    274         SpdyHttpHeaders.removeScheme(httpMessage);
    275
    276         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
    277         // headers are not valid and MUST not be sent.
    278         httpMessage.removeHeader(HttpHeaders.Names.CONNECTION);
    279         httpMessage.removeHeader("Keep-Alive");
    280         httpMessage.removeHeader("Proxy-Connection");
    281         httpMessage.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
    282
    283         SpdySynStreamFrame spdySynStreamFrame =
    284                 new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority);
    285
    286         // Unfold the first line of the message into name/value pairs
    287         if (httpMessage instanceof HttpRequest) {
    288             HttpRequest httpRequest = (HttpRequest) httpMessage;
    289             SpdyHeaders.setMethod(spdyVersion, spdySynStreamFrame, httpRequest.getMethod());
    290             SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, httpRequest.getUri());
    291             SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion());
    292         }
    293         if (httpMessage instanceof HttpResponse) {
    294             HttpResponse httpResponse = (HttpResponse) httpMessage;
    295             SpdyHeaders.setStatus(spdyVersion, spdySynStreamFrame, httpResponse.getStatus());
    296             SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, URL);
    297             spdySynStreamFrame.setUnidirectional(true);
    298         }
    299
    300         // Replace the HTTP host header with the SPDY host header
    301         if (spdyVersion >= 3) {
    302             String host = HttpHeaders.getHost(httpMessage);
    303             httpMessage.removeHeader(HttpHeaders.Names.HOST);
    304             SpdyHeaders.setHost(spdySynStreamFrame, host);
    305         }
    306
    307         // Set the SPDY scheme header
    308         if (scheme == null) {
    309             scheme = "https";
    310         }
    311         SpdyHeaders.setScheme(spdyVersion, spdySynStreamFrame, scheme);
    312
    313         // Transfer the remaining HTTP headers
    314         for (Map.Entry<String, String> entry: httpMessage.getHeaders()) {
    315             spdySynStreamFrame.addHeader(entry.getKey(), entry.getValue());
    316         }
    317
    318         if (chunked) {
    319             currentStreamID = streamID;
    320             spdySynStreamFrame.setLast(false);
    321         } else {
    322             spdySynStreamFrame.setLast(httpMessage.getContent().readableBytes() == 0);
    323         }
    324
    325         return spdySynStreamFrame;
    326     }
    327
    328     private SpdySynReplyFrame createSynReplyFrame(HttpResponse httpResponse)
    329             throws Exception {
    330         boolean chunked = httpResponse.isChunked();
    331
    332         // Get the Stream-ID from the headers
    333         int streamID = SpdyHttpHeaders.getStreamId(httpResponse);
    334         SpdyHttpHeaders.removeStreamId(httpResponse);
    335
    336         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-ENcoding
    337         // headers are not valid and MUST not be sent.
    338         httpResponse.removeHeader(HttpHeaders.Names.CONNECTION);
    339         httpResponse.removeHeader("Keep-Alive");
    340         httpResponse.removeHeader("Proxy-Connection");
    341         httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
    342
    343         SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID);
    344
    345         // Unfold the first line of the response into name/value pairs
    346         SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, httpResponse.getStatus());
    347         SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, httpResponse.getProtocolVersion());
    348
    349         // Transfer the remaining HTTP headers
    350         for (Map.Entry<String, String> entry: httpResponse.getHeaders()) {
    351             spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue());
    352         }
    353
    354         if (chunked) {
    355             currentStreamID = streamID;
    356             spdySynReplyFrame.setLast(false);
    357         } else {
    358             spdySynReplyFrame.setLast(httpResponse.getContent().readableBytes() == 0);
    359         }
    360
    361         return spdySynReplyFrame;
    362     }
    363 }