1
2
3
4
5
6
7
8
9
10
11
12
13
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
67
68 public final class HttpConversionUtil {
69
70
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
101
102
103 private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
104
105 private HttpConversionUtil() {
106 }
107
108
109
110
111 public enum ExtensionHeaderNames {
112
113
114
115
116
117
118 STREAM_ID("x-http3-stream-id"),
119
120
121
122
123
124
125 SCHEME("x-http3-scheme"),
126
127
128
129
130
131
132 PATH("x-http3-path"),
133
134
135
136
137
138
139 STREAM_PROMISE_ID("x-http3-stream-promise-id"),
140
141
142
143
144
145
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
162
163
164
165
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
186
187
188
189
190
191
192
193
194
195
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
202
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
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
231
232
233
234
235
236
237
238
239
240
241
242 static FullHttpRequest toFullHttpRequest(long streamId, Http3Headers http3Headers, ByteBufAllocator alloc,
243 boolean validateHttpHeaders) throws Http3Exception {
244 ByteBuf content = alloc.buffer();
245
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
266
267
268
269
270
271
272
273
274
275
276 static HttpRequest toHttpRequest(long streamId, Http3Headers http3Headers, boolean validateHttpHeaders)
277 throws Http3Exception {
278
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
297
298
299
300
301
302
303
304
305
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
312
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
327
328
329
330
331
332
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
343
344
345
346
347
348
349
350
351
352
353
354 static void addHttp3ToHttpHeaders(long streamId, Http3Headers inputHeaders, HttpHeaders outputHeaders,
355 HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http3Exception {
356
357
358
359
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
382
383
384
385
386
387
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
396
397
398
399
400 String authorityForm = request.uri();
401 if (authorityForm != null) {
402
403
404
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
421
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
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
479
480 throw new IllegalStateException(e);
481 }
482 }
483 return result;
484 }
485
486
487
488
489
490
491
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
513
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
521
522 if (aName.contentEqualsIgnoreCase(TE)) {
523 toHttp3HeadersFilterTE(entry, out);
524 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
525 AsciiString value = AsciiString.of(entry.getValue());
526
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
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
546
547 throw new IllegalStateException(e);
548 }
549 } else {
550 out.add(aName, entry.getValue());
551 }
552 }
553 }
554 }
555
556
557
558
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
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
585 if (!isValidScheme(uri, i)) {
586 i = 0;
587 } else {
588 int authorityStart = i + 3;
589
590 int queryOrFragmentStart = queryOrFragmentStart(uri, authorityStart);
591 i = uri.indexOf('/', authorityStart);
592 if (i == -1 || (queryOrFragmentStart != -1 && queryOrFragmentStart < i)) {
593
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
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
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
630 pathBuilder.append(query);
631 } else if (fragmentStart == query.length() - 1) {
632
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
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
658 private static String http3PathlessRequestTarget(String requestTarget) {
659 int schemeEnd = requestTarget.indexOf("://");
660 int authorityStart = schemeEnd + 3;
661
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
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
697 static void setHttp3Authority(String authority, Http3Headers out) {
698
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
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
746
747 private static final class Http3ToHttpHeaderTranslator {
748
749
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
757
758
759
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
784
785
786
787
788
789
790
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
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
820
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
831
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
840
841
842
843
844
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 }