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