From 1e86c50f6d147beaa43a005184b12e06146e3e54 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 20 Oct 2023 15:37:01 -0400 Subject: [PATCH] Make X-Forwarded-For and X-Forwarded-Port available as request headers when using proxy protocol. --- .../testing/junit5/HexStringDecoder.java | 3 +++ .../webserver/http2/Http2Connection.java | 15 +++++++++++++++ .../webserver/tests/ProxyProtocolTest.java | 5 ++++- .../helidon/webserver/ConnectionContext.java | 3 +-- .../helidon/webserver/ProxyProtocolData.java | 4 ++-- .../webserver/ProxyProtocolHandler.java | 6 +++--- .../helidon/webserver/http/ServerRequest.java | 3 +-- .../webserver/http1/Http1Connection.java | 18 ++++++++++++++++++ .../webserver/ProxyProtocolHandlerTest.java | 9 ++++----- 9 files changed, 51 insertions(+), 15 deletions(-) diff --git a/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java b/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java index 725f8084fc3..04c174a133e 100644 --- a/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java +++ b/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java @@ -15,6 +15,9 @@ */ package io.helidon.common.testing.junit5; +/** + * Utility class to decode hex strings. + */ public final class HexStringDecoder { private HexStringDecoder() { diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java index c1947c521d5..172f1d4d8f9 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java @@ -64,6 +64,8 @@ import io.helidon.webserver.http2.spi.Http2SubProtocolSelector; import io.helidon.webserver.spi.ServerConnection; +import static io.helidon.http.HeaderNames.X_FORWARDED_FOR; +import static io.helidon.http.HeaderNames.X_FORWARDED_PORT; import static io.helidon.http.HeaderNames.X_HELIDON_CN; import static io.helidon.http.http2.Http2Util.PREFACE_LENGTH; import static java.lang.System.Logger.Level.DEBUG; @@ -614,6 +616,19 @@ private void doHeaders(Semaphore requestSemaphore) { ctx.remotePeer().tlsCertificates() .flatMap(TlsUtils::parseCn) .ifPresent(cn -> connectionHeaders.add(X_HELIDON_CN, cn)); + + // proxy protocol related headers X-Forwarded-For and X-Forwarded-Port + ctx.proxyProtocolData().ifPresent(proxyProtocolData -> { + String sourceAddress = proxyProtocolData.sourceAddress(); + if (!sourceAddress.isEmpty()) { + connectionHeaders.add(X_FORWARDED_FOR, sourceAddress); + } + int sourcePort = proxyProtocolData.sourcePort(); + if (sourcePort != -1) { + connectionHeaders.set(X_FORWARDED_PORT, sourcePort); + } + }); + initConnectionHeaders = false; } diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java index 9439edae9f6..1b1af17cfd2 100644 --- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java @@ -16,6 +16,7 @@ package io.helidon.webserver.tests; import io.helidon.common.testing.http.junit5.SocketHttpClient; +import io.helidon.http.HeaderNames; import io.helidon.http.Method; import io.helidon.http.Status; import io.helidon.webserver.ProxyProtocolData; @@ -57,7 +58,9 @@ static void routing(HttpRules routing) { && data.sourceAddress().equals("192.168.0.1") && data.destAddress().equals("192.168.0.11") && data.sourcePort() == 56324 - && data.destPort() == 443) { + && data.destPort() == 443 + && "192.168.0.1".equals(req.headers().first(HeaderNames.X_FORWARDED_FOR).orElse(null)) + && "56324".equals(req.headers().first(HeaderNames.X_FORWARDED_PORT).orElse(null))) { res.status(Status.OK_200).send(); return; } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java index 67a2d349176..1c14c051e91 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java @@ -22,7 +22,6 @@ import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; import io.helidon.common.socket.SocketContext; -import io.helidon.common.socket.SocketOptions; /** * Server connection context. @@ -67,7 +66,7 @@ public interface ConnectionContext extends SocketContext { * Proxy protocol header data. * * @return protocol header data if proxy protocol is enabled on socket - * @see SocketOptions#enableProxyProtocol() + * @see ListenerConfig#enableProxyProtocol() */ default Optional proxyProtocolData() { return Optional.empty(); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java index 4d2ba1ac566..dedcc71812e 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java @@ -101,14 +101,14 @@ static Protocol fromString(String s) { /** * Source address that is either IP4 or IP6 depending on {@link #family()}. * - * @return source address + * @return source address or {@code ""} if not provided */ String sourceAddress(); /** * Destination address that is either IP4 or IP46 depending on {@link #family()}. * - * @return source address + * @return source address or (@code ""} if not provided */ String destAddress(); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java index 6c081d3e186..70fa610a060 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -116,7 +116,7 @@ static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throw // special case for just UNKNOWN family if (family == ProxyProtocolData.Family.UNKNOWN) { return new ProxyProtocolDataImpl(Family.UNKNOWN, Protocol.UNKNOWN, - null, null, -1, -1); + "", "", -1, -1); } } @@ -178,8 +178,8 @@ static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throw int headerLength = ((b << 8) & 0xFF00) | (readNext(inputStream) & 0xFF); // decode addresses and ports - String sourceAddress = null; - String destAddress = null; + String sourceAddress = ""; + String destAddress = ""; int sourcePort = -1; int destPort = -1; switch (family) { diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerRequest.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerRequest.java index cb3b84bc455..deb358e15ca 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerRequest.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerRequest.java @@ -21,7 +21,6 @@ import java.util.function.UnaryOperator; import io.helidon.common.context.Context; -import io.helidon.common.socket.SocketOptions; import io.helidon.http.RoutedPath; import io.helidon.http.media.ReadableEntity; import io.helidon.webserver.ListenerContext; @@ -118,7 +117,7 @@ public interface ServerRequest extends HttpRequest { * Access proxy protocol data for the connection on which this request was sent. * * @return proxy protocol data, if available - * @see SocketOptions#enableProxyProtocol() + * @see io.helidon.webserver.ListenerConfig#enableProxyProtocol() */ Optional proxyProtocolData(); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java index 4496805b7cb..cb8e5dc572e 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java @@ -47,11 +47,14 @@ import io.helidon.http.encoding.ContentEncodingContext; import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ProxyProtocolData; import io.helidon.webserver.http.DirectTransportRequest; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.http1.spi.Http1Upgrader; import io.helidon.webserver.spi.ServerConnection; +import static io.helidon.http.HeaderNames.X_FORWARDED_FOR; +import static io.helidon.http.HeaderNames.X_FORWARDED_PORT; import static io.helidon.http.HeaderNames.X_HELIDON_CN; import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.WARNING; @@ -128,6 +131,9 @@ public boolean canInterrupt() { public void handle(Semaphore requestSemaphore) throws InterruptedException { this.myThread = Thread.currentThread(); try { + // look for protocol data + ProxyProtocolData proxyProtocolData = ctx.proxyProtocolData().orElse(null); + // handle connection until an exception (or explicit connection close) while (canRun) { // prologue (first line of request) @@ -145,6 +151,18 @@ public void handle(Semaphore requestSemaphore) throws InterruptedException { .ifPresent(name -> headers.set(X_HELIDON_CN, name)); recvListener.headers(ctx, headers); + // proxy protocol related headers X-Forwarded-For and X-Forwarded-Port + if (proxyProtocolData != null) { + String sourceAddress = proxyProtocolData.sourceAddress(); + if (!sourceAddress.isEmpty()) { + headers.add(X_FORWARDED_FOR, sourceAddress); + } + int sourcePort = proxyProtocolData.sourcePort(); + if (sourcePort != -1) { + headers.add(X_FORWARDED_PORT, sourcePort); + } + } + if (canUpgrade) { if (headers.contains(HeaderNames.UPGRADE)) { Http1Upgrader upgrader = upgradeProviderMap.get(headers.get(HeaderNames.UPGRADE).get()); diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java index fdfebbce7ba..988add3c9d8 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString; @@ -53,8 +52,8 @@ void unknownV1Test() throws IOException { new ByteArrayInputStream(header.getBytes(StandardCharsets.US_ASCII)))); assertThat(data.family(), is(ProxyProtocolData.Family.UNKNOWN)); assertThat(data.protocol(), is(ProxyProtocolData.Protocol.UNKNOWN)); - assertThat(data.sourceAddress(), nullValue()); - assertThat(data.destAddress(), nullValue()); + assertThat(data.sourceAddress(), is("")); + assertThat(data.destAddress(), is("")); assertThat(data.sourcePort(), is(-1)); assertThat(data.destPort(), is(-1)); } @@ -133,8 +132,8 @@ void unknownV2Test() throws IOException { new ByteArrayInputStream(decodeHexString(header)))); assertThat(data.family(), is(ProxyProtocolData.Family.UNKNOWN)); assertThat(data.protocol(), is(ProxyProtocolData.Protocol.UNKNOWN)); - assertThat(data.sourceAddress(), nullValue()); - assertThat(data.destAddress(), nullValue()); + assertThat(data.sourceAddress(), is("")); + assertThat(data.destAddress(), is("")); assertThat(data.sourcePort(), is(-1)); assertThat(data.destPort(), is(-1)); }