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    * https://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 io.netty.buffer.ByteBuf;
18  import io.netty.buffer.ByteBufAllocator;
19  import io.netty.handler.codec.UnsupportedValueConverter;
20  import io.netty.handler.codec.http.DefaultFullHttpRequest;
21  import io.netty.handler.codec.http.DefaultFullHttpResponse;
22  import io.netty.handler.codec.http.DefaultHttpRequest;
23  import io.netty.handler.codec.http.DefaultHttpResponse;
24  import io.netty.handler.codec.http.FullHttpMessage;
25  import io.netty.handler.codec.http.FullHttpRequest;
26  import io.netty.handler.codec.http.FullHttpResponse;
27  import io.netty.handler.codec.http.HttpHeaderNames;
28  import io.netty.handler.codec.http.HttpHeaders;
29  import io.netty.handler.codec.http.HttpMessage;
30  import io.netty.handler.codec.http.HttpMethod;
31  import io.netty.handler.codec.http.HttpRequest;
32  import io.netty.handler.codec.http.HttpResponse;
33  import io.netty.handler.codec.http.HttpResponseStatus;
34  import io.netty.handler.codec.http.HttpUtil;
35  import io.netty.handler.codec.http.HttpVersion;
36  import io.netty.util.AsciiString;
37  import io.netty.util.internal.InternalThreadLocalMap;
38  import io.netty.util.internal.StringUtil;
39  
40  import java.net.URI;
41  import java.util.Iterator;
42  import java.util.List;
43  import java.util.Map.Entry;
44  
45  import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
46  import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
47  import static io.netty.handler.codec.http.HttpHeaderNames.TE;
48  import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
49  import static io.netty.handler.codec.http.HttpResponseStatus.parseLine;
50  import static io.netty.handler.codec.http.HttpScheme.HTTP;
51  import static io.netty.handler.codec.http.HttpScheme.HTTPS;
52  import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm;
53  import static io.netty.handler.codec.http.HttpUtil.isOriginForm;
54  import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
55  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
56  import static io.netty.handler.codec.http2.Http2Exception.streamError;
57  import static io.netty.util.AsciiString.EMPTY_STRING;
58  import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
59  import static io.netty.util.AsciiString.indexOf;
60  import static io.netty.util.AsciiString.trim;
61  import static io.netty.util.ByteProcessor.FIND_COMMA;
62  import static io.netty.util.ByteProcessor.FIND_SEMI_COLON;
63  import static io.netty.util.internal.ObjectUtil.checkNotNull;
64  import static io.netty.util.internal.StringUtil.isNullOrEmpty;
65  import static io.netty.util.internal.StringUtil.unescapeCsvFields;
66  
67  /**
68   * Provides utility methods and constants for the HTTP/2 to HTTP conversion
69   */
70  public final class HttpConversionUtil {
71      // Parsing logic adapted from Vert.x HttpUtils.parsePath/parseQuery:
72      // https://github.com/eclipse-vertx/vert.x/blob/98a8ef6c8b408009ff86eb8277fd0bbb2b866857/
73      // vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java#L279-L319
74      /**
75       * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2.
76       */
77      private static final CharSequenceMap<AsciiString> HTTP_TO_HTTP2_HEADER_BLACKLIST =
78              new CharSequenceMap<AsciiString>();
79      static {
80          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(CONNECTION, EMPTY_STRING);
81          @SuppressWarnings("deprecation")
82          AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
83          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
84          @SuppressWarnings("deprecation")
85          AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION;
86          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING);
87          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
88          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
89          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
90          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
91          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
92          HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
93      }
94  
95      /**
96       * This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
97       * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
98       */
99      public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
100 
101     /**
102      * This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
103      * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
104      */
105     public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
106 
107     /**
108      * This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined
109      * in <a href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
110      */
111     public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
112 
113     /**
114      * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">[RFC 7540], 8.1.2.3</a> states the path must not
115      * be empty, and instead should be {@code /}.
116      */
117     private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
118 
119     private HttpConversionUtil() {
120     }
121 
122     /**
123      * Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects
124      */
125     public enum ExtensionHeaderNames {
126         /**
127          * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for
128          * generating an {@code HttpObject}
129          * <p>
130          * {@code "x-http2-stream-id"}
131          */
132         STREAM_ID("x-http2-stream-id"),
133         /**
134          * HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for
135          * generating an {@code HttpObject}
136          * <p>
137          * {@code "x-http2-scheme"}
138          */
139         SCHEME("x-http2-scheme"),
140         /**
141          * HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for
142          * generating an {@code HttpObject}
143          * <p>
144          * {@code "x-http2-path"}
145          */
146         PATH("x-http2-path"),
147         /**
148          * HTTP extension header which will identify the stream id used to create this stream in an HTTP/2 push promise
149          * frame
150          * <p>
151          * {@code "x-http2-stream-promise-id"}
152          */
153         STREAM_PROMISE_ID("x-http2-stream-promise-id"),
154         /**
155          * HTTP extension header which will identify the stream id which this stream is dependent on. This stream will
156          * be a child node of the stream id associated with this header value.
157          * <p>
158          * {@code "x-http2-stream-dependency-id"}
159          */
160         STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
161         /**
162          * HTTP extension header which will identify the weight (if non-default and the priority is not on the default
163          * stream) of the associated HTTP/2 stream responsible responsible for generating an {@code HttpObject}
164          * <p>
165          * {@code "x-http2-stream-weight"}
166          */
167         STREAM_WEIGHT("x-http2-stream-weight");
168 
169         private final AsciiString text;
170 
171         ExtensionHeaderNames(String text) {
172             this.text = AsciiString.cached(text);
173         }
174 
175         public AsciiString text() {
176             return text;
177         }
178     }
179 
180     /**
181      * Apply HTTP/2 rules while translating status code to {@link HttpResponseStatus}
182      *
183      * @param status The status from an HTTP/2 frame
184      * @return The HTTP/1.x status
185      * @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
186      */
187     public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception {
188         HttpResponseStatus result;
189         try {
190             result = parseLine(status);
191             if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
192                 throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
193             }
194         } catch (Http2Exception e) {
195             throw e;
196         } catch (Throwable t) {
197             throw connectionError(PROTOCOL_ERROR, t,
198                             "Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
199         }
200         return result;
201     }
202 
203     /**
204      * Create a new object to contain the response data
205      *
206      * @param streamId The stream associated with the response
207      * @param http2Headers The initial set of HTTP/2 headers to create the response with
208      * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
209      * @param validateHttpHeaders <ul>
210      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
211      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
212      *        </ul>
213      * @return A new response object which represents headers/data
214      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
215      */
216     public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
217                                                       boolean validateHttpHeaders) throws Http2Exception {
218         return toFullHttpResponse(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
219     }
220 
221     /**
222      * Create a new object to contain the response data
223      *
224      * @param streamId The stream associated with the response
225      * @param http2Headers The initial set of HTTP/2 headers to create the response with
226      * @param content {@link ByteBuf} content to put in {@link FullHttpResponse}
227      * @param validateHttpHeaders <ul>
228      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
229      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
230      *        </ul>
231      * @return A new response object which represents headers/data
232      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
233      */
234     public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBuf content,
235                                                       boolean validateHttpHeaders)
236                     throws Http2Exception {
237         HttpResponseStatus status = parseStatus(http2Headers.status());
238         // HTTP/2 does not define a way to carry the version or reason phrase that is included in an
239         // HTTP/1.1 status line.
240         FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content,
241                                                            validateHttpHeaders);
242         try {
243             addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
244         } catch (Http2Exception e) {
245             msg.release();
246             throw e;
247         } catch (Throwable t) {
248             msg.release();
249             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
250         }
251         return msg;
252     }
253 
254     /**
255      * Create a new object to contain the request data
256      *
257      * @param streamId The stream associated with the request
258      * @param http2Headers The initial set of HTTP/2 headers to create the request with
259      * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
260      * @param validateHttpHeaders <ul>
261      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
262      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
263      *        </ul>
264      * @return A new request object which represents headers/data
265      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
266      */
267     public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
268                                                     boolean validateHttpHeaders) throws Http2Exception {
269         return toFullHttpRequest(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
270     }
271 
272     private static String extractPath(CharSequence method, Http2Headers headers) {
273         if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
274             // See https://tools.ietf.org/html/rfc7231#section-4.3.6
275             return checkNotNull(headers.authority(),
276                     "authority header cannot be null in the conversion to HTTP/1.x").toString();
277         } else {
278             return checkNotNull(headers.path(),
279                     "path header cannot be null in conversion to HTTP/1.x").toString();
280         }
281     }
282 
283     /**
284      * Create a new object to contain the request data
285      *
286      * @param streamId The stream associated with the request
287      * @param http2Headers The initial set of HTTP/2 headers to create the request with
288      * @param content {@link ByteBuf} content to put in {@link FullHttpRequest}
289      * @param validateHttpHeaders <ul>
290      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
291      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
292      *        </ul>
293      * @return A new request object which represents headers/data
294      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
295      */
296     public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBuf content,
297                                                 boolean validateHttpHeaders) throws Http2Exception {
298         // HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
299         final CharSequence method = checkNotNull(http2Headers.method(),
300                 "method header cannot be null in conversion to HTTP/1.x");
301         final CharSequence path = extractPath(method, http2Headers);
302         FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
303                         .toString()), path.toString(), content, validateHttpHeaders);
304         try {
305             addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
306         } catch (Http2Exception e) {
307             msg.release();
308             throw e;
309         } catch (Throwable t) {
310             msg.release();
311             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
312         }
313         return msg;
314     }
315 
316     /**
317      * Create a new object to contain the request data.
318      *
319      * @param streamId The stream associated with the request
320      * @param http2Headers The initial set of HTTP/2 headers to create the request with
321      * @param validateHttpHeaders <ul>
322      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
323      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
324      *        </ul>
325      * @return A new request object which represents headers for a chunked request
326      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
327      */
328     public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
329                     throws Http2Exception {
330         // HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
331         final CharSequence method = checkNotNull(http2Headers.method(),
332                 "method header cannot be null in conversion to HTTP/1.x");
333         final CharSequence path = extractPath(method, http2Headers);
334         HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
335                 path.toString(), validateHttpHeaders);
336         try {
337             addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
338         } catch (Http2Exception e) {
339             throw e;
340         } catch (Throwable t) {
341             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
342         }
343         return msg;
344     }
345 
346     /**
347      * Create a new object to contain the response data.
348      *
349      * @param streamId The stream associated with the response
350      * @param http2Headers The initial set of HTTP/2 headers to create the response with
351      * @param validateHttpHeaders <ul>
352      *        <li>{@code true} to validate HTTP headers in the http-codec</li>
353      *        <li>{@code false} not to validate HTTP headers in the http-codec</li>
354      *        </ul>
355      * @return A new response object which represents headers for a chunked response
356      * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers,
357      *         HttpHeaders, HttpVersion, boolean, boolean)}
358      */
359     public static HttpResponse toHttpResponse(final int streamId,
360                                               final Http2Headers http2Headers,
361                                               final boolean validateHttpHeaders) throws Http2Exception {
362         final HttpResponseStatus status = parseStatus(http2Headers.status());
363         // HTTP/2 does not define a way to carry the version or reason phrase that is included in an
364         // HTTP/1.1 status line.
365         final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
366         try {
367             addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, false);
368         } catch (final Http2Exception e) {
369             throw e;
370         } catch (final Throwable t) {
371             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
372         }
373         return msg;
374     }
375 
376     /**
377      * Translate and add HTTP/2 headers to HTTP/1.x headers.
378      *
379      * @param streamId The stream associated with {@code sourceHeaders}.
380      * @param inputHeaders The HTTP/2 headers to convert.
381      * @param destinationMessage The object which will contain the resulting HTTP/1.x headers.
382      * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
383      * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
384      * @see #addHttp2ToHttpHeaders(int, Http2Headers, HttpHeaders, HttpVersion, boolean, boolean)
385      */
386     public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
387                     FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
388         addHttp2ToHttpHeaders(streamId, inputHeaders,
389                 addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(),
390                 destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest);
391     }
392 
393     /**
394      * Translate and add HTTP/2 headers to HTTP/1.x headers.
395      *
396      * @param streamId The stream associated with {@code sourceHeaders}.
397      * @param inputHeaders The HTTP/2 headers to convert.
398      * @param outputHeaders The object which will contain the resulting HTTP/1.x headers..
399      * @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion.
400      * @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers.
401      * {@code false} otherwise.
402      * @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message.
403      * {@code false} for response message.
404      * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
405      */
406     public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, HttpHeaders outputHeaders,
407             HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
408         Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
409         try {
410             translator.translateHeaders(inputHeaders);
411         } catch (Http2Exception ex) {
412             throw ex;
413         } catch (Throwable t) {
414             throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
415         }
416 
417         outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
418         outputHeaders.remove(HttpHeaderNames.TRAILER);
419         if (!isTrailer) {
420             outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
421             HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
422         }
423     }
424 
425     /**
426      * Converts the given HTTP/1.x headers into HTTP/2 headers.
427      * The following headers are only used if they can not be found in from the {@code HOST} header or the
428      * {@code Request-Line} as defined by <a href="https://tools.ietf.org/html/rfc7230">rfc7230</a>
429      * <ul>
430      * <li>{@link ExtensionHeaderNames#SCHEME}</li>
431      * </ul>
432      * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
433      */
434     public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) {
435         HttpHeaders inHeaders = in.headers();
436         final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
437         if (in instanceof HttpRequest) {
438             HttpRequest request = (HttpRequest) in;
439             String host = inHeaders.getAsString(HttpHeaderNames.HOST);
440             if (isOriginForm(request.uri()) || isAsteriskForm(request.uri())) {
441                 out.path(new AsciiString(request.uri()));
442                 setHttp2Scheme(inHeaders, out);
443             } else {
444                 String requestTarget = request.uri();
445                 out.path(toHttp2Path(requestTarget));
446                 if (hasSchemeAndAuthority(requestTarget)) {
447                     URI requestTargetUri = URI.create(http2PathlessRequestTarget(requestTarget));
448                     // Take from the request-line if HOST header was empty
449                     host = isNullOrEmpty(host) ? requestTargetUri.getAuthority() : host;
450                     setHttp2Scheme(inHeaders, requestTargetUri, out);
451                 } else {
452                     int schemeEnd = schemeEnd(requestTarget);
453                     if (schemeEnd != -1) {
454                         setHttp2Scheme(inHeaders, requestTarget.substring(0, schemeEnd), -1, out);
455                     } else {
456                         setHttp2Scheme(inHeaders, out);
457                     }
458                 }
459             }
460             setHttp2Authority(host, out);
461             out.method(request.method().asciiName());
462         } else if (in instanceof HttpResponse) {
463             HttpResponse response = (HttpResponse) in;
464             out.status(response.status().codeAsText());
465         }
466 
467         // Add the HTTP headers which have not been consumed above
468         toHttp2Headers(inHeaders, out);
469         return out;
470     }
471 
472     public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
473         if (inHeaders.isEmpty()) {
474             return EmptyHttp2Headers.INSTANCE;
475         }
476 
477         final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
478         toHttp2Headers(inHeaders, out);
479         return out;
480     }
481 
482     private static CharSequenceMap<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
483                                                                int arraySizeHint) {
484         UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.<AsciiString>instance();
485         CharSequenceMap<AsciiString> result = new CharSequenceMap<AsciiString>(true, valueConverter, arraySizeHint);
486 
487         while (valuesIter.hasNext()) {
488             AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
489             try {
490                 int index = lowerCased.forEachByte(FIND_COMMA);
491                 if (index != -1) {
492                     int start = 0;
493                     do {
494                         result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
495                         start = index + 1;
496                     } while (start < lowerCased.length() &&
497                              (index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1);
498                     result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
499                 } else {
500                     result.add(lowerCased.trim(), EMPTY_STRING);
501                 }
502             } catch (Exception e) {
503                 // This is not expect to happen because FIND_COMMA never throws but must be caught
504                 // because of the ByteProcessor interface.
505                 throw new IllegalStateException(e);
506             }
507         }
508         return result;
509     }
510 
511     /**
512      * Filter the {@link HttpHeaderNames#TE} header according to the
513      * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.2">special rules in the HTTP/2 RFC</a>.
514      * @param entry An entry whose name is {@link HttpHeaderNames#TE}.
515      * @param out the resulting HTTP/2 headers.
516      */
517     private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
518                                                Http2Headers out) {
519         if (indexOf(entry.getValue(), ',', 0) == -1) {
520             if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
521                 out.add(TE, TRAILERS);
522             }
523         } else {
524             List<CharSequence> teValues = unescapeCsvFields(entry.getValue());
525             for (CharSequence teValue : teValues) {
526                 if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
527                     out.add(TE, TRAILERS);
528                     break;
529                 }
530             }
531         }
532     }
533 
534     public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
535         Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
536         // Choose 8 as a default size because it is unlikely we will see more than 4 Connection headers values, but
537         // still allowing for "enough" space in the map to reduce the chance of hash code collision.
538         CharSequenceMap<AsciiString> connectionBlacklist =
539             toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
540         while (iter.hasNext()) {
541             Entry<CharSequence, CharSequence> entry = iter.next();
542             final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
543             if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
544                 // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
545                 if (aName.contentEqualsIgnoreCase(TE)) {
546                     toHttp2HeadersFilterTE(entry, out);
547                 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
548                     CharSequence valueCs = entry.getValue();
549                     // validate
550                     boolean invalid = false;
551                     for (int i = 0; i < valueCs.length(); i++) {
552                         char c = valueCs.charAt(i);
553                         if (c == ';') {
554                             if (i + 1 >= valueCs.length() || valueCs.charAt(i + 1) != ' ') {
555                                 // semicolon not followed by space. invalid, don't split
556                                 invalid = true;
557                                 break;
558                             }
559                             i++; // skip space
560                         } else if (c > 255) {
561                             // not ascii, don't split
562                             invalid = true;
563                             break;
564                         }
565                     }
566 
567                     if (invalid) {
568                         out.add(COOKIE, valueCs);
569                     } else {
570                         splitValidCookieHeader(out, valueCs);
571                     }
572                 } else {
573                     out.add(aName, entry.getValue());
574                 }
575             }
576         }
577     }
578 
579     private static void splitValidCookieHeader(Http2Headers out, CharSequence valueCs) {
580         try {
581             AsciiString value = AsciiString.of(valueCs);
582             // split up cookies to allow for better compression
583             // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
584             int index = value.forEachByte(FIND_SEMI_COLON);
585             if (index != -1) {
586                 int start = 0;
587                 do {
588                     out.add(COOKIE, value.subSequence(start, index, false));
589                     assert index + 1 < value.length();
590                     assert value.charAt(index + 1) == ' ';
591                     // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1)
592                     start = index + 2;
593                 } while (start < value.length() &&
594                         (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
595                 assert start < value.length();
596                 out.add(COOKIE, value.subSequence(start, value.length(), false));
597             } else {
598                 out.add(COOKIE, value);
599             }
600         } catch (Exception e) {
601             // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught
602             // because of the ByteProcessor interface.
603             throw new IllegalStateException(e);
604         }
605     }
606 
607     /**
608      * Generate an HTTP/2 {code :path} from a request-target in accordance with
609      * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
610      */
611     private static AsciiString toHttp2Path(String uri) {
612         String path = dropEmptyFragment(parsePath(uri));
613         String query = parseQuery(uri);
614         if (isNullOrEmpty(query)) {
615             return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
616         }
617         StringBuilder pathBuilder = new StringBuilder(path.length() + query.length() + 1);
618         pathBuilder.append(path);
619         appendQuery(pathBuilder, query);
620         return new AsciiString(pathBuilder.toString());
621     }
622 
623     /**
624      * Extract the path out of the request-target. Based on Vert.x' HttpUtils.parsePath logic.
625      */
626     private static String parsePath(String uri) {
627         if (uri.isEmpty()) {
628             return StringUtil.EMPTY_STRING;
629         }
630         int i;
631         if (uri.charAt(0) == '/') {
632             i = 0;
633         } else {
634             i = uri.indexOf("://");
635             // Netty change: validate the scheme before treating :// as authority syntax.
636             if (!isValidScheme(uri, i)) {
637                 i = 0;
638             } else {
639                 int authorityStart = i + 3;
640                 // Netty change: only accept '/' before query/fragment as path start.
641                 int queryOrFragmentStart = queryOrFragmentStart(uri, authorityStart);
642                 i = uri.indexOf('/', authorityStart);
643                 if (i == -1 || (queryOrFragmentStart != -1 && queryOrFragmentStart < i)) {
644                     // contains no /
645                     return "/";
646                 }
647             }
648         }
649 
650         int queryStart = uri.indexOf('?', i);
651         if (queryStart == -1) {
652             queryStart = uri.length();
653             if (i == 0) {
654                 return uri;
655             }
656         }
657         return uri.substring(i, queryStart);
658     }
659 
660     /**
661      * Extract the query out of a request-target or returns {@code null} if no query was found.
662      */
663     private static String parseQuery(String uri) {
664         int i = uri.indexOf('?');
665         if (i == -1) {
666             return null;
667         } else {
668             return uri.substring(i + 1);
669         }
670     }
671 
672     private static String dropEmptyFragment(String path) {
673         // Netty change: old URI-based conversion dropped an empty fragment delimiter.
674         return path.endsWith("#") ? path.substring(0, path.length() - 1) : path;
675     }
676 
677     private static void appendQuery(StringBuilder pathBuilder, String query) {
678         int fragmentStart = query.indexOf('#');
679         if (fragmentStart == 0) {
680             // Netty change: old URI-based conversion skipped an empty query before a fragment.
681             pathBuilder.append(query);
682         } else if (fragmentStart == query.length() - 1) {
683             // Netty change: old URI-based conversion dropped an empty fragment delimiter after a query.
684             pathBuilder.append('?').append(query, 0, fragmentStart);
685         } else {
686             pathBuilder.append('?').append(query);
687         }
688     }
689 
690     static int queryOrFragmentStart(String uri, int searchStart) {
691         int queryStart = uri.indexOf('?', searchStart);
692         int fragmentStart = uri.indexOf('#', searchStart);
693         return queryStart == -1 ? fragmentStart :
694                 fragmentStart == -1 ? queryStart : Math.min(queryStart, fragmentStart);
695     }
696 
697     // Netty addition: detect authority for HTTP/2 :scheme/:authority extraction.
698     static boolean hasSchemeAndAuthority(String requestTarget) {
699         int schemeEnd = requestTarget.indexOf("://");
700         return isValidScheme(requestTarget, schemeEnd);
701     }
702 
703     private static int schemeEnd(String requestTarget) {
704         int schemeEnd = requestTarget.indexOf(':');
705         return isValidScheme(requestTarget, schemeEnd) ? schemeEnd : -1;
706     }
707 
708     // Netty addition: prepare only scheme://authority for URI validation.
709     private static String http2PathlessRequestTarget(String requestTarget) {
710         int schemeEnd = requestTarget.indexOf("://");
711         int authorityStart = schemeEnd + 3;
712         // Netty addition: strip before path/query/fragment; Vert.x parsePath does not validate authority.
713         int pathStart = requestTarget.indexOf('/', authorityStart);
714         int delimiter = queryOrFragmentStart(requestTarget, authorityStart);
715         if (pathStart != -1 && (delimiter == -1 || pathStart < delimiter)) {
716             delimiter = pathStart;
717         }
718         if (delimiter == -1) {
719             return requestTarget;
720         }
721         return delimiter == authorityStart ? requestTarget.substring(0, delimiter + 1) :
722                 requestTarget.substring(0, delimiter);
723     }
724 
725     // Netty addition: validate the text before :// as a scheme.
726     static boolean isValidScheme(String uri, int schemeEnd) {
727         if (schemeEnd <= 0) {
728             return false;
729         }
730         char first = uri.charAt(0);
731         if (!isAlpha(first)) {
732             return false;
733         }
734         for (int i = 1; i < schemeEnd; ++i) {
735             char c = uri.charAt(i);
736             if (!isAlpha(c) && (c < '0' || c > '9') && c != '+' && c != '-' && c != '.') {
737                 return false;
738             }
739         }
740         return true;
741     }
742 
743     private static boolean isAlpha(char c) {
744         return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
745     }
746 
747     // package-private for testing only
748     static void setHttp2Authority(String authority, Http2Headers out) {
749         // The authority MUST NOT include the deprecated "userinfo" subcomponent
750         if (authority != null) {
751             if (authority.isEmpty()) {
752                 out.authority(EMPTY_STRING);
753             } else {
754                 int start = authority.indexOf('@') + 1;
755                 int length = authority.length() - start;
756                 if (length == 0) {
757                     throw new IllegalArgumentException("authority: " + authority);
758                 }
759                 out.authority(new AsciiString(authority, start, length));
760             }
761         }
762     }
763 
764     private static void setHttp2Scheme(HttpHeaders in, Http2Headers out) {
765         setHttp2Scheme(in, URI.create(""), out);
766     }
767 
768     private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
769         setHttp2Scheme(in, uri.getScheme(), uri.getPort(), out);
770     }
771 
772     private static void setHttp2Scheme(HttpHeaders in, String scheme, int port, Http2Headers out) {
773         if (!isNullOrEmpty(scheme)) {
774             out.scheme(new AsciiString(scheme));
775             return;
776         }
777 
778         // Consume the Scheme extension header if present
779         CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
780         if (cValue != null) {
781             out.scheme(AsciiString.of(cValue));
782             return;
783         }
784 
785         if (port == HTTPS.port()) {
786             out.scheme(HTTPS.name());
787         } else if (port == HTTP.port()) {
788             out.scheme(HTTP.name());
789         } else {
790             throw new IllegalArgumentException(":scheme must be specified. " +
791                     "see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
792         }
793     }
794 
795     /**
796      * Utility which translates HTTP/2 headers to HTTP/1 headers.
797      */
798     private static final class Http2ToHttpHeaderTranslator {
799         /**
800          * Translations from HTTP/2 header name to the HTTP/1.x equivalent.
801          */
802         private static final CharSequenceMap<AsciiString>
803             REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
804         private static final CharSequenceMap<AsciiString>
805             RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
806         static {
807             RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
808                             HttpHeaderNames.HOST);
809             RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
810                             ExtensionHeaderNames.SCHEME.text());
811             REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
812             RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
813                             ExtensionHeaderNames.PATH.text());
814         }
815 
816         private final int streamId;
817         private final HttpHeaders output;
818         private final CharSequenceMap<AsciiString> translations;
819 
820         /**
821          * Create a new instance
822          *
823          * @param output The HTTP/1.x headers object to store the results of the translation
824          * @param request if {@code true}, translates headers using the request translation map. Otherwise uses the
825          *        response translation map.
826          */
827         Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
828             this.streamId = streamId;
829             this.output = output;
830             translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
831         }
832 
833         void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
834             // lazily created as needed
835             StringBuilder cookies = null;
836 
837             for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
838                 final CharSequence name = entry.getKey();
839                 final CharSequence value = entry.getValue();
840                 AsciiString translatedName = translations.get(name);
841                 if (translatedName != null) {
842                     output.add(translatedName, AsciiString.of(value));
843                 } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
844                     // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
845                     // All headers that start with ':' are only valid in HTTP/2 context
846                     if (name.length() == 0 || name.charAt(0) == ':') {
847                         throw streamError(streamId, PROTOCOL_ERROR,
848                                 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
849                     }
850                     if (COOKIE.equals(name)) {
851                         // combine the cookie values into 1 header entry.
852                         // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
853                         if (cookies == null) {
854                             cookies = InternalThreadLocalMap.get().stringBuilder();
855                         } else if (cookies.length() > 0) {
856                             cookies.append("; ");
857                         }
858                         cookies.append(value);
859                     } else {
860                         output.add(name, value);
861                     }
862                 }
863             }
864             if (cookies != null) {
865                 output.add(COOKIE, cookies.toString());
866             }
867         }
868     }
869 }