io.servicetalk.http.netty.H2ToStH1Utils Maven / Gradle / Ivy
Show all versions of servicetalk-http-netty Show documentation
/*
* Copyright © 2019 Apple Inc. and the ServiceTalk project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.servicetalk.http.netty;
import io.servicetalk.http.api.HttpHeaders;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Headers;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static io.netty.handler.codec.http.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase;
import static io.servicetalk.buffer.api.CharSequences.newAsciiString;
import static io.servicetalk.http.api.HttpHeaderNames.CONNECTION;
import static io.servicetalk.http.api.HttpHeaderNames.COOKIE;
import static io.servicetalk.http.api.HttpHeaderNames.TRANSFER_ENCODING;
import static io.servicetalk.http.api.HttpHeaderNames.UPGRADE;
import static io.servicetalk.http.api.HttpHeaderValues.KEEP_ALIVE;
import static io.servicetalk.http.netty.HeaderUtils.indexOf;
import static java.lang.Boolean.parseBoolean;
import static java.lang.System.getProperty;
final class H2ToStH1Utils {
static final CharSequence PROXY_CONNECTION = newAsciiString("proxy-connection");
/**
* Keep consistent with {@link io.servicetalk.http.api.HeaderUtils}.
*
* Whether cookie parsing should be strictly spec compliant with
* RFC6265 ({@code true}), or allow some deviations that are
* commonly observed in practice and allowed by the obsolete
* RFC2965/
* RFC2109 ({@code false}, the default).
*/
static final boolean COOKIE_STRICT_RFC_6265 = parseBoolean(getProperty(
"io.servicetalk.http.api.headers.cookieParsingStrictRfc6265", "false"));
private H2ToStH1Utils() {
// no instances.
}
static void h2HeadersSanitizeForH1(Http2Headers h2Headers) {
h2HeadersCompressCookieCrumbs(h2Headers);
}
/**
* Combine the cookie values into 1 header entry as required by
* RFC 7540, 8.1.2.5.
*
* @param h2Headers The headers which may contain cookies.
*/
static void h2HeadersCompressCookieCrumbs(Http2Headers h2Headers) {
// Netty's value iterator doesn't return elements in insertion order, this is not strictly compliant with the
// RFC and may result in reversed order cookies.
Iterator cookieItr = h2Headers.valueIterator(HttpHeaderNames.COOKIE);
if (cookieItr.hasNext()) {
CharSequence prevCookItr = cookieItr.next();
if (cookieItr.hasNext()) {
CharSequence cookieHeader = cookieItr.next();
// *2 gives some space for an extra cookie.
StringBuilder sb = new StringBuilder(prevCookItr.length() * 2 + cookieHeader.length() + 2);
sb.append(prevCookItr).append("; ").append(cookieHeader);
while (cookieItr.hasNext()) {
cookieHeader = cookieItr.next();
sb.append("; ").append(cookieHeader);
}
h2Headers.set(HttpHeaderNames.COOKIE, sb.toString());
}
}
}
/**
* Split up cookies to allow for better compression as described in
* RFC 7540, 8.1.2.5.
*
* @param h1Headers The headers which may contain cookies.
*/
static void h1HeadersSplitCookieCrumbs(HttpHeaders h1Headers) {
Iterator cookieItr = h1Headers.valuesIterator(COOKIE);
// We want to avoid "concurrent modifications" of the headers while we are iterating. So we insert crumbs
// into an intermediate collection and insert them after the split process concludes.
List cookiesToAdd = null;
while (cookieItr.hasNext()) {
CharSequence nextCookie = cookieItr.next();
int i = indexOf(nextCookie, ';', 0);
if (i > 0) {
if (cookiesToAdd == null) {
cookiesToAdd = new ArrayList<>(4);
}
int start = 0;
if (COOKIE_STRICT_RFC_6265) {
do {
final CharSequence cookieCrumb = nextCookie.subSequence(start, i);
cookiesToAdd.add(cookieCrumb);
if (i + 1 < nextCookie.length() && nextCookie.charAt(i + 1) != ' ') {
throwNoSpaceAfterCookieCrumb(cookieCrumb);
}
if (nextCookie.length() - 2 <= i) {
throwNotAllowedToEndWithSemicolon(cookieCrumb);
}
// skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1).
start = i + 2;
} while (start >= 0 && start < nextCookie.length() &&
(i = indexOf(nextCookie, ';', start)) >= 0);
} else {
do {
cookiesToAdd.add(nextCookie.subSequence(start, i));
start = i + 1 < nextCookie.length() && nextCookie.charAt(i + 1) == ' ' ? i + 2 : i + 1;
} while (start >= 0 && start < nextCookie.length() &&
(i = indexOf(nextCookie, ';', start)) >= 0);
}
if (start >= 0 && start < nextCookie.length()) {
cookiesToAdd.add(nextCookie.subSequence(start, nextCookie.length()));
}
cookieItr.remove();
}
}
if (cookiesToAdd != null) {
for (CharSequence crumb : cookiesToAdd) {
h1Headers.add(COOKIE, crumb);
}
}
}
private static void throwNoSpaceAfterCookieCrumb(CharSequence cookieCrumb) {
final int nameEnd = indexOf(cookieCrumb, '=', 0);
final CharSequence name = nameEnd > 0 ? cookieCrumb.subSequence(0, nameEnd) : cookieCrumb;
throw new IllegalArgumentException("cookie " + name +
" must have a space after ; in cookie attribute-value lists");
}
private static void throwNotAllowedToEndWithSemicolon(CharSequence cookieCrumb) {
final int nameEnd = indexOf(cookieCrumb, '=', 0);
final CharSequence name = nameEnd > 0 ? cookieCrumb.subSequence(0, nameEnd) : cookieCrumb;
throw new IllegalArgumentException("cookie '" + name + "': cookie is not allowed to end with ;");
}
static Http2Headers h1HeadersToH2Headers(HttpHeaders h1Headers) {
if (h1Headers.isEmpty()) {
if (h1Headers instanceof NettyH2HeadersToHttpHeaders) {
return ((NettyH2HeadersToHttpHeaders) h1Headers).nettyHeaders();
}
return new DefaultHttp2Headers(false, 0);
}
// H2 doesn't support connection headers, so remove each one, and the headers corresponding to the
// connection value.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
Iterator connectionItr = h1Headers.valuesIterator(CONNECTION);
if (connectionItr.hasNext()) {
do {
CharSequence connectionHeader = connectionItr.next();
connectionItr.remove();
int i = indexOf(connectionHeader, ',', 0);
if (i != -1) {
int start = 0;
do {
h1Headers.remove(connectionHeader.subSequence(start, i));
start = i + 1;
// Skip OWS
if (start < connectionHeader.length() && connectionHeader.charAt(start) == ' ') {
++start;
}
} while (start < connectionHeader.length() && (i = indexOf(connectionHeader, ',', start)) != -1);
h1Headers.remove(connectionHeader.subSequence(start, connectionHeader.length()));
} else {
h1Headers.remove(connectionHeader);
}
} while (connectionItr.hasNext());
}
// remove other illegal headers
h1Headers.remove(KEEP_ALIVE);
h1Headers.remove(TRANSFER_ENCODING);
h1Headers.remove(UPGRADE);
h1Headers.remove(PROXY_CONNECTION);
// TE header is treated specially https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// (only value of "trailers" is allowed).
Iterator teItr = h1Headers.valuesIterator(TE);
boolean addTrailers = false;
while (teItr.hasNext()) {
final CharSequence teSequence = teItr.next();
if (addTrailers) {
teItr.remove();
} else {
int i = indexOf(teSequence, ',', 0);
if (i != -1) {
int start = 0;
do {
if (contentEqualsIgnoreCase(teSequence.subSequence(start, i), TRAILERS)) {
addTrailers = true;
break;
}
start = i + 1;
// Check if we need to skip OWS
// https://www.rfc-editor.org/rfc/rfc9110.html#section-10.1.4
if (start < teSequence.length() && teSequence.charAt(start) == ' ') {
++start;
}
} while (start < teSequence.length() && (i = indexOf(teSequence, ',', start)) != -1);
if (!addTrailers && start < teSequence.length() &&
contentEqualsIgnoreCase(teSequence.subSequence(start, teSequence.length()), TRAILERS)) {
addTrailers = true;
}
teItr.remove();
} else if (!contentEqualsIgnoreCase(teSequence, TRAILERS)) {
teItr.remove();
}
}
}
if (addTrailers) { // add after iteration to avoid concurrent modification.
h1Headers.add(TE, TRAILERS);
}
h1HeadersSplitCookieCrumbs(h1Headers);
if (h1Headers instanceof NettyH2HeadersToHttpHeaders) {
// Assume header field names are already lowercase if they reside in the Http2Headers. We may want to be
// more strict in the future, but that would require iteration.
return ((NettyH2HeadersToHttpHeaders) h1Headers).nettyHeaders();
}
if (h1Headers.isEmpty()) {
return new DefaultHttp2Headers(false, 0);
}
DefaultHttp2Headers http2Headers = new DefaultHttp2Headers(false);
for (Map.Entry h1Entry : h1Headers) {
// header field names MUST be converted to lowercase prior to their encoding in HTTP/2
// https://tools.ietf.org/html/rfc7540#section-8.1.2
http2Headers.add(h1Entry.getKey().toString().toLowerCase(Locale.ENGLISH), h1Entry.getValue());
}
return http2Headers;
}
}