Skip to content

Commit

Permalink
Add support for Forwarded / X-Forwarded-* headers
Browse files Browse the repository at this point in the history
This commit adds new information to the `HttpServerRequest`:
* the host (server) address
* the remote (client) address
* the scheme used by the current request

This information can be either derived from the current channel, or
extracted from the incoming HTTP request headers using "Forwarded"
or "X-Forwarded-*".

This feature is opt-in, and must be configured during server setup:

  HttpServer.create().forwarded().port(8080);

Closes gh-220
  • Loading branch information
bclozel authored and violetagg committed Jan 15, 2018
1 parent aa391eb commit a8c9b24
Show file tree
Hide file tree
Showing 5 changed files with 497 additions and 6 deletions.
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;

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.tcp.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];
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;
}
}
46 changes: 45 additions & 1 deletion src/main/java/reactor/ipc/netty/http/server/HttpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,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(FORWARDED_ATTR_CONFIG);
}

/**
* Enable GZip response compression if the client request presents accept encoding
* headers
Expand Down Expand Up @@ -225,6 +235,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(FORWARDED_ATTR_DISABLE);
}

/**
* Apply {@link ServerBootstrap} configuration given mapper taking currently
* configured one and returning a new one to be ultimately used for socket binding.
Expand Down Expand Up @@ -277,7 +297,20 @@ protected TcpServer tcpConfiguration() {
@Nullable
@Override
public ChannelOperations<?, ?> create(Connection c, ConnectionEvents listener, Object msg) {
return HttpServerOperations.bindHttp(c, listener, msg);
return HttpServerOperations.bindHttp(c, listener, msg, false);
}

@Override
public boolean createOnConnected() {
return false;
}
};

static final ChannelOperations.OnSetup HTTP_OPS_FORWARDED = new ChannelOperations.OnSetup() {
@Nullable
@Override
public ChannelOperations<?, ?> create(Connection c, ConnectionEvents listener, Object msg) {
return HttpServerOperations.bindHttp(c, listener, msg, true);
}

@Override
Expand All @@ -294,6 +327,11 @@ public boolean createOnConnected() {
return b;
};

static final Function<ServerBootstrap, ServerBootstrap> HTTP_OPS_FORWARDED_CONF = b -> {
BootstrapHandlers.channelOperationFactory(b, HTTP_OPS_FORWARDED);
return b;
};

static final TcpServer DEFAULT_TCP_SERVER = TcpServer.create()
.bootstrap(HTTP_OPS_CONF)
.port(DEFAULT_PORT);
Expand All @@ -306,4 +344,10 @@ public boolean createOnConnected() {

static final Function<TcpServer, TcpServer> COMPRESS_ATTR_DISABLE =
tcp -> tcp.selectorAttr(HttpServerBind.PRODUCE_GZIP, null);

static final Function<TcpServer, TcpServer> FORWARDED_ATTR_CONFIG =
tcp -> tcp.bootstrap(HTTP_OPS_FORWARDED_CONF);

static final Function<TcpServer, TcpServer> FORWARDED_ATTR_DISABLE =
tcp -> tcp.bootstrap(HTTP_OPS_CONF);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,11 +26,13 @@
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.annotation.Nullable;

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 All @@ -48,6 +51,7 @@
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.util.AsciiString;
import io.netty.util.AttributeKey;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand All @@ -74,9 +78,9 @@ class HttpServerOperations extends HttpOperations<HttpServerRequest, HttpServerR

@SuppressWarnings("unchecked")
static HttpServerOperations bindHttp(Connection connection, ConnectionEvents listener,
Object msg) {
Object msg, boolean forwarded) {
HttpServerOperations ops =
new HttpServerOperations(connection, listener, (HttpRequest) msg);
new HttpServerOperations(connection, listener, (HttpRequest) msg, forwarded);

listener.onStart(ops);

Expand All @@ -87,12 +91,14 @@ static HttpServerOperations bindHttp(Connection connection, ConnectionEvents lis
final HttpHeaders responseHeaders;
final Cookies cookieHolder;
final HttpRequest nettyRequest;
final ConnectionInfo connectionInfo;

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

HttpServerOperations(HttpServerOperations replaced) {
super(replaced);
this.cookieHolder = replaced.cookieHolder;
this.connectionInfo = replaced.connectionInfo;
this.responseHeaders = replaced.responseHeaders;
this.nettyResponse = replaced.nettyResponse;
this.paramsResolver = replaced.paramsResolver;
Expand All @@ -101,15 +107,20 @@ static HttpServerOperations bindHttp(Connection connection, ConnectionEvents lis

HttpServerOperations(Connection c,
ConnectionEvents listener,
HttpRequest nettyRequest) {
HttpRequest nettyRequest,
boolean forwarded) {
super(c, listener);
this.nettyRequest = Objects.requireNonNull(nettyRequest, "nettyRequest");
this.nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
this.responseHeaders = nettyResponse.headers();
this.cookieHolder = Cookies.newServerRequestHolder(requestHeaders());
if (forwarded) {
this.connectionInfo = ConnectionInfo.newForwardedConnectionInfo(this, (SocketChannel) channel());
}
else {
this.connectionInfo = ConnectionInfo.newConnectionInfo(this, (SocketChannel) channel());
}
chunkedTransfer(true);


}

@Override
Expand Down Expand Up @@ -259,6 +270,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 +288,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
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

0 comments on commit a8c9b24

Please sign in to comment.