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.StringUtil;
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.unescapeCsvFields;
66
67
68
69
70 public final class HttpConversionUtil {
71
72
73
74
75
76
77 private static final CharSequenceMap<AsciiString> HTTP_TO_HTTP2_HEADER_BLACKLIST =
78 new CharSequenceMap<AsciiString>();
79 static {
80 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(CONNECTION, EMPTY_STRING);
81 @SuppressWarnings("deprecation")
82 AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
83 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
84 @SuppressWarnings("deprecation")
85 AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION;
86 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING);
87 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
88 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
89 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
90 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
91 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
92 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
93 HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PROTOCOL.text(), EMPTY_STRING);
94 }
95
96 private static final CharSequenceMap<AsciiString> HTTP2_TO_HTTP_HEADER_BLACKLIST =
97 new CharSequenceMap<>(false);
98 static {
99 for (ExtensionHeaderNames name : ExtensionHeaderNames.values()) {
100 HTTP2_TO_HTTP_HEADER_BLACKLIST.add(name.text(), EMPTY_STRING);
101 }
102 }
103
104
105
106
107
108 public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
109
110
111
112
113
114 public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
115
116
117
118
119
120 public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
121
122
123
124
125
126 private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
127
128 private HttpConversionUtil() {
129 }
130
131
132
133
134 public enum ExtensionHeaderNames {
135
136
137
138
139
140
141 STREAM_ID("x-http2-stream-id"),
142
143
144
145
146
147
148 SCHEME("x-http2-scheme"),
149
150
151
152
153
154
155 PATH("x-http2-path"),
156
157
158
159
160
161
162 STREAM_PROMISE_ID("x-http2-stream-promise-id"),
163
164
165
166
167
168
169 STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
170
171
172
173
174
175
176 STREAM_WEIGHT("x-http2-stream-weight"),
177
178
179
180
181
182
183
184 PROTOCOL("x-http2-protocol");
185
186 private final AsciiString text;
187
188 ExtensionHeaderNames(String text) {
189 this.text = AsciiString.cached(text);
190 }
191
192 public AsciiString text() {
193 return text;
194 }
195 }
196
197
198
199
200
201
202
203
204 public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception {
205 HttpResponseStatus result;
206 try {
207 result = parseLine(status);
208 if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
209 throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
210 }
211 } catch (Http2Exception e) {
212 throw e;
213 } catch (Throwable t) {
214 throw connectionError(PROTOCOL_ERROR, t,
215 "Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
216 }
217 return result;
218 }
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233 public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
234 boolean validateHttpHeaders) throws Http2Exception {
235 return toFullHttpResponse(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
236 }
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBuf content,
252 boolean validateHttpHeaders)
253 throws Http2Exception {
254 HttpResponseStatus status = parseStatus(http2Headers.status());
255
256
257 FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content,
258 validateHttpHeaders);
259 try {
260 addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
261 } catch (Http2Exception e) {
262 msg.release();
263 throw e;
264 } catch (Throwable t) {
265 msg.release();
266 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
267 }
268 return msg;
269 }
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284 public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
285 boolean validateHttpHeaders) throws Http2Exception {
286 return toFullHttpRequest(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
287 }
288
289 private static String extractPath(CharSequence method, Http2Headers headers) {
290 if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
291
292 return checkNotNull(headers.authority(),
293 "authority header cannot be null in the conversion to HTTP/1.x").toString();
294 } else {
295 return checkNotNull(headers.path(),
296 "path header cannot be null in conversion to HTTP/1.x").toString();
297 }
298 }
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313 public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBuf content,
314 boolean validateHttpHeaders) throws Http2Exception {
315
316 final CharSequence method = checkNotNull(http2Headers.method(),
317 "method header cannot be null in conversion to HTTP/1.x");
318 final CharSequence path = extractPath(method, http2Headers);
319 FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
320 .toString()), path.toString(), content, validateHttpHeaders);
321 try {
322 addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
323 } catch (Http2Exception e) {
324 msg.release();
325 throw e;
326 } catch (Throwable t) {
327 msg.release();
328 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
329 }
330 return msg;
331 }
332
333
334
335
336
337
338
339
340
341
342
343
344
345 public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
346 throws Http2Exception {
347
348 final CharSequence method = checkNotNull(http2Headers.method(),
349 "method header cannot be null in conversion to HTTP/1.x");
350 final CharSequence path = extractPath(method, http2Headers);
351 HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
352 path.toString(), validateHttpHeaders);
353 try {
354 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
355 } catch (Http2Exception e) {
356 throw e;
357 } catch (Throwable t) {
358 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
359 }
360 return msg;
361 }
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376 public static HttpResponse toHttpResponse(final int streamId,
377 final Http2Headers http2Headers,
378 final boolean validateHttpHeaders) throws Http2Exception {
379 final HttpResponseStatus status = parseStatus(http2Headers.status());
380
381
382 final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
383 try {
384 addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, false);
385 } catch (final Http2Exception e) {
386 throw e;
387 } catch (final Throwable t) {
388 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
389 }
390 return msg;
391 }
392
393
394
395
396
397
398
399
400
401
402
403 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
404 FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
405 addHttp2ToHttpHeaders(streamId, inputHeaders,
406 addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(),
407 destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest);
408 }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423 public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, HttpHeaders outputHeaders,
424 HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
425
426
427
428
429 boolean isConnect = isRequest && HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(inputHeaders.method());
430 Http2ToHttpHeaderTranslator translator =
431 new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest, isConnect);
432 try {
433 translator.translateHeaders(inputHeaders);
434 } catch (Http2Exception ex) {
435 throw ex;
436 } catch (Throwable t) {
437 throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
438 }
439
440 outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
441 outputHeaders.remove(HttpHeaderNames.TRAILER);
442 if (!isTrailer) {
443 outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
444 HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
445 }
446 }
447
448
449
450
451
452
453
454
455
456
457 public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) {
458 HttpHeaders inHeaders = in.headers();
459 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
460 if (in instanceof HttpRequest) {
461 HttpRequest request = (HttpRequest) in;
462 if (request.method().equals(HttpMethod.CONNECT)) {
463
464
465
466
467
468 String authorityForm = request.uri();
469 if (authorityForm != null) {
470
471
472
473 if (authorityForm.isEmpty() || authorityForm.indexOf('@') >= 0 || authorityForm.indexOf('/') >= 0) {
474 throw new IllegalArgumentException("Invalid CONNECT request target: " + authorityForm);
475 }
476 out.authority(new AsciiString(authorityForm));
477 }
478 } else {
479 String host = inHeaders.getAsString(HttpHeaderNames.HOST);
480 if (isOriginForm(request.uri()) || isAsteriskForm(request.uri())) {
481 out.path(new AsciiString(request.uri()));
482 setHttp2Scheme(inHeaders, out);
483 } else {
484 String requestTarget = request.uri();
485 out.path(toHttp2Path(requestTarget));
486 if (hasSchemeAndAuthority(requestTarget)) {
487 URI requestTargetUri = URI.create(http2PathlessRequestTarget(requestTarget));
488
489
490 String requestTargetAuthority = requestTargetUri.getAuthority();
491 host = isNullOrEmpty(requestTargetAuthority) ? host : requestTargetAuthority;
492 setHttp2Scheme(inHeaders, requestTargetUri, out);
493 } else {
494 int schemeEnd = schemeEnd(requestTarget);
495 if (schemeEnd != -1) {
496 setHttp2Scheme(inHeaders, requestTarget.substring(0, schemeEnd), -1, out);
497 } else {
498 setHttp2Scheme(inHeaders, out);
499 }
500 }
501 }
502 setHttp2Authority(host, out);
503 }
504 out.method(request.method().asciiName());
505 } else if (in instanceof HttpResponse) {
506 HttpResponse response = (HttpResponse) in;
507 out.status(response.status().codeAsText());
508 }
509
510
511 toHttp2Headers(inHeaders, out);
512 return out;
513 }
514
515 public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
516 if (inHeaders.isEmpty()) {
517 return EmptyHttp2Headers.INSTANCE;
518 }
519
520 final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
521 toHttp2Headers(inHeaders, out);
522 return out;
523 }
524
525 private static CharSequenceMap<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
526 int arraySizeHint) {
527 UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.<AsciiString>instance();
528 CharSequenceMap<AsciiString> result = new CharSequenceMap<AsciiString>(true, valueConverter, arraySizeHint);
529
530 while (valuesIter.hasNext()) {
531 AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
532 try {
533 int index = lowerCased.forEachByte(FIND_COMMA);
534 if (index != -1) {
535 int start = 0;
536 do {
537 result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
538 start = index + 1;
539 } while (start < lowerCased.length() &&
540 (index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1);
541 result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
542 } else {
543 result.add(lowerCased.trim(), EMPTY_STRING);
544 }
545 } catch (Exception e) {
546
547
548 throw new IllegalStateException(e);
549 }
550 }
551 return result;
552 }
553
554
555
556
557
558
559
560 private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
561 Http2Headers out) {
562 if (indexOf(entry.getValue(), ',', 0) == -1) {
563 if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
564 out.add(TE, TRAILERS);
565 }
566 } else {
567 List<CharSequence> teValues = unescapeCsvFields(entry.getValue());
568 for (CharSequence teValue : teValues) {
569 if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
570 out.add(TE, TRAILERS);
571 break;
572 }
573 }
574 }
575 }
576
577 public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
578 Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
579
580
581 CharSequenceMap<AsciiString> connectionBlacklist =
582 toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
583 while (iter.hasNext()) {
584 Entry<CharSequence, CharSequence> entry = iter.next();
585 final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
586 if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
587
588 if (aName.contentEqualsIgnoreCase(TE)) {
589 toHttp2HeadersFilterTE(entry, out);
590 } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
591 CharSequence valueCs = entry.getValue();
592
593 boolean invalid = false;
594 for (int i = 0; i < valueCs.length(); i++) {
595 char c = valueCs.charAt(i);
596 if (c == ';') {
597 if (i + 1 >= valueCs.length() || valueCs.charAt(i + 1) != ' ') {
598
599 invalid = true;
600 break;
601 }
602 i++;
603 } else if (c > 255) {
604
605 invalid = true;
606 break;
607 }
608 }
609
610 if (invalid) {
611 out.add(COOKIE, valueCs);
612 } else {
613 splitValidCookieHeader(out, valueCs);
614 }
615 } else {
616 out.add(aName, entry.getValue());
617 }
618 }
619 }
620 }
621
622 private static void splitValidCookieHeader(Http2Headers out, CharSequence valueCs) {
623 try {
624 AsciiString value = AsciiString.of(valueCs);
625
626
627 int index = value.forEachByte(FIND_SEMI_COLON);
628 if (index != -1) {
629 int start = 0;
630 do {
631 out.add(COOKIE, value.subSequence(start, index, false));
632 assert index + 1 < value.length();
633 assert value.charAt(index + 1) == ' ';
634
635 start = index + 2;
636 } while (start < value.length() &&
637 (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
638 assert start < value.length();
639 out.add(COOKIE, value.subSequence(start, value.length(), false));
640 } else {
641 out.add(COOKIE, value);
642 }
643 } catch (Exception e) {
644
645
646 throw new IllegalStateException(e);
647 }
648 }
649
650
651
652
653
654 private static AsciiString toHttp2Path(String uri) {
655 String path = dropEmptyFragment(parsePath(uri));
656 String query = parseQuery(uri);
657 if (isNullOrEmpty(query)) {
658 return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
659 }
660 StringBuilder pathBuilder = new StringBuilder(path.length() + query.length() + 1);
661 pathBuilder.append(path);
662 appendQuery(pathBuilder, query);
663 return new AsciiString(pathBuilder.toString());
664 }
665
666
667
668
669 private static String parsePath(String uri) {
670 if (uri.isEmpty()) {
671 return StringUtil.EMPTY_STRING;
672 }
673 int i;
674 if (uri.charAt(0) == '/') {
675 i = 0;
676 } else {
677 i = uri.indexOf("://");
678
679 if (!isValidScheme(uri, i)) {
680 i = 0;
681 } else {
682 int authorityStart = i + 3;
683
684 int queryOrFragmentStart = queryOrFragmentStart(uri, authorityStart);
685 i = uri.indexOf('/', authorityStart);
686 if (i == -1 || (queryOrFragmentStart != -1 && queryOrFragmentStart < i)) {
687
688 return "/";
689 }
690 }
691 }
692
693 int queryStart = uri.indexOf('?', i);
694 if (queryStart == -1) {
695 queryStart = uri.length();
696 if (i == 0) {
697 return uri;
698 }
699 }
700 return uri.substring(i, queryStart);
701 }
702
703
704
705
706 private static String parseQuery(String uri) {
707 int i = uri.indexOf('?');
708 if (i == -1) {
709 return null;
710 } else {
711 return uri.substring(i + 1);
712 }
713 }
714
715 private static String dropEmptyFragment(String path) {
716
717 return path.endsWith("#") ? path.substring(0, path.length() - 1) : path;
718 }
719
720 private static void appendQuery(StringBuilder pathBuilder, String query) {
721 int fragmentStart = query.indexOf('#');
722 if (fragmentStart == 0) {
723
724 pathBuilder.append(query);
725 } else if (fragmentStart == query.length() - 1) {
726
727 pathBuilder.append('?').append(query, 0, fragmentStart);
728 } else {
729 pathBuilder.append('?').append(query);
730 }
731 }
732
733 static int queryOrFragmentStart(String uri, int searchStart) {
734 int queryStart = uri.indexOf('?', searchStart);
735 int fragmentStart = uri.indexOf('#', searchStart);
736 return queryStart == -1 ? fragmentStart :
737 fragmentStart == -1 ? queryStart : Math.min(queryStart, fragmentStart);
738 }
739
740
741 static boolean hasSchemeAndAuthority(String requestTarget) {
742 int schemeEnd = requestTarget.indexOf("://");
743 return isValidScheme(requestTarget, schemeEnd);
744 }
745
746 private static int schemeEnd(String requestTarget) {
747 int schemeEnd = requestTarget.indexOf(':');
748 return isValidScheme(requestTarget, schemeEnd) ? schemeEnd : -1;
749 }
750
751
752 private static String http2PathlessRequestTarget(String requestTarget) {
753 int schemeEnd = requestTarget.indexOf("://");
754 int authorityStart = schemeEnd + 3;
755
756 int pathStart = requestTarget.indexOf('/', authorityStart);
757 int delimiter = queryOrFragmentStart(requestTarget, authorityStart);
758 if (pathStart != -1 && (delimiter == -1 || pathStart < delimiter)) {
759 delimiter = pathStart;
760 }
761 if (delimiter == -1) {
762 return requestTarget;
763 }
764 return delimiter == authorityStart ? requestTarget.substring(0, delimiter + 1) :
765 requestTarget.substring(0, delimiter);
766 }
767
768
769 static boolean isValidScheme(String uri, int schemeEnd) {
770 if (schemeEnd <= 0) {
771 return false;
772 }
773 char first = uri.charAt(0);
774 if (!isAlpha(first)) {
775 return false;
776 }
777 for (int i = 1; i < schemeEnd; ++i) {
778 char c = uri.charAt(i);
779 if (!isAlpha(c) && (c < '0' || c > '9') && c != '+' && c != '-' && c != '.') {
780 return false;
781 }
782 }
783 return true;
784 }
785
786 private static boolean isAlpha(char c) {
787 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
788 }
789
790
791 static void setHttp2Authority(String authority, Http2Headers out) {
792
793 if (authority != null) {
794 if (authority.isEmpty()) {
795 out.authority(EMPTY_STRING);
796 } else {
797 int start = authority.indexOf('@') + 1;
798 int length = authority.length() - start;
799 if (length == 0) {
800 throw new IllegalArgumentException("authority: " + authority);
801 }
802 out.authority(new AsciiString(authority, start, length));
803 }
804 }
805 }
806
807 private static void setHttp2Scheme(HttpHeaders in, Http2Headers out) {
808 setHttp2Scheme(in, URI.create(""), out);
809 }
810
811 private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
812 setHttp2Scheme(in, uri.getScheme(), uri.getPort(), out);
813 }
814
815 private static void setHttp2Scheme(HttpHeaders in, String scheme, int port, Http2Headers out) {
816 if (!isNullOrEmpty(scheme)) {
817 out.scheme(new AsciiString(scheme));
818 return;
819 }
820
821
822 CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
823 if (cValue != null) {
824 out.scheme(AsciiString.of(cValue));
825 return;
826 }
827
828 if (port == HTTPS.port()) {
829 out.scheme(HTTPS.name());
830 } else if (port == HTTP.port()) {
831 out.scheme(HTTP.name());
832 } else {
833 throw new IllegalArgumentException(":scheme must be specified. " +
834 "see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
835 }
836 }
837
838
839
840
841 private static final class Http2ToHttpHeaderTranslator {
842
843
844
845 private static final CharSequenceMap<AsciiString>
846 REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
847 private static final CharSequenceMap<AsciiString>
848 RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
849
850
851
852
853
854
855 private static final CharSequenceMap<AsciiString>
856 CONNECT_REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
857 static {
858 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
859 HttpHeaderNames.HOST);
860 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
861 ExtensionHeaderNames.SCHEME.text());
862 REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
863 RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
864 ExtensionHeaderNames.PATH.text());
865 CONNECT_REQUEST_HEADER_TRANSLATIONS.add(REQUEST_HEADER_TRANSLATIONS);
866 CONNECT_REQUEST_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
867 ExtensionHeaderNames.PATH.text());
868 CONNECT_REQUEST_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PROTOCOL.value(),
869 ExtensionHeaderNames.PROTOCOL.text());
870 }
871
872 private final int streamId;
873 private final HttpHeaders output;
874 private final CharSequenceMap<AsciiString> translations;
875
876
877
878
879
880
881
882
883
884
885
886 Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request, boolean connect) {
887 this.streamId = streamId;
888 this.output = output;
889 if (request) {
890 translations = connect ? CONNECT_REQUEST_HEADER_TRANSLATIONS : REQUEST_HEADER_TRANSLATIONS;
891 } else {
892 translations = RESPONSE_HEADER_TRANSLATIONS;
893 }
894 }
895
896 void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
897
898 StringBuilder cookies = null;
899 boolean hostHeaderFound = false;
900
901 for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
902 final CharSequence name = entry.getKey();
903 final CharSequence value = entry.getValue();
904 AsciiString translatedName = translations.get(name);
905 if (translatedName != null) {
906 if (translatedName.contentEqualsIgnoreCase(HttpHeaderNames.HOST)) {
907 hostHeaderFound = true;
908 }
909 output.add(translatedName, AsciiString.of(value));
910 } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
911
912
913 if (name.length() == 0 || name.charAt(0) == ':') {
914 throw streamError(streamId, PROTOCOL_ERROR,
915 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
916 }
917 if (HTTP2_TO_HTTP_HEADER_BLACKLIST.contains(name)) {
918 continue;
919 }
920 if (COOKIE.equals(name)) {
921
922
923 if (cookies == null) {
924 cookies = InternalThreadLocalMap.get().stringBuilder();
925 } else if (cookies.length() > 0) {
926 cookies.append("; ");
927 }
928 cookies.append(value);
929 } else if (contentEqualsIgnoreCase(HttpHeaderNames.HOST, name)) {
930
931
932
933
934 if (hostHeaderFound) {
935 if (!contentEqualsIgnoreCase(output.get(HttpHeaderNames.HOST), value)) {
936 throw streamError(streamId, PROTOCOL_ERROR,
937 "Conflicting ':authority' and 'host' headers found");
938 }
939 } else {
940 hostHeaderFound = true;
941 output.add(name, value);
942 }
943 } else {
944 output.add(name, value);
945 }
946 }
947 }
948 if (cookies != null) {
949 output.add(COOKIE, cookies.toString());
950 }
951 }
952 }
953 }