org.xbib.helianthus.internal.http.HelianthusHttpUtil Maven / Gradle / Ivy
package org.xbib.helianthus.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 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;
import org.xbib.helianthus.common.http.DefaultHttpHeaders;
import org.xbib.helianthus.common.http.HttpHeaderNames;
import org.xbib.helianthus.common.http.HttpHeaders;
import org.xbib.helianthus.common.http.HttpMethod;
import org.xbib.helianthus.common.http.HttpStatus;
import org.xbib.helianthus.common.http.HttpStatusClass;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map.Entry;
/**
* Provides various utility functions for internal use related with HTTP.
* The conversion between HTTP/1 and HTTP/2 has been forked from Netty's {@link HttpConversionUtil}.
*/
public final class HelianthusHttpUtil {
private static final URI ROOT = URI.create("/");
public static final HashingStrategy HTTP2_HEADER_NAME_HASHER =
new HashingStrategy() {
@Override
public int hashCode(AsciiString o) {
return o.hashCode();
}
@Override
public boolean equals(AsciiString a, AsciiString b) {
return a.equals(b);
}
};
/**
* 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);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.KEEP_ALIVE, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.PROXY_CONNECTION, 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);
}
private HelianthusHttpUtil() {
}
public static boolean isContentAlwaysEmpty(HttpStatus status) {
if (status.codeClass() == HttpStatusClass.INFORMATIONAL) {
return true;
}
switch (status.code()) {
case 204:
case 205:
case 304:
return true;
default:
return false;
}
}
public static HttpHeaders toHelianthus(Http2Headers headers) {
final HttpHeaders converted = new DefaultHttpHeaders(false, headers.size());
for (Entry e : headers) {
converted.add(AsciiString.of(e.getKey()), e.getValue().toString());
}
return converted;
}
/**
* Converts the given HTTP/1.x headers into 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 rfc7230
*
* - {@link ExtensionHeaderNames#SCHEME}
*
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static HttpHeaders toHelianthus(HttpRequest in) throws URISyntaxException {
final URI requestTargetUri = toUri(in);
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
toHelianthus(inHeaders, out);
return out;
}
/**
* Converts the headers of the given Netty HTTP/1.x response into Armeria HTTP/2 headers.
*/
public static HttpHeaders toHelianthus(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
toHelianthus(inHeaders, out);
return out;
}
public static HttpHeaders toHelianthus(io.netty.handler.codec.http.HttpHeaders inHeaders) {
if (inHeaders.isEmpty()) {
return HttpHeaders.EMPTY_HEADERS;
}
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
toHelianthus(inHeaders, out);
return out;
}
public static void toHelianthus(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeaders out) {
final Iterator> i = inHeaders.iteratorCharSequence();
while (i.hasNext()) {
final Entry 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
* rfc7230, 5.3.
*/
private static String 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());
}
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");
}
}
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;
}
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 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 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 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 {
CharSequenceMap() {
super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy