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  import io.netty.util.AsciiString;
31  
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  
37  /**
38   * Encodes {@link HttpRequest}s, {@link HttpResponse}s, and {@link HttpContent}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 HttpRequest}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   * </table>
99   *
100  * <h3>Required Annotations</h3>
101  *
102  * SPDY requires that all Requests and Pushed Resources contain
103  * an HTTP "Host" header.
104  *
105  * <h3>Optional Annotations</h3>
106  *
107  * Requests and Pushed Resources must contain a SPDY scheme header.
108  * This can be set via the {@code "X-SPDY-Scheme"} header but otherwise
109  * defaults to "https" as that is the most common SPDY deployment.
110  *
111  * <h3>Chunked Content</h3>
112  *
113  * This encoder associates all {@link HttpContent}s that it receives
114  * with the most recently received 'chunked' {@link HttpRequest}
115  * or {@link HttpResponse}.
116  *
117  * <h3>Pushed Resources</h3>
118  *
119  * All pushed resources should be sent before sending the response
120  * that corresponds to the initial request.
121  */
122 public class SpdyHttpEncoder extends MessageToMessageEncoder<HttpObject> {
123 
124     private int currentStreamId;
125 
126     private final boolean validateHeaders;
127     private final boolean headersToLowerCase;
128 
129     /**
130      * Creates a new instance.
131      *
132      * @param version the protocol version
133      */
134     public SpdyHttpEncoder(SpdyVersion version) {
135         this(version, true, true);
136     }
137 
138     /**
139      * Creates a new instance.
140      *
141      * @param version            the protocol version
142      * @param headersToLowerCase convert header names to lowercase. In a controlled environment,
143      *                           one can disable the conversion.
144      * @param validateHeaders    validate the header names and values when adding them to the {@link SpdyHeaders}
145      */
146     public SpdyHttpEncoder(SpdyVersion version, boolean headersToLowerCase, boolean validateHeaders) {
147         if (version == null) {
148             throw new NullPointerException("version");
149         }
150         this.headersToLowerCase = headersToLowerCase;
151         this.validateHeaders = validateHeaders;
152     }
153 
154     @Override
155     protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
156 
157         boolean valid = false;
158         boolean last = false;
159 
160         if (msg instanceof HttpRequest) {
161 
162             HttpRequest httpRequest = (HttpRequest) msg;
163             SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpRequest);
164             out.add(spdySynStreamFrame);
165 
166             last = spdySynStreamFrame.isLast() || spdySynStreamFrame.isUnidirectional();
167             valid = true;
168         }
169         if (msg instanceof HttpResponse) {
170 
171             HttpResponse httpResponse = (HttpResponse) msg;
172             SpdyHeadersFrame spdyHeadersFrame = createHeadersFrame(httpResponse);
173             out.add(spdyHeadersFrame);
174 
175             last = spdyHeadersFrame.isLast();
176             valid = true;
177         }
178         if (msg instanceof HttpContent && !last) {
179 
180             HttpContent chunk = (HttpContent) msg;
181 
182             chunk.content().retain();
183             SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(currentStreamId, chunk.content());
184             if (chunk instanceof LastHttpContent) {
185                 LastHttpContent trailer = (LastHttpContent) chunk;
186                 HttpHeaders trailers = trailer.trailingHeaders();
187                 if (trailers.isEmpty()) {
188                     spdyDataFrame.setLast(true);
189                     out.add(spdyDataFrame);
190                 } else {
191                     // Create SPDY HEADERS frame out of trailers
192                     SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(currentStreamId, validateHeaders);
193                     spdyHeadersFrame.setLast(true);
194                     Iterator<Entry<CharSequence, CharSequence>> itr = trailers.iteratorCharSequence();
195                     while (itr.hasNext()) {
196                         Map.Entry<CharSequence, CharSequence> entry = itr.next();
197                         final CharSequence headerName =
198                                 headersToLowerCase ? AsciiString.of(entry.getKey()).toLowerCase() : entry.getKey();
199                         spdyHeadersFrame.headers().add(headerName, entry.getValue());
200                     }
201 
202                     // Write DATA frame and append HEADERS frame
203                     out.add(spdyDataFrame);
204                     out.add(spdyHeadersFrame);
205                 }
206             } else {
207                 out.add(spdyDataFrame);
208             }
209 
210             valid = true;
211         }
212 
213         if (!valid) {
214             throw new UnsupportedMessageTypeException(msg);
215         }
216     }
217 
218     @SuppressWarnings("deprecation")
219     private SpdySynStreamFrame createSynStreamFrame(HttpRequest httpRequest) throws Exception {
220         // Get the Stream-ID, Associated-To-Stream-ID, Priority, and scheme from the headers
221         final HttpHeaders httpHeaders = httpRequest.headers();
222         int streamId = httpHeaders.getInt(SpdyHttpHeaders.Names.STREAM_ID);
223         int associatedToStreamId = httpHeaders.getInt(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID, 0);
224         byte priority = (byte) httpHeaders.getInt(SpdyHttpHeaders.Names.PRIORITY, 0);
225         CharSequence scheme = httpHeaders.get(SpdyHttpHeaders.Names.SCHEME);
226         httpHeaders.remove(SpdyHttpHeaders.Names.STREAM_ID);
227         httpHeaders.remove(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID);
228         httpHeaders.remove(SpdyHttpHeaders.Names.PRIORITY);
229         httpHeaders.remove(SpdyHttpHeaders.Names.SCHEME);
230 
231         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
232         // headers are not valid and MUST not be sent.
233         httpHeaders.remove(HttpHeaderNames.CONNECTION);
234         httpHeaders.remove("Keep-Alive");
235         httpHeaders.remove("Proxy-Connection");
236         httpHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
237 
238         SpdySynStreamFrame spdySynStreamFrame =
239                 new DefaultSpdySynStreamFrame(streamId, associatedToStreamId, priority, validateHeaders);
240 
241         // Unfold the first line of the message into name/value pairs
242         SpdyHeaders frameHeaders = spdySynStreamFrame.headers();
243         frameHeaders.set(SpdyHeaders.HttpNames.METHOD, httpRequest.method().name());
244         frameHeaders.set(SpdyHeaders.HttpNames.PATH, httpRequest.uri());
245         frameHeaders.set(SpdyHeaders.HttpNames.VERSION, httpRequest.protocolVersion().text());
246 
247         // Replace the HTTP host header with the SPDY host header
248         CharSequence host = httpHeaders.get(HttpHeaderNames.HOST);
249         httpHeaders.remove(HttpHeaderNames.HOST);
250         frameHeaders.set(SpdyHeaders.HttpNames.HOST, host);
251 
252         // Set the SPDY scheme header
253         if (scheme == null) {
254             scheme = "https";
255         }
256         frameHeaders.set(SpdyHeaders.HttpNames.SCHEME, scheme);
257 
258         // Transfer the remaining HTTP headers
259         Iterator<Entry<CharSequence, CharSequence>> itr = httpHeaders.iteratorCharSequence();
260         while (itr.hasNext()) {
261             Map.Entry<CharSequence, CharSequence> entry = itr.next();
262             final CharSequence headerName =
263                     headersToLowerCase ? AsciiString.of(entry.getKey()).toLowerCase() : entry.getKey();
264             frameHeaders.add(headerName, entry.getValue());
265         }
266         currentStreamId = spdySynStreamFrame.streamId();
267         if (associatedToStreamId == 0) {
268             spdySynStreamFrame.setLast(isLast(httpRequest));
269         } else {
270             spdySynStreamFrame.setUnidirectional(true);
271         }
272 
273         return spdySynStreamFrame;
274     }
275 
276     @SuppressWarnings("deprecation")
277     private SpdyHeadersFrame createHeadersFrame(HttpResponse httpResponse) throws Exception {
278         // Get the Stream-ID from the headers
279         final HttpHeaders httpHeaders = httpResponse.headers();
280         int streamId = httpHeaders.getInt(SpdyHttpHeaders.Names.STREAM_ID);
281         httpHeaders.remove(SpdyHttpHeaders.Names.STREAM_ID);
282 
283         // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding
284         // headers are not valid and MUST not be sent.
285         httpHeaders.remove(HttpHeaderNames.CONNECTION);
286         httpHeaders.remove("Keep-Alive");
287         httpHeaders.remove("Proxy-Connection");
288         httpHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
289 
290         SpdyHeadersFrame spdyHeadersFrame;
291         if (SpdyCodecUtil.isServerId(streamId)) {
292             spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamId, validateHeaders);
293         } else {
294             spdyHeadersFrame = new DefaultSpdySynReplyFrame(streamId, validateHeaders);
295         }
296         SpdyHeaders frameHeaders = spdyHeadersFrame.headers();
297         // Unfold the first line of the response into name/value pairs
298         frameHeaders.set(SpdyHeaders.HttpNames.STATUS, httpResponse.status().codeAsText());
299         frameHeaders.set(SpdyHeaders.HttpNames.VERSION, httpResponse.protocolVersion().text());
300 
301         // Transfer the remaining HTTP headers
302         Iterator<Entry<CharSequence, CharSequence>> itr = httpHeaders.iteratorCharSequence();
303         while (itr.hasNext()) {
304             Map.Entry<CharSequence, CharSequence> entry = itr.next();
305             final CharSequence headerName =
306                     headersToLowerCase ? AsciiString.of(entry.getKey()).toLowerCase() : entry.getKey();
307             spdyHeadersFrame.headers().add(headerName, entry.getValue());
308         }
309 
310         currentStreamId = streamId;
311         spdyHeadersFrame.setLast(isLast(httpResponse));
312 
313         return spdyHeadersFrame;
314     }
315 
316     /**
317      * Checks if the given HTTP message should be considered as a last SPDY frame.
318      *
319      * @param httpMessage check this HTTP message
320      * @return whether the given HTTP message should generate a <em>last</em> SPDY frame.
321      */
322     private static boolean isLast(HttpMessage httpMessage) {
323         if (httpMessage instanceof FullHttpMessage) {
324             FullHttpMessage fullMessage = (FullHttpMessage) httpMessage;
325             if (fullMessage.trailingHeaders().isEmpty() && !fullMessage.content().isReadable()) {
326                 return true;
327             }
328         }
329 
330         return false;
331     }
332 }