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