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