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         super(HttpObject.class);
149         ObjectUtil.checkNotNull(version, "version");
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 }