1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package io.netty.handler.codec.http2;
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.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.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.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
55 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
56 import static io.netty.handler.codec.http2.Http2Exception.streamError;
57 import static io.netty.util.AsciiString.EMPTY_STRING;
58 import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
59 import static io.netty.util.AsciiString.indexOf;
60 import static io.netty.util.AsciiString.trim;
61 import static io.netty.util.ByteProcessor.FIND_COMMA;
62 import static io.netty.util.ByteProcessor.FIND_SEMI_COLON;
63 import static io.netty.util.internal.ObjectUtil.checkNotNull;
64 import static io.netty.util.internal.StringUtil.isNullOrEmpty;
65 import static io.netty.util.internal.StringUtil.length;
66 import static io.netty.util.internal.StringUtil.unescapeCsvFields;
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<AsciiString>();
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, ByteBufAllocator alloc,
216 boolean validateHttpHeaders) throws Http2Exception {
217 return toFullHttpResponse(streamId, http2Headers, alloc.buffer(), 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, ByteBuf 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.release();
245 throw e;
246 } catch (Throwable t) {
247 msg.release();
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, ByteBufAllocator alloc,
267 boolean validateHttpHeaders) throws Http2Exception {
268 return toFullHttpRequest(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
269 }
270
271 private static String extractPath(CharSequence method, Http2Headers headers) {
272 if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
273
274 return checkNotNull(headers.authority(),
275 "authority header cannot be null in the conversion to HTTP/1.x").toString();
276 } else {
277 return checkNotNull(headers.path(),
278 "path header cannot be null in conversion to HTTP/1.x").toString();
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, ByteBuf content,
296 boolean validateHttpHeaders) throws Http2Exception {
297
298 final CharSequence method = checkNotNull(http2Headers.method(),
299 "method header cannot be null in conversion to HTTP/1.x");
300 final CharSequence path = extractPath(method, http2Headers);
301 FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
302 .toString()), path.toString(), content, validateHttpHeaders);
303 try {
304 addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
305 } catch (Http2Exception e) {
306 msg.release();
307 throw e;
308 } catch (Throwable t) {
309 msg.release();
310 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
311 }
312 return msg;
313 }
314
315
316
317
318
319
320
321
322
323
324
325
326
327 public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
328 throws Http2Exception {
329
330 final CharSequence method = checkNotNull(http2Headers.method(),
331 "method header cannot be null in conversion to HTTP/1.x");
332 final CharSequence path = extractPath(method, http2Headers);
333 HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
334 path.toString(), validateHttpHeaders);
335 try {
336 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
337 } catch (Http2Exception e) {
338 throw e;
339 } catch (Throwable t) {
340 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
341 }
342 return msg;
343 }
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358 public static HttpResponse toHttpResponse(final int streamId,
359 final Http2Headers http2Headers,
360 final boolean validateHttpHeaders) throws Http2Exception {
361 final HttpResponseStatus status = parseStatus(http2Headers.status());
362
363
364 final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
365 try {
366 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, false);
367 } catch (final Http2Exception e) {
368 throw e;
369 } catch (final Throwable t) {
370 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
371 }
372 return msg;
373 }
374
375
376
377
378
379
380
381
382
383
384
385 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
386 FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
387 addHttp2ToHttpHeaders(streamId, inputHeaders,
388 addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(),
389 destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest);
390 }
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, HttpHeaders outputHeaders,
406 HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
407 Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
408 try {
409 translator.translateHeaders(inputHeaders);
410 } catch (Http2Exception ex) {
411 throw ex;
412 } catch (Throwable t) {
413 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
414 }
415
416 outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
417 outputHeaders.remove(HttpHeaderNames.TRAILER);
418 if (!isTrailer) {
419 outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
420 HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
421 }
422 }
423
424
425
426
427
428
429
430
431
432
433 public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) {
434 HttpHeaders inHeaders = in.headers();
435 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
436 if (in instanceof HttpRequest) {
437 HttpRequest request = (HttpRequest) in;
438 String host = inHeaders.getAsString(HttpHeaderNames.HOST);
439 if (isOriginForm(request.uri()) || isAsteriskForm(request.uri())) {
440 out.path(new AsciiString(request.uri()));
441 setHttp2Scheme(inHeaders, out);
442 } else {
443 URI requestTargetUri = URI.create(request.uri());
444 out.path(toHttp2Path(requestTargetUri));
445
446 host = isNullOrEmpty(host) ? requestTargetUri.getAuthority() : host;
447 setHttp2Scheme(inHeaders, requestTargetUri, out);
448 }
449 setHttp2Authority(host, out);
450 out.method(request.method().asciiName());
451 } else if (in instanceof HttpResponse) {
452 HttpResponse response = (HttpResponse) in;
453 out.status(response.status().codeAsText());
454 }
455
456
457 toHttp2Headers(inHeaders, out);
458 return out;
459 }
460
461 public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
462 if (inHeaders.isEmpty()) {
463 return EmptyHttp2Headers.INSTANCE;
464 }
465
466 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
467 toHttp2Headers(inHeaders, out);
468 return out;
469 }
470
471 private static CharSequenceMap<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
472 int arraySizeHint) {
473 UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.<AsciiString>instance();
474 CharSequenceMap<AsciiString> result = new CharSequenceMap<AsciiString>(true, valueConverter, arraySizeHint);
475
476 while (valuesIter.hasNext()) {
477 AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
478 try {
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 } catch (Exception e) {
492
493
494 throw new IllegalStateException(e);
495 }
496 }
497 return result;
498 }
499
500
501
502
503
504
505
506 private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
507 Http2Headers out) {
508 if (indexOf(entry.getValue(), ',', 0) == -1) {
509 if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
510 out.add(TE, TRAILERS);
511 }
512 } else {
513 List<CharSequence> teValues = unescapeCsvFields(entry.getValue());
514 for (CharSequence teValue : teValues) {
515 if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
516 out.add(TE, TRAILERS);
517 break;
518 }
519 }
520 }
521 }
522
523 public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
524 Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
525
526
527 CharSequenceMap<AsciiString> connectionBlacklist =
528 toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
529 while (iter.hasNext()) {
530 Entry<CharSequence, CharSequence> entry = iter.next();
531 final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
532 if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
533
534 if (aName.contentEqualsIgnoreCase(TE)) {
535 toHttp2HeadersFilterTE(entry, out);
536 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
537 CharSequence valueCs = entry.getValue();
538
539 boolean invalid = false;
540 for (int i = 0; i < valueCs.length(); i++) {
541 char c = valueCs.charAt(i);
542 if (c == ';') {
543 if (i + 1 >= valueCs.length() || valueCs.charAt(i + 1) != ' ') {
544
545 invalid = true;
546 break;
547 }
548 i++;
549 } else if (c > 255) {
550
551 invalid = true;
552 break;
553 }
554 }
555
556 if (invalid) {
557 out.add(COOKIE, valueCs);
558 } else {
559 splitValidCookieHeader(out, valueCs);
560 }
561 } else {
562 out.add(aName, entry.getValue());
563 }
564 }
565 }
566 }
567
568 private static void splitValidCookieHeader(Http2Headers out, CharSequence valueCs) {
569 try {
570 AsciiString value = AsciiString.of(valueCs);
571
572
573 int index = value.forEachByte(FIND_SEMI_COLON);
574 if (index != -1) {
575 int start = 0;
576 do {
577 out.add(COOKIE, value.subSequence(start, index, false));
578 assert index + 1 < value.length();
579 assert value.charAt(index + 1) == ' ';
580
581 start = index + 2;
582 } while (start < value.length() &&
583 (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
584 assert start < value.length();
585 out.add(COOKIE, value.subSequence(start, value.length(), false));
586 } else {
587 out.add(COOKIE, value);
588 }
589 } catch (Exception e) {
590
591
592 throw new IllegalStateException(e);
593 }
594 }
595
596
597
598
599
600 private static AsciiString toHttp2Path(URI uri) {
601 StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) +
602 length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2);
603 if (!isNullOrEmpty(uri.getRawPath())) {
604 pathBuilder.append(uri.getRawPath());
605 }
606 if (!isNullOrEmpty(uri.getRawQuery())) {
607 pathBuilder.append('?');
608 pathBuilder.append(uri.getRawQuery());
609 }
610 if (!isNullOrEmpty(uri.getRawFragment())) {
611 pathBuilder.append('#');
612 pathBuilder.append(uri.getRawFragment());
613 }
614 String path = pathBuilder.toString();
615 return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
616 }
617
618
619 static void setHttp2Authority(String authority, Http2Headers out) {
620
621 if (authority != null) {
622 if (authority.isEmpty()) {
623 out.authority(EMPTY_STRING);
624 } else {
625 int start = authority.indexOf('@') + 1;
626 int length = authority.length() - start;
627 if (length == 0) {
628 throw new IllegalArgumentException("authority: " + authority);
629 }
630 out.authority(new AsciiString(authority, start, length));
631 }
632 }
633 }
634
635 private static void setHttp2Scheme(HttpHeaders in, Http2Headers out) {
636 setHttp2Scheme(in, URI.create(""), out);
637 }
638
639 private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
640 String value = uri.getScheme();
641 if (!isNullOrEmpty(value)) {
642 out.scheme(new AsciiString(value));
643 return;
644 }
645
646
647 CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
648 if (cValue != null) {
649 out.scheme(AsciiString.of(cValue));
650 return;
651 }
652
653 if (uri.getPort() == HTTPS.port()) {
654 out.scheme(HTTPS.name());
655 } else if (uri.getPort() == HTTP.port()) {
656 out.scheme(HTTP.name());
657 } else {
658 throw new IllegalArgumentException(":scheme must be specified. " +
659 "see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
660 }
661 }
662
663
664
665
666 private static final class Http2ToHttpHeaderTranslator {
667
668
669
670 private static final CharSequenceMap<AsciiString>
671 REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
672 private static final CharSequenceMap<AsciiString>
673 RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
674 static {
675 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
676 HttpHeaderNames.HOST);
677 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
678 ExtensionHeaderNames.SCHEME.text());
679 REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
680 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
681 ExtensionHeaderNames.PATH.text());
682 }
683
684 private final int streamId;
685 private final HttpHeaders output;
686 private final CharSequenceMap<AsciiString> translations;
687
688
689
690
691
692
693
694
695 Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
696 this.streamId = streamId;
697 this.output = output;
698 translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
699 }
700
701 void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
702
703 StringBuilder cookies = null;
704
705 for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
706 final CharSequence name = entry.getKey();
707 final CharSequence value = entry.getValue();
708 AsciiString translatedName = translations.get(name);
709 if (translatedName != null) {
710 output.add(translatedName, AsciiString.of(value));
711 } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
712
713
714 if (name.length() == 0 || name.charAt(0) == ':') {
715 throw streamError(streamId, PROTOCOL_ERROR,
716 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
717 }
718 if (COOKIE.equals(name)) {
719
720
721 if (cookies == null) {
722 cookies = InternalThreadLocalMap.get().stringBuilder();
723 } else if (cookies.length() > 0) {
724 cookies.append("; ");
725 }
726 cookies.append(value);
727 } else {
728 output.add(name, value);
729 }
730 }
731 }
732 if (cookies != null) {
733 output.add(COOKIE, cookies.toString());
734 }
735 }
736 }
737 }