1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package io.netty5.handler.codec.http2;
16
17 import io.netty5.buffer.api.Buffer;
18 import io.netty5.buffer.api.BufferAllocator;
19 import io.netty5.handler.codec.UnsupportedValueConverter;
20 import io.netty5.handler.codec.http.DefaultFullHttpRequest;
21 import io.netty5.handler.codec.http.DefaultFullHttpResponse;
22 import io.netty5.handler.codec.http.DefaultHttpRequest;
23 import io.netty5.handler.codec.http.DefaultHttpResponse;
24 import io.netty5.handler.codec.http.FullHttpMessage;
25 import io.netty5.handler.codec.http.FullHttpRequest;
26 import io.netty5.handler.codec.http.FullHttpResponse;
27 import io.netty5.handler.codec.http.HttpHeaderNames;
28 import io.netty5.handler.codec.http.HttpHeaders;
29 import io.netty5.handler.codec.http.HttpMessage;
30 import io.netty5.handler.codec.http.HttpMethod;
31 import io.netty5.handler.codec.http.HttpRequest;
32 import io.netty5.handler.codec.http.HttpResponse;
33 import io.netty5.handler.codec.http.HttpResponseStatus;
34 import io.netty5.handler.codec.http.HttpUtil;
35 import io.netty5.handler.codec.http.HttpVersion;
36 import io.netty5.util.AsciiString;
37 import io.netty5.util.internal.StringUtil;
38 import io.netty5.util.internal.UnstableApi;
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.netty5.handler.codec.http.HttpHeaderNames.CONNECTION;
46 import static io.netty5.handler.codec.http.HttpHeaderNames.COOKIE;
47 import static io.netty5.handler.codec.http.HttpHeaderNames.TE;
48 import static io.netty5.handler.codec.http.HttpHeaderValues.TRAILERS;
49 import static io.netty5.handler.codec.http.HttpResponseStatus.parseLine;
50 import static io.netty5.handler.codec.http.HttpScheme.HTTP;
51 import static io.netty5.handler.codec.http.HttpScheme.HTTPS;
52 import static io.netty5.handler.codec.http.HttpUtil.isAsteriskForm;
53 import static io.netty5.handler.codec.http.HttpUtil.isOriginForm;
54 import static io.netty5.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
55 import static io.netty5.handler.codec.http2.Http2Exception.connectionError;
56 import static io.netty5.handler.codec.http2.Http2Exception.streamError;
57 import static io.netty5.util.AsciiString.EMPTY_STRING;
58 import static io.netty5.util.AsciiString.contentEqualsIgnoreCase;
59 import static io.netty5.util.AsciiString.indexOf;
60 import static io.netty5.util.AsciiString.trim;
61 import static io.netty5.util.ByteProcessor.FIND_COMMA;
62 import static io.netty5.util.ByteProcessor.FIND_SEMI_COLON;
63 import static io.netty5.util.internal.StringUtil.isNullOrEmpty;
64 import static io.netty5.util.internal.StringUtil.length;
65 import static io.netty5.util.internal.StringUtil.unescapeCsvFields;
66 import static java.util.Objects.requireNonNull;
67
68
69
70
71 @UnstableApi
72 public final class HttpConversionUtil {
73
74
75
76 private static final CharSequenceMap<AsciiString> HTTP_TO_HTTP2_HEADER_BLACKLIST =
77 new CharSequenceMap<>();
78 static {
79 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(CONNECTION, EMPTY_STRING);
80 @SuppressWarnings("deprecation")
81 AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
82 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
83 @SuppressWarnings("deprecation")
84 AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION;
85 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING);
86 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
87 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
88 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
89 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
90 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
91 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
92 }
93
94
95
96
97
98 public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
99
100
101
102
103
104 public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
105
106
107
108
109
110 public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
111
112
113
114
115
116 private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
117
118 private HttpConversionUtil() {
119 }
120
121
122
123
124 public enum ExtensionHeaderNames {
125
126
127
128
129
130
131 STREAM_ID("x-http2-stream-id"),
132
133
134
135
136
137
138 SCHEME("x-http2-scheme"),
139
140
141
142
143
144
145 PATH("x-http2-path"),
146
147
148
149
150
151
152 STREAM_PROMISE_ID("x-http2-stream-promise-id"),
153
154
155
156
157
158
159 STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
160
161
162
163
164
165
166 STREAM_WEIGHT("x-http2-stream-weight");
167
168 private final AsciiString text;
169
170 ExtensionHeaderNames(String text) {
171 this.text = AsciiString.cached(text);
172 }
173
174 public AsciiString text() {
175 return text;
176 }
177 }
178
179
180
181
182
183
184
185
186 public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception {
187 HttpResponseStatus result;
188 try {
189 result = parseLine(status);
190 if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
191 throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
192 }
193 } catch (Http2Exception e) {
194 throw e;
195 } catch (Throwable t) {
196 throw connectionError(PROTOCOL_ERROR, t,
197 "Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
198 }
199 return result;
200 }
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, BufferAllocator alloc,
216 boolean validateHttpHeaders) throws Http2Exception {
217 return toFullHttpResponse(streamId, http2Headers, alloc.allocate(0), validateHttpHeaders);
218 }
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233 public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, Buffer content,
234 boolean validateHttpHeaders)
235 throws Http2Exception {
236 HttpResponseStatus status = parseStatus(http2Headers.status());
237
238
239 FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content,
240 validateHttpHeaders);
241 try {
242 addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
243 } catch (Http2Exception e) {
244 msg.close();
245 throw e;
246 } catch (Throwable t) {
247 msg.close();
248 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
249 }
250 return msg;
251 }
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266 public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, BufferAllocator alloc,
267 boolean validateHttpHeaders) throws Http2Exception {
268 return toFullHttpRequest(streamId, http2Headers, alloc.allocate(0), validateHttpHeaders, alloc);
269 }
270
271 private static CharSequence extractPath(CharSequence method, Http2Headers headers) {
272 if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
273
274 return requireNonNull(headers.authority(),
275 "authority header cannot be null in the conversion to HTTP/1.x");
276 } else {
277 return requireNonNull(headers.path(),
278 "path header cannot be null in conversion to HTTP/1.x");
279 }
280 }
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295 public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, Buffer content,
296 boolean validateHttpHeaders, BufferAllocator allocator)
297 throws Http2Exception {
298
299 final CharSequence method = requireNonNull(http2Headers.method(),
300 "method header cannot be null in conversion to HTTP/1.x");
301 final CharSequence path = extractPath(method, http2Headers);
302 FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
303 .toString()), path.toString(), content, validateHttpHeaders);
304 try {
305 addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
306 } catch (Http2Exception e) {
307 msg.close();
308 throw e;
309 } catch (Throwable t) {
310 msg.close();
311 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
312 }
313 return msg;
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327
328 public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
329 throws Http2Exception {
330
331 final CharSequence method = requireNonNull(http2Headers.method(),
332 "method header cannot be null in conversion to HTTP/1.x");
333 final CharSequence path = extractPath(method, http2Headers);
334 HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
335 path.toString(), validateHttpHeaders);
336 try {
337 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
338 } catch (Http2Exception e) {
339 throw e;
340 } catch (Throwable t) {
341 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
342 }
343 return msg;
344 }
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359 public static HttpResponse toHttpResponse(final int streamId,
360 final Http2Headers http2Headers,
361 final boolean validateHttpHeaders) throws Http2Exception {
362 final HttpResponseStatus status = parseStatus(http2Headers.status());
363
364
365 final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
366 try {
367 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, false);
368 } catch (final Http2Exception e) {
369 throw e;
370 } catch (final Throwable t) {
371 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
372 }
373 return msg;
374 }
375
376
377
378
379
380
381
382
383
384
385
386 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
387 FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
388 addHttp2ToHttpHeaders(streamId, inputHeaders,
389 addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(),
390 destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest);
391 }
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, HttpHeaders outputHeaders,
407 HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
408 Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
409 try {
410 translator.translateHeaders(inputHeaders);
411 } catch (Http2Exception ex) {
412 throw ex;
413 } catch (Throwable t) {
414 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
415 }
416
417 outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
418 outputHeaders.remove(HttpHeaderNames.TRAILER);
419 if (!isTrailer) {
420 outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
421 HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
422 }
423 }
424
425
426
427
428
429
430
431
432
433
434 public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) {
435 HttpHeaders inHeaders = in.headers();
436 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
437 if (in instanceof HttpRequest) {
438 HttpRequest request = (HttpRequest) in;
439 String host = inHeaders.getAsString(HttpHeaderNames.HOST);
440 if (isOriginForm(request.uri()) || isAsteriskForm(request.uri())) {
441 out.path(new AsciiString(request.uri()));
442 setHttp2Scheme(inHeaders, out);
443 } else {
444 URI requestTargetUri = URI.create(request.uri());
445 out.path(toHttp2Path(requestTargetUri));
446
447 host = isNullOrEmpty(host) ? requestTargetUri.getAuthority() : host;
448 setHttp2Scheme(inHeaders, requestTargetUri, out);
449 }
450 setHttp2Authority(host, out);
451 out.method(request.method().asciiName());
452 } else if (in instanceof HttpResponse) {
453 HttpResponse response = (HttpResponse) in;
454 out.status(response.status().codeAsText());
455 }
456
457
458 toHttp2Headers(inHeaders, out);
459 return out;
460 }
461
462 public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
463 if (inHeaders.isEmpty()) {
464 return EmptyHttp2Headers.INSTANCE;
465 }
466
467 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
468 toHttp2Headers(inHeaders, out);
469 return out;
470 }
471
472 private static CharSequenceMap<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
473 int arraySizeHint) {
474 UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.instance();
475 CharSequenceMap<AsciiString> result = new CharSequenceMap<>(true, valueConverter, arraySizeHint);
476
477 while (valuesIter.hasNext()) {
478 AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
479 int index = lowerCased.forEachByte(FIND_COMMA);
480 if (index != -1) {
481 int start = 0;
482 do {
483 result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
484 start = index + 1;
485 } while (start < lowerCased.length() &&
486 (index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1);
487 result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
488 } else {
489 result.add(lowerCased.trim(), EMPTY_STRING);
490 }
491 }
492 return result;
493 }
494
495
496
497
498
499
500
501 private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
502 Http2Headers out) {
503 if (indexOf(entry.getValue(), ',', 0) == -1) {
504 if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
505 out.add(TE, TRAILERS);
506 }
507 } else {
508 List<CharSequence> teValues = unescapeCsvFields(entry.getValue());
509 for (CharSequence teValue : teValues) {
510 if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
511 out.add(TE, TRAILERS);
512 break;
513 }
514 }
515 }
516 }
517
518 public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
519 Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
520
521
522 CharSequenceMap<AsciiString> connectionBlacklist =
523 toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
524 while (iter.hasNext()) {
525 Entry<CharSequence, CharSequence> entry = iter.next();
526 final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
527 if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
528
529 if (aName.contentEqualsIgnoreCase(TE)) {
530 toHttp2HeadersFilterTE(entry, out);
531 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
532 AsciiString value = AsciiString.of(entry.getValue());
533
534
535 int index = value.forEachByte(FIND_SEMI_COLON);
536 if (index != -1) {
537 int start = 0;
538 do {
539 out.add(COOKIE, value.subSequence(start, index, false));
540
541 start = index + 2;
542 } while (start < value.length() &&
543 (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
544 if (start >= value.length()) {
545 throw new IllegalArgumentException("cookie value is of unexpected format: " + value);
546 }
547 out.add(COOKIE, value.subSequence(start, value.length(), false));
548 } else {
549 out.add(COOKIE, value);
550 }
551 } else {
552 out.add(aName, entry.getValue());
553 }
554 }
555 }
556 }
557
558
559
560
561
562 private static AsciiString toHttp2Path(URI uri) {
563 StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) +
564 length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2);
565 if (!isNullOrEmpty(uri.getRawPath())) {
566 pathBuilder.append(uri.getRawPath());
567 }
568 if (!isNullOrEmpty(uri.getRawQuery())) {
569 pathBuilder.append('?');
570 pathBuilder.append(uri.getRawQuery());
571 }
572 if (!isNullOrEmpty(uri.getRawFragment())) {
573 pathBuilder.append('#');
574 pathBuilder.append(uri.getRawFragment());
575 }
576 String path = pathBuilder.toString();
577 return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
578 }
579
580
581 static void setHttp2Authority(String authority, Http2Headers out) {
582
583 if (authority != null) {
584 if (authority.isEmpty()) {
585 out.authority(EMPTY_STRING);
586 } else {
587 int start = authority.indexOf('@') + 1;
588 int length = authority.length() - start;
589 if (length == 0) {
590 throw new IllegalArgumentException("authority: " + authority);
591 }
592 out.authority(new AsciiString(authority, start, length));
593 }
594 }
595 }
596
597 private static void setHttp2Scheme(HttpHeaders in, Http2Headers out) {
598 setHttp2Scheme(in, URI.create(""), out);
599 }
600
601 private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
602 String value = uri.getScheme();
603 if (!isNullOrEmpty(value)) {
604 out.scheme(new AsciiString(value));
605 return;
606 }
607
608
609 CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
610 if (cValue != null) {
611 out.scheme(AsciiString.of(cValue));
612 return;
613 }
614
615 if (uri.getPort() == HTTPS.port()) {
616 out.scheme(HTTPS.name());
617 } else if (uri.getPort() == HTTP.port()) {
618 out.scheme(HTTP.name());
619 } else {
620 throw new IllegalArgumentException(":scheme must be specified. " +
621 "see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
622 }
623 }
624
625
626
627
628 private static final class Http2ToHttpHeaderTranslator {
629
630
631
632 private static final CharSequenceMap<AsciiString>
633 REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<>();
634 private static final CharSequenceMap<AsciiString>
635 RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<>();
636 static {
637 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
638 HttpHeaderNames.HOST);
639 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
640 ExtensionHeaderNames.SCHEME.text());
641 REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
642 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
643 ExtensionHeaderNames.PATH.text());
644 }
645
646 private final int streamId;
647 private final HttpHeaders output;
648 private final CharSequenceMap<AsciiString> translations;
649
650
651
652
653
654
655
656
657 Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
658 this.streamId = streamId;
659 this.output = output;
660 translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
661 }
662
663 void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
664
665 StringBuilder cookies = null;
666
667 for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
668 final CharSequence name = entry.getKey();
669 final CharSequence value = entry.getValue();
670 AsciiString translatedName = translations.get(name);
671 if (translatedName != null) {
672 output.add(translatedName, AsciiString.of(value));
673 } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
674
675
676 if (name.length() == 0 || name.charAt(0) == ':') {
677 throw streamError(streamId, PROTOCOL_ERROR,
678 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
679 }
680 if (COOKIE.equals(name)) {
681
682
683 if (cookies == null) {
684 cookies = StringUtil.threadLocalStringBuilder();
685 } else if (cookies.length() > 0) {
686 cookies.append("; ");
687 }
688 cookies.append(value);
689 } else {
690 output.add(name, value);
691 }
692 }
693 }
694 if (cookies != null) {
695 output.add(COOKIE, cookies.toString());
696 }
697 }
698 }
699 }