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