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