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 committed Jan 15, 2018
1 parent ffd10eb commit e5a729c
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 3 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.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];
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

0 comments on commit e5a729c

Please sign in to comment.