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 URI requestTargetUri = URI.create(request.uri());
439 out.path(toHttp2Path(requestTargetUri));
440 out.method(request.method().asciiName());
441 setHttp2Scheme(inHeaders, requestTargetUri, out);
442
443 if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
444
445 String host = inHeaders.getAsString(HttpHeaderNames.HOST);
446 setHttp2Authority(host == null || host.isEmpty() ? requestTargetUri.getAuthority() : host, out);
447 }
448 } else if (in instanceof HttpResponse) {
449 HttpResponse response = (HttpResponse) in;
450 out.status(response.status().codeAsText());
451 }
452
453
454 toHttp2Headers(inHeaders, out);
455 return out;
456 }
457
458 public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
459 if (inHeaders.isEmpty()) {
460 return EmptyHttp2Headers.INSTANCE;
461 }
462
463 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
464 toHttp2Headers(inHeaders, out);
465 return out;
466 }
467
468 private static CharSequenceMap<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
469 int arraySizeHint) {
470 UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.<AsciiString>instance();
471 CharSequenceMap<AsciiString> result = new CharSequenceMap<AsciiString>(true, valueConverter, arraySizeHint);
472
473 while (valuesIter.hasNext()) {
474 AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
475 try {
476 int index = lowerCased.forEachByte(FIND_COMMA);
477 if (index != -1) {
478 int start = 0;
479 do {
480 result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
481 start = index + 1;
482 } while (start < lowerCased.length() &&
483 (index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1);
484 result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
485 } else {
486 result.add(lowerCased.trim(), EMPTY_STRING);
487 }
488 } catch (Exception e) {
489
490
491 throw new IllegalStateException(e);
492 }
493 }
494 return result;
495 }
496
497
498
499
500
501
502
503 private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
504 Http2Headers out) {
505 if (indexOf(entry.getValue(), ',', 0) == -1) {
506 if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
507 out.add(TE, TRAILERS);
508 }
509 } else {
510 List<CharSequence> teValues = unescapeCsvFields(entry.getValue());
511 for (CharSequence teValue : teValues) {
512 if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
513 out.add(TE, TRAILERS);
514 break;
515 }
516 }
517 }
518 }
519
520 public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
521 Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
522
523
524 CharSequenceMap<AsciiString> connectionBlacklist =
525 toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
526 while (iter.hasNext()) {
527 Entry<CharSequence, CharSequence> entry = iter.next();
528 final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
529 if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
530
531 if (aName.contentEqualsIgnoreCase(TE)) {
532 toHttp2HeadersFilterTE(entry, out);
533 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
534 AsciiString value = AsciiString.of(entry.getValue());
535
536
537 try {
538 int index = value.forEachByte(FIND_SEMI_COLON);
539 if (index != -1) {
540 int start = 0;
541 do {
542 out.add(COOKIE, value.subSequence(start, index, false));
543
544 start = index + 2;
545 } while (start < value.length() &&
546 (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
547 if (start >= value.length()) {
548 throw new IllegalArgumentException("cookie value is of unexpected format: " + value);
549 }
550 out.add(COOKIE, value.subSequence(start, value.length(), false));
551 } else {
552 out.add(COOKIE, value);
553 }
554 } catch (Exception e) {
555
556
557 throw new IllegalStateException(e);
558 }
559 } else {
560 out.add(aName, entry.getValue());
561 }
562 }
563 }
564 }
565
566
567
568
569
570 private static AsciiString toHttp2Path(URI uri) {
571 StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) +
572 length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2);
573 if (!isNullOrEmpty(uri.getRawPath())) {
574 pathBuilder.append(uri.getRawPath());
575 }
576 if (!isNullOrEmpty(uri.getRawQuery())) {
577 pathBuilder.append('?');
578 pathBuilder.append(uri.getRawQuery());
579 }
580 if (!isNullOrEmpty(uri.getRawFragment())) {
581 pathBuilder.append('#');
582 pathBuilder.append(uri.getRawFragment());
583 }
584 String path = pathBuilder.toString();
585 return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
586 }
587
588
589 static void setHttp2Authority(String authority, Http2Headers out) {
590
591 if (authority != null) {
592 if (authority.isEmpty()) {
593 out.authority(EMPTY_STRING);
594 } else {
595 int start = authority.indexOf('@') + 1;
596 int length = authority.length() - start;
597 if (length == 0) {
598 throw new IllegalArgumentException("authority: " + authority);
599 }
600 out.authority(new AsciiString(authority, start, length));
601 }
602 }
603 }
604
605 private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
606 String value = uri.getScheme();
607 if (value != null) {
608 out.scheme(new AsciiString(value));
609 return;
610 }
611
612
613 CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
614 if (cValue != null) {
615 out.scheme(AsciiString.of(cValue));
616 return;
617 }
618
619 if (uri.getPort() == HTTPS.port()) {
620 out.scheme(HTTPS.name());
621 } else if (uri.getPort() == HTTP.port()) {
622 out.scheme(HTTP.name());
623 } else {
624 throw new IllegalArgumentException(":scheme must be specified. " +
625 "see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
626 }
627 }
628
629
630
631
632 private static final class Http2ToHttpHeaderTranslator {
633
634
635
636 private static final CharSequenceMap<AsciiString>
637 REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
638 private static final CharSequenceMap<AsciiString>
639 RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
640 static {
641 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
642 HttpHeaderNames.HOST);
643 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
644 ExtensionHeaderNames.SCHEME.text());
645 REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
646 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
647 ExtensionHeaderNames.PATH.text());
648 }
649
650 private final int streamId;
651 private final HttpHeaders output;
652 private final CharSequenceMap<AsciiString> translations;
653
654
655
656
657
658
659
660
661 Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
662 this.streamId = streamId;
663 this.output = output;
664 translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
665 }
666
667 void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
668
669 StringBuilder cookies = null;
670
671 for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
672 final CharSequence name = entry.getKey();
673 final CharSequence value = entry.getValue();
674 AsciiString translatedName = translations.get(name);
675 if (translatedName != null) {
676 output.add(translatedName, AsciiString.of(value));
677 } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
678
679
680 if (name.length() == 0 || name.charAt(0) == ':') {
681 throw streamError(streamId, PROTOCOL_ERROR,
682 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
683 }
684 if (COOKIE.equals(name)) {
685
686
687 if (cookies == null) {
688 cookies = InternalThreadLocalMap.get().stringBuilder();
689 } else if (cookies.length() > 0) {
690 cookies.append("; ");
691 }
692 cookies.append(value);
693 } else {
694 output.add(name, value);
695 }
696 }
697 }
698 if (cookies != null) {
699 output.add(COOKIE, cookies.toString());
700 }
701 }
702 }
703 }