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