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 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
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 }
89
90
91
92
93
94 private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
95
96 private HttpConversionUtil() {
97 }
98
99
100
101
102 public enum ExtensionHeaderNames {
103
104
105
106
107
108
109 STREAM_ID("x-http3-stream-id"),
110
111
112
113
114
115
116 SCHEME("x-http3-scheme"),
117
118
119
120
121
122
123 PATH("x-http3-path"),
124
125
126
127
128
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
145
146
147
148
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
169
170
171
172
173
174
175
176
177
178
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
185
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
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
214
215
216
217
218
219
220
221
222
223
224
225 static FullHttpRequest toFullHttpRequest(long streamId, Http3Headers http3Headers, ByteBufAllocator alloc,
226 boolean validateHttpHeaders) throws Http3Exception {
227 ByteBuf content = alloc.buffer();
228
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
249
250
251
252
253
254
255
256
257
258
259 static HttpRequest toHttpRequest(long streamId, Http3Headers http3Headers, boolean validateHttpHeaders)
260 throws Http3Exception {
261
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
280
281
282
283
284
285
286
287
288
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
295
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
310
311
312
313
314
315
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
326
327
328
329
330
331
332
333
334
335
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
359
360
361
362
363
364
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
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
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
427
428 throw new IllegalStateException(e);
429 }
430 }
431 return result;
432 }
433
434
435
436
437
438
439
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
461
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
469
470 if (aName.contentEqualsIgnoreCase(TE)) {
471 toHttp3HeadersFilterTE(entry, out);
472 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
473 AsciiString value = AsciiString.of(entry.getValue());
474
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
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
494
495 throw new IllegalStateException(e);
496 }
497 } else {
498 out.add(aName, entry.getValue());
499 }
500 }
501 }
502 }
503
504
505
506
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
527 static void setHttp3Authority(@Nullable String authority, Http3Headers out) {
528
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
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
569
570 private static final class Http3ToHttpHeaderTranslator {
571
572
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
594
595
596
597
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
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
617
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
625
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 }