com.netflix.zuul.message.http.HttpRequestMessageImpl Maven / Gradle / Ivy
/*
* Copyright 2018 Netflix, Inc.
*
* 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 com.netflix.zuul.message.http;
import com.google.common.annotations.VisibleForTesting;
import com.netflix.config.CachedDynamicBooleanProperty;
import com.netflix.config.CachedDynamicIntProperty;
import com.netflix.config.DynamicStringProperty;
import com.netflix.util.Pair;
import com.netflix.zuul.context.CommonContextKeys;
import com.netflix.zuul.context.SessionContext;
import com.netflix.zuul.filters.ZuulFilter;
import com.netflix.zuul.message.Headers;
import com.netflix.zuul.message.ZuulMessage;
import com.netflix.zuul.message.ZuulMessageImpl;
import com.netflix.zuul.util.HttpUtils;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* User: michaels
* Date: 2/24/15
* Time: 10:54 AM
*/
public class HttpRequestMessageImpl implements HttpRequestMessage {
private static final Logger LOG = LoggerFactory.getLogger(HttpRequestMessageImpl.class);
private static final CachedDynamicBooleanProperty STRICT_HOST_HEADER_VALIDATION =
new CachedDynamicBooleanProperty("zuul.HttpRequestMessage.host.header.strict.validation", true);
private static final CachedDynamicIntProperty MAX_BODY_SIZE_PROP =
new CachedDynamicIntProperty("zuul.HttpRequestMessage.body.max.size", 15 * 1000 * 1024);
private static final CachedDynamicBooleanProperty CLEAN_COOKIES =
new CachedDynamicBooleanProperty("zuul.HttpRequestMessage.cookies.clean", false);
/** ":::"-delimited list of regexes to strip out of the cookie headers. */
private static final DynamicStringProperty REGEX_PTNS_TO_STRIP_PROP =
new DynamicStringProperty("zuul.request.cookie.cleaner.strip", " Secure,");
private static final List RE_STRIP;
static {
RE_STRIP = new ArrayList<>();
for (String ptn : REGEX_PTNS_TO_STRIP_PROP.get().split(":::")) {
RE_STRIP.add(Pattern.compile(ptn));
}
}
private static final String URI_SCHEME_SEP = "://";
private static final String URI_SCHEME_HTTP = "http";
private static final String URI_SCHEME_HTTPS = "https";
private final boolean immutable;
private ZuulMessage message;
private String protocol;
private String method;
private String path;
private String decodedPath;
private HttpQueryParams queryParams;
private String clientIp;
private String scheme;
private int port;
private String serverName;
private SocketAddress clientRemoteAddress;
private HttpRequestInfo inboundRequest = null;
private Cookies parsedCookies = null;
// These attributes are populated only if immutable=true.
private String reconstructedUri = null;
private String pathAndQuery = null;
private String infoForLogging = null;
private String originalHost = null;
private static final SocketAddress UNDEFINED_CLIENT_DEST_ADDRESS = new SocketAddress() {
@Override
public String toString() {
return "Undefined destination address.";
}
};
public HttpRequestMessageImpl(
SessionContext context,
String protocol,
String method,
String path,
HttpQueryParams queryParams,
Headers headers,
String clientIp,
String scheme,
int port,
String serverName) {
this(
context,
protocol,
method,
path,
queryParams,
headers,
clientIp,
scheme,
port,
serverName,
UNDEFINED_CLIENT_DEST_ADDRESS,
false);
}
public HttpRequestMessageImpl(
SessionContext context,
String protocol,
String method,
String path,
HttpQueryParams queryParams,
Headers headers,
String clientIp,
String scheme,
int port,
String serverName,
SocketAddress clientRemoteAddress,
boolean immutable) {
this.immutable = immutable;
this.message = new ZuulMessageImpl(context, headers);
this.protocol = protocol;
this.method = method;
this.path = path;
try {
this.decodedPath = URLDecoder.decode(path, "UTF-8");
} catch (Exception e) {
// fail to decode URI
// just set decodedPath to original path
this.decodedPath = path;
}
// Don't allow this to be null.
this.queryParams = queryParams == null ? new HttpQueryParams() : queryParams;
this.clientIp = clientIp;
this.scheme = scheme;
this.port = port;
this.serverName = serverName;
this.clientRemoteAddress = clientRemoteAddress;
}
private void immutableCheck() {
if (immutable) {
throw new IllegalStateException(
"This HttpRequestMessageImpl is immutable. No mutating operations allowed!");
}
}
@Override
public SessionContext getContext() {
return message.getContext();
}
@Override
public Headers getHeaders() {
return message.getHeaders();
}
@Override
public void setHeaders(Headers newHeaders) {
immutableCheck();
message.setHeaders(newHeaders);
}
@Override
public void setHasBody(boolean hasBody) {
message.setHasBody(hasBody);
}
@Override
public boolean hasBody() {
return message.hasBody();
}
@Override
public void bufferBodyContents(HttpContent chunk) {
message.bufferBodyContents(chunk);
}
@Override
public void setBodyAsText(String bodyText) {
message.setBodyAsText(bodyText);
}
@Override
public void setBody(byte[] body) {
message.setBody(body);
}
@Override
public boolean finishBufferedBodyIfIncomplete() {
return message.finishBufferedBodyIfIncomplete();
}
@Override
public Iterable getBodyContents() {
return message.getBodyContents();
}
@Override
public void runBufferedBodyContentThroughFilter(ZuulFilter filter) {
message.runBufferedBodyContentThroughFilter(filter);
}
@Override
public String getBodyAsText() {
return message.getBodyAsText();
}
@Override
public byte[] getBody() {
return message.getBody();
}
@Override
public int getBodyLength() {
return message.getBodyLength();
}
@Override
public void resetBodyReader() {
message.resetBodyReader();
}
@Override
public boolean hasCompleteBody() {
return message.hasCompleteBody();
}
@Override
public void disposeBufferedBody() {
message.disposeBufferedBody();
}
@Override
public String getProtocol() {
return protocol;
}
@Override
public void setProtocol(String protocol) {
immutableCheck();
this.protocol = protocol;
}
@Override
public String getMethod() {
return method;
}
@Override
public void setMethod(String method) {
immutableCheck();
this.method = method;
}
@Override
public String getPath() {
if (message.getContext().get(CommonContextKeys.ZUUL_USE_DECODED_URI) == Boolean.TRUE) {
return decodedPath;
}
return path;
}
@Override
public void setPath(String path) {
immutableCheck();
this.path = path;
this.decodedPath = path;
}
@Override
public HttpQueryParams getQueryParams() {
return queryParams;
}
@Override
public String getPathAndQuery() {
// If this instance is immutable, then lazy-cache.
if (immutable) {
if (pathAndQuery == null) {
pathAndQuery = generatePathAndQuery();
}
return pathAndQuery;
} else {
return generatePathAndQuery();
}
}
protected String generatePathAndQuery() {
if (queryParams != null && queryParams.entries().size() > 0) {
return getPath() + "?" + queryParams.toEncodedString();
} else {
return getPath();
}
}
@Override
public String getClientIp() {
return clientIp;
}
@Deprecated
@VisibleForTesting
void setClientIp(String clientIp) {
immutableCheck();
this.clientIp = clientIp;
}
/**
* Note: this is meant to be used typically in cases where TLS termination happens on instance.
* For CLB/ALB fronted traffic, consider using {@link #getOriginalScheme()} instead.
*/
@Override
public String getScheme() {
return scheme;
}
@Override
public void setScheme(String scheme) {
immutableCheck();
this.scheme = scheme;
}
@Override
public int getPort() {
return port;
}
@Deprecated
@VisibleForTesting
void setPort(int port) {
immutableCheck();
this.port = port;
}
@Override
public String getServerName() {
return serverName;
}
@Override
public void setServerName(String serverName) {
immutableCheck();
this.serverName = serverName;
}
@Override
public Cookies parseCookies() {
if (parsedCookies == null) {
parsedCookies = reParseCookies();
}
return parsedCookies;
}
@Override
public Cookies reParseCookies() {
Cookies cookies = new Cookies();
for (String aCookieHeader : getHeaders().getAll(HttpHeaderNames.COOKIE)) {
try {
if (CLEAN_COOKIES.get()) {
aCookieHeader = cleanCookieHeader(aCookieHeader);
}
List decoded = ServerCookieDecoder.LAX.decodeAll(aCookieHeader);
for (Cookie cookie : decoded) {
cookies.add(cookie);
}
} catch (Exception e) {
LOG.warn(
"Error parsing request Cookie header. cookie={}, request-info={}",
aCookieHeader,
getInfoForLogging(),
e);
}
}
parsedCookies = cookies;
return cookies;
}
@VisibleForTesting
static String cleanCookieHeader(String cookie) {
for (Pattern stripPtn : RE_STRIP) {
Matcher matcher = stripPtn.matcher(cookie);
if (matcher.find()) {
cookie = matcher.replaceAll("");
}
}
return cookie;
}
@Override
public int getMaxBodySize() {
return MAX_BODY_SIZE_PROP.get();
}
@Override
public ZuulMessage clone() {
HttpRequestMessageImpl clone = new HttpRequestMessageImpl(
message.getContext().clone(),
protocol,
method,
path,
queryParams.clone(),
Headers.copyOf(message.getHeaders()),
clientIp,
scheme,
port,
serverName,
clientRemoteAddress,
immutable);
if (getInboundRequest() != null) {
clone.inboundRequest = (HttpRequestInfo) getInboundRequest().clone();
}
return clone;
}
protected HttpRequestInfo copyRequestInfo() {
HttpRequestMessageImpl req = new HttpRequestMessageImpl(
message.getContext(),
protocol,
method,
path,
queryParams.immutableCopy(),
Headers.copyOf(message.getHeaders()),
clientIp,
scheme,
port,
serverName,
clientRemoteAddress,
true);
req.setHasBody(hasBody());
return req;
}
@Override
public void storeInboundRequest() {
inboundRequest = copyRequestInfo();
}
@Override
public HttpRequestInfo getInboundRequest() {
return inboundRequest;
}
@Override
public void setQueryParams(HttpQueryParams queryParams) {
immutableCheck();
this.queryParams = queryParams;
}
@Override
public String getInfoForLogging() {
// If this instance is immutable, then lazy-cache generating this info.
if (immutable) {
if (infoForLogging == null) {
infoForLogging = generateInfoForLogging();
}
return infoForLogging;
} else {
return generateInfoForLogging();
}
}
protected String generateInfoForLogging() {
HttpRequestInfo req = getInboundRequest() == null ? this : getInboundRequest();
StringBuilder sb = new StringBuilder()
.append("uri=")
.append(req.reconstructURI())
.append(", method=")
.append(req.getMethod())
.append(", clientip=")
.append(HttpUtils.getClientIP(req));
return sb.toString();
}
/**
* The originally request host. This will NOT include port.
*
* The Host header may contain port, but in this method we strip it out for consistency - use the
* getOriginalPort method for that.
*/
@Override
public String getOriginalHost() {
try {
if (originalHost == null) {
originalHost = getOriginalHost(getHeaders(), getServerName());
}
return originalHost;
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
@VisibleForTesting
static String getOriginalHost(Headers headers, String serverName) throws URISyntaxException {
String xForwardedHost = headers.getFirst(HttpHeaderNames.X_FORWARDED_HOST);
if (xForwardedHost != null) {
return xForwardedHost;
}
Pair host = parseHostHeader(headers);
if (host.first() != null) {
return host.first();
}
return serverName;
}
@Override
public String getOriginalScheme() {
String scheme = getHeaders().getFirst(HttpHeaderNames.X_FORWARDED_PROTO);
if (scheme == null) {
scheme = getScheme();
}
return scheme;
}
@Override
public String getOriginalProtocol() {
String proto = getHeaders().getFirst(HttpHeaderNames.X_FORWARDED_PROTO_VERSION);
if (proto == null) {
proto = getProtocol();
}
return proto;
}
@Override
public int getOriginalPort() {
try {
return getOriginalPort(getContext(), getHeaders(), getPort());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
@VisibleForTesting
static int getOriginalPort(SessionContext context, Headers headers, int serverPort) throws URISyntaxException {
if (context.containsKey(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS)) {
return ((InetSocketAddress) context.get(CommonContextKeys.PROXY_PROTOCOL_DESTINATION_ADDRESS)).getPort();
}
String portStr = headers.getFirst(HttpHeaderNames.X_FORWARDED_PORT);
if (portStr != null && !portStr.isEmpty()) {
return Integer.parseInt(portStr);
}
try {
// Check if port was specified on a Host header.
Pair host = parseHostHeader(headers);
if (host.second() != -1) {
return host.second();
}
} catch (URISyntaxException e) {
LOG.debug("Invalid host header, falling back to serverPort", e);
}
return serverPort;
}
@Override
public Optional getClientDestinationPort() {
if (clientRemoteAddress instanceof InetSocketAddress) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) this.clientRemoteAddress;
return Optional.of(inetSocketAddress.getPort());
} else {
return Optional.empty();
}
}
/**
* Attempt to parse the Host header from the collection of headers
* and return the hostname and port components.
*
* @return Hostname and Port pair.
* Hostname may be null. Port may be -1 when no valid port is found in the host header.
*/
private static Pair parseHostHeader(Headers headers) throws URISyntaxException {
String host = headers.getFirst(HttpHeaderNames.HOST);
if (host == null) {
return new Pair<>(null, -1);
}
try {
// attempt to use default URI parsing - this can fail when not strictly following RFC2396,
// for example, having underscores in host names will fail parsing
URI uri = new URI(/* scheme= */ null, host, /* path= */ null, /* query= */ null, /* fragment= */ null);
if (uri.getHost() != null) {
return new Pair<>(uri.getHost(), uri.getPort());
}
} catch (URISyntaxException e) {
LOG.debug("URI parsing failed", e);
}
if (STRICT_HOST_HEADER_VALIDATION.get()) {
throw new URISyntaxException(host, "Invalid host");
}
// fallback to using a colon split
// valid IPv6 addresses would have been handled already so any colon is safely assumed a port separator
String[] components = host.split(":");
if (components.length > 2) {
// handle case with unbracketed IPv6 addresses
return new Pair<>(null, -1);
}
String parsedHost = components[0];
int parsedPort = -1;
if (components.length > 1) {
try {
parsedPort = Integer.parseInt(components[1]);
} catch (NumberFormatException e) {
// ignore failing to parse port numbers and fallback to default port
LOG.debug("Parsing of host port component failed", e);
}
}
return new Pair<>(parsedHost, parsedPort);
}
/**
* Attempt to reconstruct the full URI that the client used.
*
* @return String
*/
@Override
public String reconstructURI() {
// If this instance is immutable, then lazy-cache reconstructing the uri.
if (immutable) {
if (reconstructedUri == null) {
reconstructedUri = _reconstructURI();
}
return reconstructedUri;
} else {
return _reconstructURI();
}
}
protected String _reconstructURI() {
try {
StringBuilder uri = new StringBuilder(100);
String scheme = getOriginalScheme().toLowerCase();
uri.append(scheme);
uri.append(URI_SCHEME_SEP).append(getOriginalHost());
int port = getOriginalPort();
if ((URI_SCHEME_HTTP.equals(scheme) && 80 == port) || (URI_SCHEME_HTTPS.equals(scheme) && 443 == port)) {
// Don't need to include port.
} else {
uri.append(':').append(port);
}
uri.append(getPathAndQuery());
return uri.toString();
} catch (IllegalArgumentException e) {
// This is not really so bad, just debug log it and move on.
LOG.debug("Error reconstructing request URI!", e);
return "";
} catch (Exception e) {
LOG.error("Error reconstructing request URI!", e);
return "";
}
}
@Override
public String toString() {
return "HttpRequestMessageImpl{" + "immutable="
+ immutable + ", message="
+ message + ", protocol='"
+ protocol + '\'' + ", method='"
+ method + '\'' + ", path='"
+ path + '\'' + ", queryParams="
+ queryParams + ", clientIp='"
+ clientIp + '\'' + ", scheme='"
+ scheme + '\'' + ", port="
+ port + ", serverName='"
+ serverName + '\'' + ", inboundRequest="
+ inboundRequest + ", parsedCookies="
+ parsedCookies + ", reconstructedUri='"
+ reconstructedUri + '\'' + ", pathAndQuery='"
+ pathAndQuery + '\'' + ", originalHost='"
+ originalHost + '\'' + ", infoForLogging='"
+ infoForLogging + '\'' + '}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy