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 org.jboss.netty.channel.ChannelDownstreamHandler;
19  import org.jboss.netty.channel.ChannelEvent;
20  import org.jboss.netty.channel.ChannelFuture;
21  import org.jboss.netty.channel.ChannelFutureListener;
22  import org.jboss.netty.channel.ChannelHandlerContext;
23  import org.jboss.netty.channel.Channels;
24  import org.jboss.netty.channel.MessageEvent;
25  import org.jboss.netty.handler.codec.http.HttpChunk;
26  import org.jboss.netty.handler.codec.http.HttpChunkTrailer;
27  import org.jboss.netty.handler.codec.http.HttpHeaders;
28  import org.jboss.netty.handler.codec.http.HttpMessage;
29  import org.jboss.netty.handler.codec.http.HttpRequest;
30  import org.jboss.netty.handler.codec.http.HttpResponse;
31  
32  import java.util.List;
33  import java.util.Map;
34  
35  import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*;
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 static 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             SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion());
298             spdySynStreamFrame.setUnidirectional(true);
299         }
300 
301         // Replace the HTTP host header with the SPDY host header
302         if (spdyVersion >= 3) {
303             String host = HttpHeaders.getHost(httpMessage);
304             httpMessage.removeHeader(HttpHeaders.Names.HOST);
305             SpdyHeaders.setHost(spdySynStreamFrame, host);
306         }
307 
308         // Set the SPDY scheme header
309         if (scheme == null) {
310             scheme = "https";
311         }
312         SpdyHeaders.setScheme(spdyVersion, spdySynStreamFrame, scheme);
313 
314         // Transfer the remaining HTTP headers
315         for (Map.Entry<String, String> entry: httpMessage.getHeaders()) {
316             spdySynStreamFrame.addHeader(entry.getKey(), entry.getValue());
317         }
318 
319         if (chunked) {
320             currentStreamID = streamID;
321             spdySynStreamFrame.setLast(false);
322         } else {
323             spdySynStreamFrame.setLast(httpMessage.getContent().readableBytes() == 0);
324         }
325 
326         return spdySynStreamFrame;
327     }
328 
329     private SpdySynReplyFrame createSynReplyFrame(HttpResponse httpResponse)
330             throws Exception {
331         boolean chunked = httpResponse.isChunked();
332 
333         // Get the Stream-ID from the headers
334         int streamID = SpdyHttpHeaders.getStreamId(httpResponse);
335         SpdyHttpHeaders.removeStreamId(httpResponse);
336 
337         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-ENcoding
338         // headers are not valid and MUST not be sent.
339         httpResponse.removeHeader(HttpHeaders.Names.CONNECTION);
340         httpResponse.removeHeader("Keep-Alive");
341         httpResponse.removeHeader("Proxy-Connection");
342         httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
343 
344         SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID);
345 
346         // Unfold the first line of the response into name/value pairs
347         SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, httpResponse.getStatus());
348         SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, httpResponse.getProtocolVersion());
349 
350         // Transfer the remaining HTTP headers
351         for (Map.Entry<String, String> entry: httpResponse.getHeaders()) {
352             spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue());
353         }
354 
355         if (chunked) {
356             currentStreamID = streamID;
357             spdySynReplyFrame.setLast(false);
358         } else {
359             spdySynReplyFrame.setLast(httpResponse.getContent().readableBytes() == 0);
360         }
361 
362         return spdySynReplyFrame;
363     }
364 }