io.netty.handler.codec.http2.HttpConversionUtil Maven / Gradle / Ivy
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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:
*
* https://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.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.UnsupportedValueConverter;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AsciiString;
import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.UnstableApi;
import java.net.URI;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
import static io.netty.handler.codec.http.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
import static io.netty.handler.codec.http.HttpResponseStatus.parseLine;
import static io.netty.handler.codec.http.HttpScheme.HTTP;
import static io.netty.handler.codec.http.HttpScheme.HTTPS;
import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm;
import static io.netty.handler.codec.http.HttpUtil.isOriginForm;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.AsciiString.EMPTY_STRING;
import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
import static io.netty.util.AsciiString.indexOf;
import static io.netty.util.AsciiString.trim;
import static io.netty.util.ByteProcessor.FIND_COMMA;
import static io.netty.util.ByteProcessor.FIND_SEMI_COLON;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.StringUtil.isNullOrEmpty;
import static io.netty.util.internal.StringUtil.length;
import static io.netty.util.internal.StringUtil.unescapeCsvFields;
/**
* Provides utility methods and constants for the HTTP/2 to HTTP conversion
*/
@UnstableApi
public final class HttpConversionUtil {
/**
* The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2.
*/
private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST =
new CharSequenceMap();
static {
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(CONNECTION, EMPTY_STRING);
@SuppressWarnings("deprecation")
AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
@SuppressWarnings("deprecation")
AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION;
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
}
/**
* This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in [RFC 7540], Section 8.1
*/
public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
/**
* This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in [RFC 7540], Section 8.1
*/
public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
/**
* This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined
* in [RFC 7540], Section 8.1
*/
public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
/**
* [RFC 7540], 8.1.2.3 states the path must not
* be empty, and instead should be {@code /}.
*/
private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
private HttpConversionUtil() {
}
/**
* Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects
*/
public enum ExtensionHeaderNames {
/**
* HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for
* generating an {@code HttpObject}
*
* {@code "x-http2-stream-id"}
*/
STREAM_ID("x-http2-stream-id"),
/**
* HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for
* generating an {@code HttpObject}
*
* {@code "x-http2-scheme"}
*/
SCHEME("x-http2-scheme"),
/**
* HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for
* generating an {@code HttpObject}
*
* {@code "x-http2-path"}
*/
PATH("x-http2-path"),
/**
* HTTP extension header which will identify the stream id used to create this stream in an HTTP/2 push promise
* frame
*
* {@code "x-http2-stream-promise-id"}
*/
STREAM_PROMISE_ID("x-http2-stream-promise-id"),
/**
* HTTP extension header which will identify the stream id which this stream is dependent on. This stream will
* be a child node of the stream id associated with this header value.
*
* {@code "x-http2-stream-dependency-id"}
*/
STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
/**
* HTTP extension header which will identify the weight (if non-default and the priority is not on the default
* stream) of the associated HTTP/2 stream responsible responsible for generating an {@code HttpObject}
*
* {@code "x-http2-stream-weight"}
*/
STREAM_WEIGHT("x-http2-stream-weight");
private final AsciiString text;
ExtensionHeaderNames(String text) {
this.text = AsciiString.cached(text);
}
public AsciiString text() {
return text;
}
}
/**
* Apply HTTP/2 rules while translating status code to {@link HttpResponseStatus}
*
* @param status The status from an HTTP/2 frame
* @return The HTTP/1.x status
* @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
*/
public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception {
HttpResponseStatus result;
try {
result = parseLine(status);
if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
}
} catch (Http2Exception e) {
throw e;
} catch (Throwable t) {
throw connectionError(PROTOCOL_ERROR, t,
"Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
}
return result;
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
boolean validateHttpHeaders) throws Http2Exception {
return toFullHttpResponse(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param content {@link ByteBuf} content to put in {@link FullHttpResponse}
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpResponse toFullHttpResponse(int streamId, Http2Headers http2Headers, ByteBuf content,
boolean validateHttpHeaders)
throws Http2Exception {
HttpResponseStatus status = parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an
// HTTP/1.1 status line.
FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content,
validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
} catch (Http2Exception e) {
msg.release();
throw e;
} catch (Throwable t) {
msg.release();
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
return msg;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc,
boolean validateHttpHeaders) throws Http2Exception {
return toFullHttpRequest(streamId, http2Headers, alloc.buffer(), validateHttpHeaders);
}
private static String extractPath(CharSequence method, Http2Headers headers) {
if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
// See https://tools.ietf.org/html/rfc7231#section-4.3.6
return checkNotNull(headers.authority(),
"authority header cannot be null in the conversion to HTTP/1.x").toString();
} else {
return checkNotNull(headers.path(),
"path header cannot be null in conversion to HTTP/1.x").toString();
}
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param content {@link ByteBuf} content to put in {@link FullHttpRequest}
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBuf content,
boolean validateHttpHeaders) throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
final CharSequence method = checkNotNull(http2Headers.method(),
"method header cannot be null in conversion to HTTP/1.x");
final CharSequence path = extractPath(method, http2Headers);
FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
.toString()), path.toString(), content, validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
} catch (Http2Exception e) {
msg.release();
throw e;
} catch (Throwable t) {
msg.release();
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
return msg;
}
/**
* Create a new object to contain the request data.
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new request object which represents headers for a chunked request
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line.
final CharSequence method = checkNotNull(http2Headers.method(),
"method header cannot be null in conversion to HTTP/1.x");
final CharSequence path = extractPath(method, http2Headers);
HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()),
path.toString(), validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
} catch (Http2Exception e) {
throw e;
} catch (Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
return msg;
}
/**
* Create a new object to contain the response data.
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param validateHttpHeaders
* - {@code true} to validate HTTP headers in the http-codec
* - {@code false} not to validate HTTP headers in the http-codec
*
* @return A new response object which represents headers for a chunked response
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers,
* HttpHeaders, HttpVersion, boolean, boolean)}
*/
public static HttpResponse toHttpResponse(final int streamId,
final Http2Headers http2Headers,
final boolean validateHttpHeaders) throws Http2Exception {
final HttpResponseStatus status = parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an
// HTTP/1.1 status line.
final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, false);
} catch (final Http2Exception e) {
throw e;
} catch (final Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers.
*
* @param streamId The stream associated with {@code sourceHeaders}.
* @param inputHeaders The HTTP/2 headers to convert.
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers.
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
* @see #addHttp2ToHttpHeaders(int, Http2Headers, HttpHeaders, HttpVersion, boolean, boolean)
*/
public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
addHttp2ToHttpHeaders(streamId, inputHeaders,
addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(),
destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest);
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers.
*
* @param streamId The stream associated with {@code sourceHeaders}.
* @param inputHeaders The HTTP/2 headers to convert.
* @param outputHeaders The object which will contain the resulting HTTP/1.x headers..
* @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion.
* @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers.
* {@code false} otherwise.
* @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message.
* {@code false} for response message.
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
*/
public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders, HttpHeaders outputHeaders,
HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
try {
translator.translateHeaders(inputHeaders);
} catch (Http2Exception ex) {
throw ex;
} catch (Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
outputHeaders.remove(HttpHeaderNames.TRAILER);
if (!isTrailer) {
outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
}
}
/**
* Converts the given HTTP/1.x headers into HTTP/2 headers.
* The following headers are only used if they can not be found in from the {@code HOST} header or the
* {@code Request-Line} as defined by rfc7230
*
* - {@link ExtensionHeaderNames#SCHEME}
*
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) {
HttpHeaders inHeaders = in.headers();
final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in;
String host = inHeaders.getAsString(HttpHeaderNames.HOST);
if (isOriginForm(request.uri()) || isAsteriskForm(request.uri())) {
out.path(new AsciiString(request.uri()));
setHttp2Scheme(inHeaders, out);
} else {
URI requestTargetUri = URI.create(request.uri());
out.path(toHttp2Path(requestTargetUri));
// Take from the request-line if HOST header was empty
host = isNullOrEmpty(host) ? requestTargetUri.getAuthority() : host;
setHttp2Scheme(inHeaders, requestTargetUri, out);
}
setHttp2Authority(host, out);
out.method(request.method().asciiName());
} else if (in instanceof HttpResponse) {
HttpResponse response = (HttpResponse) in;
out.status(response.status().codeAsText());
}
// Add the HTTP headers which have not been consumed above
toHttp2Headers(inHeaders, out);
return out;
}
public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) {
if (inHeaders.isEmpty()) {
return EmptyHttp2Headers.INSTANCE;
}
final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size());
toHttp2Headers(inHeaders, out);
return out;
}
private static CharSequenceMap toLowercaseMap(Iterator extends CharSequence> valuesIter,
int arraySizeHint) {
UnsupportedValueConverter valueConverter = UnsupportedValueConverter.instance();
CharSequenceMap result = new CharSequenceMap(true, valueConverter, arraySizeHint);
while (valuesIter.hasNext()) {
AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
try {
int index = lowerCased.forEachByte(FIND_COMMA);
if (index != -1) {
int start = 0;
do {
result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
start = index + 1;
} while (start < lowerCased.length() &&
(index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1);
result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
} else {
result.add(lowerCased.trim(), EMPTY_STRING);
}
} catch (Exception e) {
// This is not expect to happen because FIND_COMMA never throws but must be caught
// because of the ByteProcessor interface.
throw new IllegalStateException(e);
}
}
return result;
}
/**
* Filter the {@link HttpHeaderNames#TE} header according to the
* special rules in the HTTP/2 RFC.
* @param entry An entry whose name is {@link HttpHeaderNames#TE}.
* @param out the resulting HTTP/2 headers.
*/
private static void toHttp2HeadersFilterTE(Entry entry,
Http2Headers out) {
if (indexOf(entry.getValue(), ',', 0) == -1) {
if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
out.add(TE, TRAILERS);
}
} else {
List teValues = unescapeCsvFields(entry.getValue());
for (CharSequence teValue : teValues) {
if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) {
out.add(TE, TRAILERS);
break;
}
}
}
}
public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) {
Iterator> iter = inHeaders.iteratorCharSequence();
// Choose 8 as a default size because it is unlikely we will see more than 4 Connection headers values, but
// still allowing for "enough" space in the map to reduce the chance of hash code collision.
CharSequenceMap connectionBlacklist =
toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
while (iter.hasNext()) {
Entry entry = iter.next();
final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
if (aName.contentEqualsIgnoreCase(TE)) {
toHttp2HeadersFilterTE(entry, out);
} else if (aName.contentEqualsIgnoreCase(COOKIE)) {
CharSequence valueCs = entry.getValue();
// validate
boolean invalid = false;
for (int i = 0; i < valueCs.length(); i++) {
char c = valueCs.charAt(i);
if (c == ';') {
if (i + 1 >= valueCs.length() || valueCs.charAt(i + 1) != ' ') {
// semicolon not followed by space. invalid, don't split
invalid = true;
break;
}
i++; // skip space
} else if (c > 255) {
// not ascii, don't split
invalid = true;
break;
}
}
if (invalid) {
out.add(COOKIE, valueCs);
} else {
splitValidCookieHeader(out, valueCs);
}
} else {
out.add(aName, entry.getValue());
}
}
}
}
private static void splitValidCookieHeader(Http2Headers out, CharSequence valueCs) {
try {
AsciiString value = AsciiString.of(valueCs);
// split up cookies to allow for better compression
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
int index = value.forEachByte(FIND_SEMI_COLON);
if (index != -1) {
int start = 0;
do {
out.add(COOKIE, value.subSequence(start, index, false));
assert index + 1 < value.length();
assert value.charAt(index + 1) == ' ';
// skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1)
start = index + 2;
} while (start < value.length() &&
(index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
assert start < value.length();
out.add(COOKIE, value.subSequence(start, value.length(), false));
} else {
out.add(COOKIE, value);
}
} catch (Exception e) {
// This is not expect to happen because FIND_SEMI_COLON never throws but must be caught
// because of the ByteProcessor interface.
throw new IllegalStateException(e);
}
}
/**
* Generate an HTTP/2 {code :path} from a URI in accordance with
* rfc7230, 5.3.
*/
private static AsciiString toHttp2Path(URI uri) {
StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) +
length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2);
if (!isNullOrEmpty(uri.getRawPath())) {
pathBuilder.append(uri.getRawPath());
}
if (!isNullOrEmpty(uri.getRawQuery())) {
pathBuilder.append('?');
pathBuilder.append(uri.getRawQuery());
}
if (!isNullOrEmpty(uri.getRawFragment())) {
pathBuilder.append('#');
pathBuilder.append(uri.getRawFragment());
}
String path = pathBuilder.toString();
return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path);
}
// package-private for testing only
static void setHttp2Authority(String authority, Http2Headers out) {
// The authority MUST NOT include the deprecated "userinfo" subcomponent
if (authority != null) {
if (authority.isEmpty()) {
out.authority(EMPTY_STRING);
} else {
int start = authority.indexOf('@') + 1;
int length = authority.length() - start;
if (length == 0) {
throw new IllegalArgumentException("authority: " + authority);
}
out.authority(new AsciiString(authority, start, length));
}
}
}
private static void setHttp2Scheme(HttpHeaders in, Http2Headers out) {
setHttp2Scheme(in, URI.create(""), out);
}
private static void setHttp2Scheme(HttpHeaders in, URI uri, Http2Headers out) {
String value = uri.getScheme();
if (!isNullOrEmpty(value)) {
out.scheme(new AsciiString(value));
return;
}
// Consume the Scheme extension header if present
CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
if (cValue != null) {
out.scheme(AsciiString.of(cValue));
return;
}
if (uri.getPort() == HTTPS.port()) {
out.scheme(HTTPS.name());
} else if (uri.getPort() == HTTP.port()) {
out.scheme(HTTP.name());
} else {
throw new IllegalArgumentException(":scheme must be specified. " +
"see https://tools.ietf.org/html/rfc7540#section-8.1.2.3");
}
}
/**
* Utility which translates HTTP/2 headers to HTTP/1 headers.
*/
private static final class Http2ToHttpHeaderTranslator {
/**
* Translations from HTTP/2 header name to the HTTP/1.x equivalent.
*/
private static final CharSequenceMap
REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap();
private static final CharSequenceMap
RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap();
static {
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
HttpHeaderNames.HOST);
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
ExtensionHeaderNames.SCHEME.text());
REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
ExtensionHeaderNames.PATH.text());
}
private final int streamId;
private final HttpHeaders output;
private final CharSequenceMap translations;
/**
* Create a new instance
*
* @param output The HTTP/1.x headers object to store the results of the translation
* @param request if {@code true}, translates headers using the request translation map. Otherwise uses the
* response translation map.
*/
Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
this.streamId = streamId;
this.output = output;
translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
}
void translateHeaders(Iterable> inputHeaders) throws Http2Exception {
// lazily created as needed
StringBuilder cookies = null;
for (Entry entry : inputHeaders) {
final CharSequence name = entry.getKey();
final CharSequence value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null) {
output.add(translatedName, AsciiString.of(value));
} else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (name.length() == 0 || name.charAt(0) == ':') {
throw streamError(streamId, PROTOCOL_ERROR,
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
}
if (COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
if (cookies == null) {
cookies = InternalThreadLocalMap.get().stringBuilder();
} else if (cookies.length() > 0) {
cookies.append("; ");
}
cookies.append(value);
} else {
output.add(name, value);
}
}
}
if (cookies != null) {
output.add(COOKIE, cookies.toString());
}
}
}
}