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 io.netty.handler.codec.spdy;
17  
18  import io.netty.channel.ChannelHandlerContext;
19  import io.netty.handler.codec.MessageToMessageEncoder;
20  import io.netty.handler.codec.UnsupportedMessageTypeException;
21  import io.netty.handler.codec.http.FullHttpMessage;
22  import io.netty.handler.codec.http.HttpContent;
23  import io.netty.handler.codec.http.HttpHeaders;
24  import io.netty.handler.codec.http.HttpMessage;
25  import io.netty.handler.codec.http.HttpObject;
26  import io.netty.handler.codec.http.HttpRequest;
27  import io.netty.handler.codec.http.HttpResponse;
28  import io.netty.handler.codec.http.LastHttpContent;
29  
30  import java.util.List;
31  import java.util.Map;
32  
33  /**
34   * Encodes {@link HttpRequest}s, {@link HttpResponse}s, and {@link HttpContent}s
35   * into {@link SpdySynStreamFrame}s and {@link SpdySynReplyFrame}s.
36   *
37   * <h3>Request Annotations</h3>
38   *
39   * SPDY specific headers must be added to {@link HttpRequest}s:
40   * <table border=1>
41   * <tr>
42   * <th>Header Name</th><th>Header Value</th>
43   * </tr>
44   * <tr>
45   * <td>{@code "X-SPDY-Stream-ID"}</td>
46   * <td>The Stream-ID for this request.
47   * Stream-IDs must be odd, positive integers, and must increase monotonically.</td>
48   * </tr>
49   * <tr>
50   * <td>{@code "X-SPDY-Priority"}</td>
51   * <td>The priority value for this request.
52   * The priority should be between 0 and 7 inclusive.
53   * 0 represents the highest priority and 7 represents the lowest.
54   * This header is optional and defaults to 0.</td>
55   * </tr>
56   * </table>
57   *
58   * <h3>Response Annotations</h3>
59   *
60   * SPDY specific headers must be added to {@link HttpResponse}s:
61   * <table border=1>
62   * <tr>
63   * <th>Header Name</th><th>Header Value</th>
64   * </tr>
65   * <tr>
66   * <td>{@code "X-SPDY-Stream-ID"}</td>
67   * <td>The Stream-ID of the request corresponding to this response.</td>
68   * </tr>
69   * </table>
70   *
71   * <h3>Pushed Resource Annotations</h3>
72   *
73   * SPDY specific headers must be added to pushed {@link HttpRequest}s:
74   * <table border=1>
75   * <tr>
76   * <th>Header Name</th><th>Header Value</th>
77   * </tr>
78   * <tr>
79   * <td>{@code "X-SPDY-Stream-ID"}</td>
80   * <td>The Stream-ID for this resource.
81   * Stream-IDs must be even, positive integers, and must increase monotonically.</td>
82   * </tr>
83   * <tr>
84   * <td>{@code "X-SPDY-Associated-To-Stream-ID"}</td>
85   * <td>The Stream-ID of the request that initiated this pushed resource.</td>
86   * </tr>
87   * <tr>
88   * <td>{@code "X-SPDY-Priority"}</td>
89   * <td>The priority value for this resource.
90   * The priority should be between 0 and 7 inclusive.
91   * 0 represents the highest priority and 7 represents the lowest.
92   * This header is optional and defaults to 0.</td>
93   * </tr>
94   * </table>
95   *
96   * <h3>Required Annotations</h3>
97   *
98   * SPDY requires that all Requests and Pushed Resources contain
99   * an HTTP "Host" header.
100  *
101  * <h3>Optional Annotations</h3>
102  *
103  * Requests and Pushed Resources must contain a SPDY scheme header.
104  * This can be set via the {@code "X-SPDY-Scheme"} header but otherwise
105  * defaults to "https" as that is the most common SPDY deployment.
106  *
107  * <h3>Chunked Content</h3>
108  *
109  * This encoder associates all {@link HttpContent}s that it receives
110  * with the most recently received 'chunked' {@link HttpRequest}
111  * or {@link HttpResponse}.
112  *
113  * <h3>Pushed Resources</h3>
114  *
115  * All pushed resources should be sent before sending the response
116  * that corresponds to the initial request.
117  */
118 public class SpdyHttpEncoder extends MessageToMessageEncoder<HttpObject> {
119 
120     private int currentStreamId;
121 
122     /**
123      * Creates a new instance.
124      *
125      * @param version the protocol version
126      */
127     public SpdyHttpEncoder(SpdyVersion version) {
128         if (version == null) {
129             throw new NullPointerException("version");
130         }
131     }
132 
133     @Override
134     protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
135 
136         boolean valid = false;
137         boolean last = false;
138 
139         if (msg instanceof HttpRequest) {
140 
141             HttpRequest httpRequest = (HttpRequest) msg;
142             SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpRequest);
143             out.add(spdySynStreamFrame);
144 
145             last = spdySynStreamFrame.isLast() || spdySynStreamFrame.isUnidirectional();
146             valid = true;
147         }
148         if (msg instanceof HttpResponse) {
149 
150             HttpResponse httpResponse = (HttpResponse) msg;
151             SpdyHeadersFrame spdyHeadersFrame = createHeadersFrame(httpResponse);
152             out.add(spdyHeadersFrame);
153 
154             last = spdyHeadersFrame.isLast();
155             valid = true;
156         }
157         if (msg instanceof HttpContent && !last) {
158 
159             HttpContent chunk = (HttpContent) msg;
160 
161             chunk.content().retain();
162             SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(currentStreamId, chunk.content());
163             if (chunk instanceof LastHttpContent) {
164                 LastHttpContent trailer = (LastHttpContent) chunk;
165                 HttpHeaders trailers = trailer.trailingHeaders();
166                 if (trailers.isEmpty()) {
167                     spdyDataFrame.setLast(true);
168                     out.add(spdyDataFrame);
169                 } else {
170                     // Create SPDY HEADERS frame out of trailers
171                     SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(currentStreamId);
172                     spdyHeadersFrame.setLast(true);
173                     for (Map.Entry<String, String> entry: trailers) {
174                         spdyHeadersFrame.headers().add(entry.getKey(), entry.getValue());
175                     }
176 
177                     // Write DATA frame and append HEADERS frame
178                     out.add(spdyDataFrame);
179                     out.add(spdyHeadersFrame);
180                 }
181             } else {
182                 out.add(spdyDataFrame);
183             }
184 
185             valid = true;
186         }
187 
188         if (!valid) {
189             throw new UnsupportedMessageTypeException(msg);
190         }
191     }
192 
193     @SuppressWarnings("deprecation")
194     private SpdySynStreamFrame createSynStreamFrame(HttpRequest httpRequest) throws Exception {
195         // Get the Stream-ID, Associated-To-Stream-ID, Priority, and scheme from the headers
196         final HttpHeaders httpHeaders = httpRequest.headers();
197         int streamId = SpdyHttpHeaders.getStreamId(httpRequest);
198         int associatedToStreamId = SpdyHttpHeaders.getAssociatedToStreamId(httpRequest);
199         byte priority = SpdyHttpHeaders.getPriority(httpRequest);
200         CharSequence scheme = httpHeaders.get(SpdyHttpHeaders.Names.SCHEME);
201         httpHeaders.remove(SpdyHttpHeaders.Names.STREAM_ID);
202         httpHeaders.remove(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID);
203         httpHeaders.remove(SpdyHttpHeaders.Names.PRIORITY);
204         httpHeaders.remove(SpdyHttpHeaders.Names.SCHEME);
205 
206         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
207         // headers are not valid and MUST not be sent.
208         httpHeaders.remove(HttpHeaders.Names.CONNECTION);
209         httpHeaders.remove("Keep-Alive");
210         httpHeaders.remove("Proxy-Connection");
211         httpHeaders.remove(HttpHeaders.Names.TRANSFER_ENCODING);
212 
213         SpdySynStreamFrame spdySynStreamFrame =
214                 new DefaultSpdySynStreamFrame(streamId, associatedToStreamId, priority);
215 
216         // Unfold the first line of the message into name/value pairs
217         SpdyHeaders frameHeaders = spdySynStreamFrame.headers();
218         frameHeaders.set(SpdyHeaders.HttpNames.METHOD, httpRequest.getMethod());
219         frameHeaders.set(SpdyHeaders.HttpNames.PATH, httpRequest.getUri());
220         frameHeaders.set(SpdyHeaders.HttpNames.VERSION, httpRequest.getProtocolVersion());
221 
222         // Replace the HTTP host header with the SPDY host header
223         CharSequence host = httpHeaders.get(HttpHeaders.Names.HOST);
224         httpHeaders.remove(HttpHeaders.Names.HOST);
225         frameHeaders.set(SpdyHeaders.HttpNames.HOST, host);
226 
227         // Set the SPDY scheme header
228         if (scheme == null) {
229             scheme = "https";
230         }
231         frameHeaders.set(SpdyHeaders.HttpNames.SCHEME, scheme);
232 
233         // Transfer the remaining HTTP headers
234         for (Map.Entry<String, String> entry: httpHeaders) {
235             frameHeaders.add(entry.getKey(), entry.getValue());
236         }
237         currentStreamId = spdySynStreamFrame.streamId();
238         if (associatedToStreamId == 0) {
239             spdySynStreamFrame.setLast(isLast(httpRequest));
240         } else {
241             spdySynStreamFrame.setUnidirectional(true);
242         }
243 
244         return spdySynStreamFrame;
245     }
246 
247     @SuppressWarnings("deprecation")
248     private SpdyHeadersFrame createHeadersFrame(HttpResponse httpResponse) throws Exception {
249         // Get the Stream-ID from the headers
250         final HttpHeaders httpHeaders = httpResponse.headers();
251         int streamId = SpdyHttpHeaders.getStreamId(httpResponse);
252         httpHeaders.remove(SpdyHttpHeaders.Names.STREAM_ID);
253 
254         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
255         // headers are not valid and MUST not be sent.
256         httpHeaders.remove(HttpHeaders.Names.CONNECTION);
257         httpHeaders.remove("Keep-Alive");
258         httpHeaders.remove("Proxy-Connection");
259         httpHeaders.remove(HttpHeaders.Names.TRANSFER_ENCODING);
260 
261         SpdyHeadersFrame spdyHeadersFrame;
262         if (SpdyCodecUtil.isServerId(streamId)) {
263             spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamId);
264         } else {
265             spdyHeadersFrame = new DefaultSpdySynReplyFrame(streamId);
266         }
267         SpdyHeaders frameHeaders = spdyHeadersFrame.headers();
268         // Unfold the first line of the response into name/value pairs
269         frameHeaders.set(SpdyHeaders.HttpNames.STATUS, httpResponse.getStatus().code());
270         frameHeaders.set(SpdyHeaders.HttpNames.VERSION, httpResponse.getProtocolVersion());
271 
272         // Transfer the remaining HTTP headers
273         for (Map.Entry<String, String> entry: httpHeaders) {
274             spdyHeadersFrame.headers().add(entry.getKey(), entry.getValue());
275         }
276 
277         currentStreamId = streamId;
278         spdyHeadersFrame.setLast(isLast(httpResponse));
279 
280         return spdyHeadersFrame;
281     }
282 
283     /**
284      * Checks if the given HTTP message should be considered as a last SPDY frame.
285      *
286      * @param httpMessage check this HTTP message
287      * @return whether the given HTTP message should generate a <em>last</em> SPDY frame.
288      */
289     private static boolean isLast(HttpMessage httpMessage) {
290         if (httpMessage instanceof FullHttpMessage) {
291             FullHttpMessage fullMessage = (FullHttpMessage) httpMessage;
292             if (fullMessage.trailingHeaders().isEmpty() && !fullMessage.content().isReadable()) {
293                 return true;
294             }
295         }
296 
297         return false;
298     }
299 }