Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Forwarded / X-Forwarded-* headers #249

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions src/main/java/reactor/ipc/netty/http/server/ConnectionInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved.
*
* 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 reactor.ipc.netty.http.server;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add copyright header


import java.net.InetSocketAddress;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.netty.channel.socket.SocketChannel;
import io.netty.handler.ssl.SslHandler;
import reactor.ipc.netty.options.InetSocketAddressUtil;

/**
* Resolve information about the current connection, including the
* host (server) address, the remote (client) address and the scheme.
*
* <p>Depending on the chosen factory method, the information
* can be retrieved directly from the channel or additionally
* using the {@code "Forwarded"}, or {@code "X-Forwarded-*"}
* HTTP request headers.
*
* @author Brian Clozel
* @since 0.8
* @see <a href="https://tools.ietf.org/html/rfc7239">rfc7239</a>
*/
public final class ConnectionInfo {

private static final String FORWARDED_HEADER = "Forwarded";
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("for=\"?([^;,\"]+)\"?");

private static final String XFORWARDED_IP_HEADER = "X-Forwarded-For";
private static final String XFORWARDED_HOST_HEADER = "X-Forwarded-Host";
private static final String XFORWARDED_PORT_HEADER = "X-Forwarded-Port";
private static final String XFORWARDED_PROTO_HEADER = "X-Forwarded-Proto";

private final InetSocketAddress hostAddress;

private final InetSocketAddress remoteAddress;

private final String scheme;

/**
* Retrieve the connection information from the current connection directly
* @param request the current server request
* @param channel the current channel
* @return the connection information
*/
public static ConnectionInfo newConnectionInfo(HttpServerRequest request, SocketChannel channel) {
InetSocketAddress hostAddress = channel.localAddress();
InetSocketAddress remoteAddress = channel.remoteAddress();
String scheme = channel.pipeline().get(SslHandler.class) != null ? "https" : "http";
return new ConnectionInfo(hostAddress, remoteAddress, scheme);
}

/**
* Retrieve the connection information from the {@code "Forwarded"}/{@code "X-Forwarded-*"}
* HTTP request headers, or from the current connection directly if none are found.
* @param request the current server request
* @param channel the current channel
* @return the connection information
*/
public static ConnectionInfo newForwardedConnectionInfo(HttpServerRequest request, SocketChannel channel) {
if (request.requestHeaders().contains(FORWARDED_HEADER)) {
return parseForwardedInfo(request, channel);
}
else {
return parseXForwardedInfo(request, channel);
}
}

private static ConnectionInfo parseForwardedInfo(HttpServerRequest request, SocketChannel channel) {
InetSocketAddress hostAddress = channel.localAddress();
InetSocketAddress remoteAddress = channel.remoteAddress();
String scheme = channel.pipeline().get(SslHandler.class) != null ? "https" : "http";

String forwarded = request.requestHeaders().get(FORWARDED_HEADER).split(",")[0];
Copy link
Member

@simonbasle simonbasle Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is splitting on the comma , character ok? quoted strings in header values are allowed to contain commas BUT in this particular case:

  • proto= definition is same as URI scheme (no commas allowed 👍 )
  • for= definition is that of a Node identifier (no commas allowed 👍 )
  • host= definition is that of the Host header field / HTTP URI. This one is a bit more complex, but I don't think unencoded commas can appear (🤔)

So this should be ok, but might want to dig into the host one.

Copy link
Member Author

@bclozel bclozel Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See rfc7239 section 5.3 and rfc7230 section 3.2.6.

, is part of the US-ASCII delimiters, so not allowed there AFAIU.

Copy link
Member

@simonbasle simonbasle Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's for the token part (here host), but the value can be a quoted string, which may contain characters in the %x23-5B range (including the comma %x2C), in addition to %x21 (!) but not %x22 (double quotes)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it - isn't that encoded still at that point?
I just tried sending a Forward: for="exa%x2Cmple.com" header and serverRequest.remoteAddress().getHostString() does return "exa%x2Cmple.com". Do you have a test case in mind that reproduces the issue?

Copy link
Member

@simonbasle simonbasle Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, just had a gut feeling splitting on , might be "too simple". I have thus briefly looked at the RFC, which I have a limited understanding of (since I don't deal with as often as you guys ;) )

from what I understand of the ABNF notation used in the RFC, %x2C means "a literal comma", not "a comma encoded as %x2C" [1] (so that might be were I'm interpreting things wrong).

rfc7230 section 3.2.6 defines the quoted string qdtext with such a range: %x23-5B (which I assume would mean "the literal characters between # an [, including A-Z and the comma.

if that's working well enough for SPR, then that's probably good 👍 just challenging the assumptions a bit.

[1] see ABNF rfc section 3.4

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may be right, actually.
At the same time it seems commas aren't allowed in hostnames (in other RFCs) and it looks like Tomcat is doing the same: https://github.com/apache/tomcat/blob/trunk/java/org/apache/catalina/valves/RemoteIpValve.java#L354

Matcher hostMatcher = FORWARDED_HOST_PATTERN.matcher(forwarded);
if (hostMatcher.find()) {
hostAddress = parseAddress(hostMatcher.group(1), hostAddress.getPort());
}
Matcher protoMatcher = FORWARDED_PROTO_PATTERN.matcher(forwarded);
if (protoMatcher.find()) {
scheme = protoMatcher.group(1).trim();
}
Matcher forMatcher = FORWARDED_FOR_PATTERN.matcher(forwarded);
if(forMatcher.find()) {
remoteAddress = parseAddress(forMatcher.group(1).trim(), remoteAddress.getPort());
}
return new ConnectionInfo(hostAddress, remoteAddress, scheme);
}

private static InetSocketAddress parseAddress(String address, int defaultPort) {
int portSeparatorIdx = address.lastIndexOf(":");
if (portSeparatorIdx > address.lastIndexOf("]")) {
return InetSocketAddressUtil.createUnresolved(address.substring(0, portSeparatorIdx),
Integer.parseInt(address.substring(portSeparatorIdx + 1)));
}
else {
return InetSocketAddressUtil.createUnresolved(address, defaultPort);
}
}

private static ConnectionInfo parseXForwardedInfo(HttpServerRequest request, SocketChannel channel) {
InetSocketAddress hostAddress = channel.localAddress();
InetSocketAddress remoteAddress = channel.remoteAddress();
String scheme = channel.pipeline().get(SslHandler.class) != null ? "https" : "http";
if (request.requestHeaders().contains(XFORWARDED_IP_HEADER)) {
String hostValue = request.requestHeaders().get(XFORWARDED_IP_HEADER).split(",")[0];
hostAddress = parseAddress(hostValue, hostAddress.getPort());
}
else if(request.requestHeaders().contains(XFORWARDED_HOST_HEADER)) {
if(request.requestHeaders().contains(XFORWARDED_PORT_HEADER)) {
hostAddress = InetSocketAddressUtil.createUnresolved(
request.requestHeaders().get(XFORWARDED_HOST_HEADER).split(",")[0].trim(),
Integer.parseInt(request.requestHeaders().get(XFORWARDED_PORT_HEADER).split(",")[0].trim()));
}
else {
hostAddress = InetSocketAddressUtil.createUnresolved(
request.requestHeaders().get(XFORWARDED_HOST_HEADER).split(",")[0].trim(),
channel.localAddress().getPort());
}
}
if (request.requestHeaders().contains(XFORWARDED_PROTO_HEADER)) {
scheme = request.requestHeaders().get(XFORWARDED_PROTO_HEADER).trim();
}
return new ConnectionInfo(hostAddress, remoteAddress, scheme);
}

private ConnectionInfo(InetSocketAddress hostAddress, InetSocketAddress remoteAddress, String scheme) {
this.hostAddress = hostAddress;
this.remoteAddress = remoteAddress;
this.scheme = scheme;
}

public InetSocketAddress getHostAddress() {
return hostAddress;
}

public InetSocketAddress getRemoteAddress() {
return remoteAddress;
}

public String getScheme() {
return scheme;
}
}
20 changes: 20 additions & 0 deletions src/main/java/reactor/ipc/netty/http/server/HttpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ public final HttpServer compress() {
return tcpConfiguration(COMPRESS_ATTR_CONFIG);
}

/**
* Enable support for the {@code "Forwarded"} and {@code "X-Forwarded-*"}
* HTTP request headers for deriving information about the connection.
*
* @return a new {@link HttpServer}
*/
public final HttpServer forwarded() {
return tcpConfiguration(tcp -> tcp.attr(HttpServerOperations.USE_FORWARDED, true));
}

/**
* Enable GZip response compression if the client request presents accept encoding
* headers
Expand Down Expand Up @@ -235,6 +245,16 @@ public final HttpServer noCompression() {
return tcpConfiguration(COMPRESS_ATTR_DISABLE);
}

/**
* Disable support for the {@code "Forwarded"} and {@code "X-Forwarded-*"}
* HTTP request headers.
*
* @return a new {@link HttpServer}
*/
public final HttpServer noForwarded() {
return tcpConfiguration(tcp -> tcp.attr(HttpServerOperations.USE_FORWARDED, false));
}

/**
* Apply {@link ServerBootstrap} configuration given mapper taking currently
* configured one and returning a new one to be ultimately used for socket binding.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved.
* Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@
package reactor.ipc.netty.http.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
Expand All @@ -25,12 +26,14 @@
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.annotation.Nullable;

import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
Expand Down Expand Up @@ -86,12 +89,14 @@ static HttpServerOperations bindHttp(Channel channel,
final HttpHeaders responseHeaders;
final Cookies cookieHolder;
final HttpRequest nettyRequest;
final ConnectionInfo connectionInfo;

Function<? super String, Map<String, String>> paramsResolver;

HttpServerOperations(Channel ch, HttpServerOperations replaced) {
super(ch, replaced);
this.cookieHolder = replaced.cookieHolder;
this.connectionInfo = replaced.connectionInfo;
this.responseHeaders = replaced.responseHeaders;
this.nettyResponse = replaced.nettyResponse;
this.paramsResolver = replaced.paramsResolver;
Expand All @@ -107,9 +112,13 @@ static HttpServerOperations bindHttp(Channel channel,
this.nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
this.responseHeaders = nettyResponse.headers();
this.cookieHolder = Cookies.newServerRequestHolder(requestHeaders());
if (ch.hasAttr(USE_FORWARDED) && ch.attr(USE_FORWARDED).get()) {
this.connectionInfo = ConnectionInfo.newForwardedConnectionInfo(this, (SocketChannel) channel());
}
else {
this.connectionInfo = ConnectionInfo.newConnectionInfo(this, (SocketChannel) channel());
}
chunkedTransfer(true);


}

@Override
Expand Down Expand Up @@ -259,6 +268,16 @@ public Flux<?> receiveObject() {
}
}

@Override
public InetSocketAddress hostAddress() {
return this.connectionInfo.getHostAddress();
}

@Override
public InetSocketAddress remoteAddress() {
return this.connectionInfo.getRemoteAddress();
}

@Override
public HttpHeaders requestHeaders() {
if (nettyRequest != null) {
Expand All @@ -267,6 +286,11 @@ public HttpHeaders requestHeaders() {
throw new IllegalStateException("request not parsed");
}

@Override
public String scheme() {
return this.connectionInfo.getScheme();
}

@Override
public HttpHeaders responseHeaders() {
return responseHeaders;
Expand Down Expand Up @@ -503,4 +527,6 @@ protected void handleOutboundWithNoContent() {
}

static final AttributeKey<Integer> PRODUCE_GZIP = AttributeKey.newInstance("produceGzip");

static final AttributeKey<Boolean> USE_FORWARDED = AttributeKey.newInstance("useForwarded");
}
22 changes: 22 additions & 0 deletions src/main/java/reactor/ipc/netty/http/server/HttpServerRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package reactor.ipc.netty.http.server;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -77,11 +78,32 @@ default Flux<HttpContent> receiveContent() {
return receiveObject().ofType(HttpContent.class);
}

/**
* Return the address of the host peer.
*
* @return the host's address
*/
InetSocketAddress hostAddress();

/**
* Return the address of the remote peer.
*
* @return the peer's address
*/
InetSocketAddress remoteAddress();

/**
* Return inbound {@link HttpHeaders}
*
* @return inbound {@link HttpHeaders}
*/
HttpHeaders requestHeaders();

/**
* Return the current scheme
*
* @return the protocol scheme
*/
String scheme();

}
Loading