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