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     public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt)
153             throws Exception {
154         if (!(evt instanceof MessageEvent)) {
155             ctx.sendDownstream(evt);
156             return;
157         }
158 
159         MessageEvent e = (MessageEvent) evt;
160         Object msg = e.getMessage();
161 
162         if (msg instanceof HttpRequest) {
163 
164             HttpRequest httpRequest = (HttpRequest) msg;
165             SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpRequest);
166             int streamID = spdySynStreamFrame.getStreamId();
167             ChannelFuture future = getContentFuture(ctx, e, streamID, httpRequest);
168             Channels.write(ctx, future, spdySynStreamFrame, e.getRemoteAddress());
169 
170         } else if (msg instanceof HttpResponse) {
171 
172             HttpResponse httpResponse = (HttpResponse) msg;
173             if (httpResponse.containsHeader(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID)) {
174                 SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpResponse);
175                 int streamID = spdySynStreamFrame.getStreamId();
176                 ChannelFuture future = getContentFuture(ctx, e, streamID, httpResponse);
177                 Channels.write(ctx, future, spdySynStreamFrame, e.getRemoteAddress());
178             } else {
179                 SpdySynReplyFrame spdySynReplyFrame = createSynReplyFrame(httpResponse);
180                 int streamID = spdySynReplyFrame.getStreamId();
181                 ChannelFuture future = getContentFuture(ctx, e, streamID, httpResponse);
182                 Channels.write(ctx, future, spdySynReplyFrame, e.getRemoteAddress());
183             }
184 
185         } else if (msg instanceof HttpChunk) {
186 
187             HttpChunk chunk = (HttpChunk) msg;
188             SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(currentStreamID);
189             spdyDataFrame.setData(chunk.getContent());
190             spdyDataFrame.setLast(chunk.isLast());
191 
192             if (chunk instanceof HttpChunkTrailer) {
193                 HttpChunkTrailer trailer = (HttpChunkTrailer) chunk;
194                 List<Map.Entry<String, String>> trailers = trailer.getHeaders();
195                 if (trailers.isEmpty()) {
196                     Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress());
197                 } else {
198                     // Create SPDY HEADERS frame out of trailers
199                     SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(currentStreamID);
200                     for (Map.Entry<String, String> entry: trailers) {
201                         spdyHeadersFrame.addHeader(entry.getKey(), entry.getValue());
202                     }
203 
204                     // Write HEADERS frame and append Data Frame
205                     ChannelFuture future = Channels.future(e.getChannel());
206                     future.addListener(new SpdyFrameWriter(ctx, e, spdyDataFrame));
207                     Channels.write(ctx, future, spdyHeadersFrame, e.getRemoteAddress());
208                 }
209             } else {
210                 Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress());
211             }
212         } else {
213             // Unknown message type
214             ctx.sendDownstream(evt);
215         }
216     }
217 
218     private static ChannelFuture getContentFuture(
219             ChannelHandlerContext ctx, MessageEvent e, int streamID, HttpMessage httpMessage) {
220         if (httpMessage.getContent().readableBytes() == 0) {
221             return e.getFuture();
222         }
223 
224         // Create SPDY Data Frame out of message content
225         SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID);
226         spdyDataFrame.setData(httpMessage.getContent());
227         spdyDataFrame.setLast(true);
228 
229         // Create new future and add listener
230         ChannelFuture future = Channels.future(e.getChannel());
231         future.addListener(new SpdyFrameWriter(ctx, e, spdyDataFrame));
232 
233         return future;
234     }
235 
236     private static class SpdyFrameWriter implements ChannelFutureListener {
237 
238         private final ChannelHandlerContext ctx;
239         private final MessageEvent e;
240         private final Object spdyFrame;
241 
242         SpdyFrameWriter(ChannelHandlerContext ctx, MessageEvent e, Object spdyFrame) {
243             this.ctx = ctx;
244             this.e = e;
245             this.spdyFrame = spdyFrame;
246         }
247 
248         public void operationComplete(ChannelFuture future) throws Exception {
249             if (future.isSuccess()) {
250                 Channels.write(ctx, e.getFuture(), spdyFrame, e.getRemoteAddress());
251             } else if (future.isCancelled()) {
252                 e.getFuture().cancel();
253             } else {
254                 e.getFuture().setFailure(future.getCause());
255             }
256         }
257     }
258 
259     private SpdySynStreamFrame createSynStreamFrame(HttpMessage httpMessage)
260             throws Exception {
261         boolean chunked = httpMessage.isChunked();
262 
263         // Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers
264         int streamID = SpdyHttpHeaders.getStreamId(httpMessage);
265         int associatedToStreamID = SpdyHttpHeaders.getAssociatedToStreamId(httpMessage);
266         byte priority = SpdyHttpHeaders.getPriority(httpMessage);
267         String URL = SpdyHttpHeaders.getUrl(httpMessage);
268         String scheme = SpdyHttpHeaders.getScheme(httpMessage);
269         SpdyHttpHeaders.removeStreamId(httpMessage);
270         SpdyHttpHeaders.removeAssociatedToStreamId(httpMessage);
271         SpdyHttpHeaders.removePriority(httpMessage);
272         SpdyHttpHeaders.removeUrl(httpMessage);
273         SpdyHttpHeaders.removeScheme(httpMessage);
274 
275         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
276         // headers are not valid and MUST not be sent.
277         httpMessage.removeHeader(HttpHeaders.Names.CONNECTION);
278         httpMessage.removeHeader("Keep-Alive");
279         httpMessage.removeHeader("Proxy-Connection");
280         httpMessage.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
281 
282         SpdySynStreamFrame spdySynStreamFrame =
283                 new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority);
284 
285         // Unfold the first line of the message into name/value pairs
286         if (httpMessage instanceof HttpRequest) {
287             HttpRequest httpRequest = (HttpRequest) httpMessage;
288             SpdyHeaders.setMethod(spdyVersion, spdySynStreamFrame, httpRequest.getMethod());
289             SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, httpRequest.getUri());
290             SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion());
291         }
292         if (httpMessage instanceof HttpResponse) {
293             HttpResponse httpResponse = (HttpResponse) httpMessage;
294             SpdyHeaders.setStatus(spdyVersion, spdySynStreamFrame, httpResponse.getStatus());
295             SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, URL);
296             SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion());
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 }