View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * 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 distributed under the License
11   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing permissions and limitations under
13   * the License.
14   */
15  package io.netty.handler.codec.http2;
16  
17  import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
18  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
19  import static io.netty.handler.codec.http2.Http2Exception.streamError;
20  import static io.netty.util.internal.ObjectUtil.checkNotNull;
21  import io.netty.handler.codec.AsciiString;
22  import io.netty.handler.codec.BinaryHeaders;
23  import io.netty.handler.codec.TextHeaders.EntryVisitor;
24  import io.netty.handler.codec.http.DefaultFullHttpRequest;
25  import io.netty.handler.codec.http.DefaultFullHttpResponse;
26  import io.netty.handler.codec.http.FullHttpMessage;
27  import io.netty.handler.codec.http.FullHttpRequest;
28  import io.netty.handler.codec.http.FullHttpResponse;
29  import io.netty.handler.codec.http.HttpHeaderNames;
30  import io.netty.handler.codec.http.HttpHeaderUtil;
31  import io.netty.handler.codec.http.HttpHeaderValues;
32  import io.netty.handler.codec.http.HttpHeaders;
33  import io.netty.handler.codec.http.HttpMethod;
34  import io.netty.handler.codec.http.HttpRequest;
35  import io.netty.handler.codec.http.HttpResponse;
36  import io.netty.handler.codec.http.HttpResponseStatus;
37  import io.netty.handler.codec.http.HttpVersion;
38  
39  import java.net.URI;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.Map;
43  import java.util.Map.Entry;
44  import java.util.Set;
45  import java.util.regex.Pattern;
46  
47  /**
48   * Provides utility methods and constants for the HTTP/2 to HTTP conversion
49   */
50  public final class HttpUtil {
51      /**
52       * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2.
53       */
54      @SuppressWarnings("deprecation")
55      private static final Set<CharSequence> HTTP_TO_HTTP2_HEADER_BLACKLIST = new HashSet<CharSequence>() {
56          private static final long serialVersionUID = -5678614530214167043L;
57          {
58              add(HttpHeaderNames.CONNECTION);
59              add(HttpHeaderNames.KEEP_ALIVE);
60              add(HttpHeaderNames.PROXY_CONNECTION);
61              add(HttpHeaderNames.TRANSFER_ENCODING);
62              add(HttpHeaderNames.HOST);
63              add(HttpHeaderNames.UPGRADE);
64              add(ExtensionHeaderNames.STREAM_ID.text());
65              add(ExtensionHeaderNames.AUTHORITY.text());
66              add(ExtensionHeaderNames.SCHEME.text());
67              add(ExtensionHeaderNames.PATH.text());
68          }
69      };
70  
71      /**
72       * This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
73       * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
74       */
75      public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
76  
77      /**
78       * This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
79       * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
80       */
81      public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
82  
83      /**
84       * This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined
85       * in <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
86       */
87      public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
88  
89      /**
90       * This pattern will use to avoid compile it each time it is used
91       * when we need to replace some part of authority.
92       */
93      private static final Pattern AUTHORITY_REPLACEMENT_PATTERN = Pattern.compile("^.*@");
94  
95      private HttpUtil() {
96      }
97  
98      /**
99       * Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects
100      */
101     public enum ExtensionHeaderNames {
102         /**
103          * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for generating a
104          * {@code HttpObject}
105          * <p>
106          * {@code "x-http2-stream-id"}
107          */
108         STREAM_ID("x-http2-stream-id"),
109 
110         /**
111          * HTTP extension header which will identify the authority pseudo header from the HTTP/2 event(s) responsible
112          * for generating a {@code HttpObject}
113          * <p>
114          * {@code "x-http2-authority"}
115          */
116         AUTHORITY("x-http2-authority"),
117         /**
118          * HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for
119          * generating a {@code HttpObject}
120          * <p>
121          * {@code "x-http2-scheme"}
122          */
123         SCHEME("x-http2-scheme"),
124         /**
125          * HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for
126          * generating a {@code HttpObject}
127          * <p>
128          * {@code "x-http2-path"}
129          */
130         PATH("x-http2-path"),
131         /**
132          * HTTP extension header which will identify the stream id used to create this stream in a HTTP/2 push promise
133          * frame
134          * <p>
135          * {@code "x-http2-stream-promise-id"}
136          */
137         STREAM_PROMISE_ID("x-http2-stream-promise-id"),
138         /**
139          * HTTP extension header which will identify the stream id which this stream is dependent on. This stream will
140          * be a child node of the stream id associated with this header value.
141          * <p>
142          * {@code "x-http2-stream-dependency-id"}
143          */
144         STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
145         /**
146          * HTTP extension header which will identify the weight (if non-default and the priority is not on the default
147          * stream) of the associated HTTP/2 stream responsible responsible for generating a {@code HttpObject}
148          * <p>
149          * {@code "x-http2-stream-weight"}
150          */
151         STREAM_WEIGHT("x-http2-stream-weight");
152 
153         private final AsciiString text;
154 
155         ExtensionHeaderNames(String text) {
156             this.text = new AsciiString(text);
157         }
158 
159         public AsciiString text() {
160             return text;
161         }
162     }
163 
164     /**
165      * Apply HTTP/2 rules while translating status code to {@link HttpResponseStatus}
166      *
167      * @param status The status from an HTTP/2 frame
168      * @return The HTTP/1.x status
169      * @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
170      */
171     public static HttpResponseStatus parseStatus(AsciiString status) throws Http2Exception {
172         HttpResponseStatus result;
173         try {
174             result = HttpResponseStatus.parseLine(status);
175             if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
176                 throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
177             }
178         } catch (Http2Exception e) {
179             throw e;
180         } catch (Throwable t) {
181             throw connectionError(PROTOCOL_ERROR, t,
182                             "Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
183         }
184         return result;
185     }
186 
187     /**
188      * Create a new object to contain the response data
189      *
190      * @param streamId The stream associated with the response
191      * @param http2Headers The initial set of HTTP/2 headers to create the response with
192      * @param validateHttpHeaders <ul>
193      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
194      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
195      *        </ul>
196      * @return A new response object which represents headers/data
197      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
198      */
199     public static FullHttpResponse toHttpResponse(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
200                     throws Http2Exception {
201         HttpResponseStatus status = parseStatus(http2Headers.status());
202         // HTTP/2 does not define a way to carry the version or reason phrase that is included in an
203         // HTTP/1.1 status line.
204         FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
205         addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
206         return msg;
207     }
208 
209     /**
210      * Create a new object to contain the request data
211      *
212      * @param streamId The stream associated with the request
213      * @param http2Headers The initial set of HTTP/2 headers to create the request with
214      * @param validateHttpHeaders <ul>
215      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
216      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
217      *        </ul>
218      * @return A new request object which represents headers/data
219      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
220      */
221     public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
222                     throws Http2Exception {
223         // HTTP/2 does not define a way to carry the version identifier that is
224         // included in the HTTP/1.1 request line.
225         final AsciiString method = checkNotNull(http2Headers.method(),
226                 "method header cannot be null in conversion to HTTP/1.x");
227         final AsciiString path = checkNotNull(http2Headers.path(),
228                 "path header cannot be null in conversion to HTTP/1.x");
229         FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
230                         .toString()), path.toString(), validateHttpHeaders);
231         addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
232         return msg;
233     }
234 
235     /**
236      * Translate and add HTTP/2 headers to HTTP/1.x headers
237      *
238      * @param streamId The stream associated with {@code sourceHeaders}
239      * @param sourceHeaders The HTTP/2 headers to convert
240      * @param destinationMessage The object which will contain the resulting HTTP/1.x headers
241      * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
242      * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x
243      */
244     public static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
245                     FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
246         HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers();
247         boolean request = destinationMessage instanceof HttpRequest;
248         Http2ToHttpHeaderTranslator visitor = new Http2ToHttpHeaderTranslator(streamId, headers, request);
249         try {
250             sourceHeaders.forEachEntry(visitor);
251         } catch (Http2Exception ex) {
252             throw ex;
253         } catch (Throwable t) {
254             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
255         }
256 
257         headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
258         headers.remove(HttpHeaderNames.TRAILER);
259         if (!addToTrailer) {
260             headers.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
261             HttpHeaderUtil.setKeepAlive(destinationMessage, true);
262         }
263     }
264 
265     /**
266      * Converts the given HTTP/1.x headers into HTTP/2 headers.
267      */
268     public static Http2Headers toHttp2Headers(FullHttpMessage in) throws Exception {
269         final Http2Headers out = new DefaultHttp2Headers();
270         HttpHeaders inHeaders = in.headers();
271         if (in instanceof HttpRequest) {
272             HttpRequest request = (HttpRequest) in;
273             out.path(new AsciiString(request.uri()));
274             out.method(new AsciiString(request.method().toString()));
275 
276             String value = inHeaders.getAndConvert(HttpHeaderNames.HOST);
277             if (value != null) {
278                 URI hostUri = URI.create(value);
279                 // The authority MUST NOT include the deprecated "userinfo" subcomponent
280                 value = hostUri.getAuthority();
281                 if (value != null) {
282                     out.authority(new AsciiString(AUTHORITY_REPLACEMENT_PATTERN.matcher(value).replaceFirst("")));
283                 }
284                 value = hostUri.getScheme();
285                 if (value != null) {
286                     out.scheme(new AsciiString(value));
287                 }
288             }
289 
290             // Consume the Authority extension header if present
291             CharSequence cValue = inHeaders.get(ExtensionHeaderNames.AUTHORITY.text());
292             if (cValue != null) {
293                 out.authority(AsciiString.of(cValue));
294             }
295 
296             // Consume the Scheme extension header if present
297             cValue = inHeaders.get(ExtensionHeaderNames.SCHEME.text());
298             if (cValue != null) {
299                 out.scheme(AsciiString.of(cValue));
300             }
301         } else if (in instanceof HttpResponse) {
302             HttpResponse response = (HttpResponse) in;
303             out.status(new AsciiString(Integer.toString(response.status().code())));
304         }
305 
306         // Add the HTTP headers which have not been consumed above
307         inHeaders.forEachEntry(new EntryVisitor() {
308             @Override
309             public boolean visit(Entry<CharSequence, CharSequence> entry) throws Exception {
310                 final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
311                 if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) {
312                     AsciiString aValue = AsciiString.of(entry.getValue());
313                     // https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.2
314                     // makes a special exception for TE
315                     if (!aName.equalsIgnoreCase(HttpHeaderNames.TE) ||
316                         aValue.equalsIgnoreCase(HttpHeaderValues.TRAILERS)) {
317                         out.add(aName, aValue);
318                     }
319                 }
320                 return true;
321             }
322         });
323         return out;
324     }
325 
326     /**
327      * A visitor which translates HTTP/2 headers to HTTP/1 headers
328      */
329     private static final class Http2ToHttpHeaderTranslator implements BinaryHeaders.EntryVisitor {
330         /**
331          * Translations from HTTP/2 header name to the HTTP/1.x equivalent.
332          */
333         private static final Map<AsciiString, AsciiString>
334             REQUEST_HEADER_TRANSLATIONS = new HashMap<AsciiString, AsciiString>();
335         private static final Map<AsciiString, AsciiString>
336             RESPONSE_HEADER_TRANSLATIONS = new HashMap<AsciiString, AsciiString>();
337         static {
338             RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
339                             ExtensionHeaderNames.AUTHORITY.text());
340             RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.SCHEME.value(),
341                             ExtensionHeaderNames.SCHEME.text());
342             REQUEST_HEADER_TRANSLATIONS.putAll(RESPONSE_HEADER_TRANSLATIONS);
343             RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.PATH.value(),
344                             ExtensionHeaderNames.PATH.text());
345         }
346 
347         private final int streamId;
348         private final HttpHeaders output;
349         private final Map<AsciiString, AsciiString> translations;
350 
351         /**
352          * Create a new instance
353          *
354          * @param output The HTTP/1.x headers object to store the results of the translation
355          * @param request if {@code true}, translates headers using the request translation map. Otherwise uses the
356          *        response translation map.
357          */
358         Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
359             this.streamId = streamId;
360             this.output = output;
361             translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
362         }
363 
364         @Override
365         public boolean visit(Entry<AsciiString, AsciiString> entry) throws Http2Exception {
366             final AsciiString name = entry.getKey();
367             final AsciiString value = entry.getValue();
368             AsciiString translatedName = translations.get(name);
369             if (translatedName != null || !Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
370                 if (translatedName == null) {
371                     translatedName = name;
372                 }
373 
374                 // http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3
375                 // All headers that start with ':' are only valid in HTTP/2 context
376                 if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
377                     throw streamError(streamId, PROTOCOL_ERROR,
378                             "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", translatedName);
379                 } else {
380                     output.add(translatedName, value);
381                 }
382             }
383             return true;
384         }
385     }
386 }