/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation 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:
*
* 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.
*/
/*
* 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:
*
* 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 com.linecorp.armeria.internal.http;
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.streamError;
import static io.netty.util.AsciiString.EMPTY_STRING;
import static io.netty.util.internal.StringUtil.isNullOrEmpty;
import static io.netty.util.internal.StringUtil.length;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map.Entry;
import com.linecorp.armeria.common.http.DefaultHttpHeaders;
import com.linecorp.armeria.common.http.HttpHeaderNames;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.common.http.HttpMethod;
import com.linecorp.armeria.common.http.HttpStatus;
import com.linecorp.armeria.common.http.HttpStatusClass;
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.UnsupportedValueConverter;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.AsciiString;
import io.netty.util.HashingStrategy;
/**
* Provides various utility functions for internal use related with HTTP.
*
* <p>The conversion between HTTP/1 and HTTP/2 has been forked from Netty's {@link HttpConversionUtil}.
*/
public final class ArmeriaHttpUtil {
/**
* The default case-insensitive {@link AsciiString} hasher and comparator for HTTP/2 headers.
*/
public static final HashingStrategy<AsciiString> HTTP2_HEADER_NAME_HASHER =
new HashingStrategy<AsciiString>() {
@Override
public int hashCode(AsciiString o) {
return o.hashCode();
}
@Override
public boolean equals(AsciiString a, AsciiString b) {
return a.contentEqualsIgnoreCase(b);
}
};
private static final URI ROOT = URI.create("/");
/**
* The set of headers that should not be directly copied when converting headers from HTTP/1 to HTTP/2.
*/
private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST = new CharSequenceMap();
/**
* The set of headers that should not be directly copied when converting headers from HTTP/2 to HTTP/1.
*/
private static final CharSequenceMap HTTP2_TO_HTTP_HEADER_BLACKLIST = new CharSequenceMap();
static {
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING);
@SuppressWarnings("deprecation")
final AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
@SuppressWarnings("deprecation")
final 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);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.AUTHORITY, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.METHOD, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.PATH, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.SCHEME, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.STATUS, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
}
/**
* Returns {@code true} if the content of the response with the given {@link HttpStatus} is expected to
* be always empty (1xx, 204, 205 and 304 responses.)
*/
public static boolean isContentAlwaysEmpty(HttpStatus status) {
if (status.codeClass() == HttpStatusClass.INFORMATIONAL) {
return true;
}
switch (status.code()) {
case 204: case 205: case 304:
return true;
}
return false;
}
/**
* Converts the specified Netty HTTP/2 into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(Http2Headers headers) {
final HttpHeaders converted = new DefaultHttpHeaders(false, headers.size());
for (Entry<CharSequence, CharSequence> e : headers) {
converted.add(AsciiString.of(e.getKey()), e.getValue().toString());
}
return converted;
}
/**
* Converts the headers of the given Netty HTTP/1.x request into Armeria HTTP/2 headers.
* The following headers are only used if they can not be found in the {@code HOST} header or the
* {@code Request-Line} as defined by <a href="https://tools.ietf.org/html/rfc7230">rfc7230</a>
* <ul>
* <li>{@link ExtensionHeaderNames#SCHEME}</li>
* </ul>
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static HttpHeaders toArmeria(HttpRequest in) throws URISyntaxException {
final URI requestTargetUri = toUri(in);
final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
out.path(toHttp2Path(requestTargetUri));
out.method(HttpMethod.valueOf(in.method().name()));
setHttp2Scheme(inHeaders, requestTargetUri, out);
if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
// Attempt to take from HOST header before taking from the request-line
String host = inHeaders.getAsString(HttpHeaderNames.HOST);
setHttp2Authority(host == null || host.isEmpty() ? requestTargetUri.getAuthority() : host, out);
}
// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the headers of the given Netty HTTP/1.x response into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(HttpResponse in) {
io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
out.status(in.status().code());
// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders) {
if (inHeaders.isEmpty()) {
return HttpHeaders.EMPTY_HEADERS;
}
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
*/
public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeaders out) {
final Iterator<Entry<CharSequence, CharSequence>> i = inHeaders.iteratorCharSequence();
while (i.hasNext()) {
final Entry<CharSequence, CharSequence> entry = i.next();
final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
if (aName.contentEqualsIgnoreCase(HttpHeaderNames.TE) &&
!AsciiString.contentEqualsIgnoreCase(entry.getValue(), HttpHeaderValues.TRAILERS)) {
continue;
}
out.add(aName, entry.getValue().toString());
}
}
}
private static URI toUri(HttpRequest in) throws URISyntaxException {
final String uri = in.uri();
if (uri.startsWith("//")) {
// Normalize the path that starts with more than one slash into the one with a single slash,
// so that java.net.URI does not raise a URISyntaxException.
for (int i = 0; i < uri.length(); i++) {
if (uri.charAt(i) != '/') {
return new URI(uri.substring(i - 1));
}
}
return ROOT;
} else {
return new URI(uri);
}
}
/**
* Generate a HTTP/2 {code :path} from a URI in accordance with
* <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
*/
private static String toHttp2Path(URI uri) {
final 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());
}
return pathBuilder.toString();
}
private static void setHttp2Authority(String authority, HttpHeaders out) {
// The authority MUST NOT include the deprecated "userinfo" subcomponent
if (authority != null) {
int endOfUserInfo = authority.indexOf('@');
if (endOfUserInfo < 0) {
out.authority(authority);
} else if (endOfUserInfo + 1 < authority.length()) {
out.authority(authority.substring(endOfUserInfo + 1));
} else {
throw new IllegalArgumentException("authority: " + authority);
}
}
}
private static void setHttp2Scheme(io.netty.handler.codec.http.HttpHeaders in, URI uri, HttpHeaders out) {
String value = uri.getScheme();
if (value != null) {
out.scheme(value);
return;
}
// Consume the Scheme extension header if present
CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
if (cValue != null) {
out.scheme(cValue.toString());
} else {
out.scheme("unknown");
}
}
/**
* Converts the specified Armeria HTTP/2 headers into Netty HTTP/2 headers.
*/
public static Http2Headers toNettyHttp2(HttpHeaders inputHeaders) {
final Http2Headers outputHeaders = new DefaultHttp2Headers(false, inputHeaders.size());
outputHeaders.set(inputHeaders);
outputHeaders.remove(HttpHeaderNames.CONNECTION);
outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
outputHeaders.remove(HttpHeaderNames.TRAILER);
return outputHeaders;
}
/**
* Converts the specified Armeria HTTP/2 headers into Netty HTTP/1 headers.
*/
public static io.netty.handler.codec.http.HttpHeaders toNettyHttp1(HttpHeaders inputHeaders) {
final io.netty.handler.codec.http.DefaultHttpHeaders outputHeaders =
new io.netty.handler.codec.http.DefaultHttpHeaders();
for (Entry<AsciiString, String> e : inputHeaders) {
final AsciiString name = e.getKey();
if (name.isEmpty() || HTTP2_TO_HTTP_HEADER_BLACKLIST.contains(name)) {
continue;
}
outputHeaders.add(name, e.getValue());
}
return outputHeaders;
}
/**
* 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 toNettyHttp1(
int streamId, HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
final Http2ToHttpHeaderTranslator translator =
new Http2ToHttpHeaderTranslator(outputHeaders, isRequest);
try {
for (Entry<AsciiString, String> entry : inputHeaders) {
translator.translate(entry);
}
} 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) {
HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
}
}
/**
* 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);
REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
}
private final io.netty.handler.codec.http.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(io.netty.handler.codec.http.HttpHeaders output,
boolean request) {
this.output = output;
translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
}
public void translate(Entry<AsciiString, String> entry) {
final AsciiString name = entry.getKey();
final String value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null) {
output.add(translatedName, value);
return;
}
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
if (name.isEmpty() || HTTP2_TO_HTTP_HEADER_BLACKLIST.contains(name)) {
return;
}
if (HttpHeaderNames.COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
String existingCookie = output.get(HttpHeaderNames.COOKIE);
output.set(HttpHeaderNames.COOKIE,
existingCookie != null ? existingCookie + "; " + value : value);
} else {
output.add(name, value);
}
}
}
private static final class CharSequenceMap
extends DefaultHeaders<AsciiString, AsciiString, CharSequenceMap> {
CharSequenceMap() {
super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance());
}
}
private ArmeriaHttpUtil() {}
}