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