From 061464835c0803061cc40b1539fc3ef01245f2ce Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 18 Oct 2023 10:07:02 -0400 Subject: [PATCH 1/8] Initial support for proxy protocol data V1 and V2. Minor changes to API to make protocol data available to application handlers. --- .../common/socket/SocketOptionsBlueprint.java | 9 + .../webserver/http2/Http2ServerRequest.java | 7 + .../helidon/webserver/ConnectionContext.java | 11 + .../helidon/webserver/ConnectionHandler.java | 18 +- .../helidon/webserver/ProxyProtocolData.java | 79 +++++++ .../webserver/ProxyProtocolHandler.java | 194 ++++++++++++++++++ .../io/helidon/webserver/ServerListener.java | 3 +- .../helidon/webserver/http/ServerRequest.java | 11 + .../webserver/http1/Http1ServerRequest.java | 7 + .../webserver/ProxyProtocolHandlerTest.java | 76 +++++++ 10 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java create mode 100644 webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java create mode 100644 webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java index c487c778a29..f0e74e9b246 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java @@ -109,6 +109,15 @@ interface SocketOptionsBlueprint { @ConfiguredOption("false") boolean tcpNoDelay(); + /** + * Enable support for proxy protocol for this socket. + * Default is {@code false}. + * + * @return proxy support status + */ + @ConfiguredOption("false") + boolean enableProxyProtocol(); + /** * Configure socket with defined socket options. * diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java index cc556612d88..97fac2aefeb 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java @@ -18,6 +18,7 @@ import java.io.InputStream; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -38,6 +39,7 @@ import io.helidon.http.media.ReadableEntity; import io.helidon.webserver.ConnectionContext; import io.helidon.webserver.ListenerContext; +import io.helidon.webserver.ProxyProtocolData; import io.helidon.webserver.http.HttpSecurity; import io.helidon.webserver.http.RoutingRequest; @@ -220,6 +222,11 @@ public void streamFilter(UnaryOperator filterFunction) { this.streamFilter = it -> filterFunction.apply(current.apply(it)); } + @Override + public Optional proxyProtocolData() { + return Optional.ofNullable(ctx.proxyProtocolData()); + } + private UriInfo createUriInfo() { return ctx.listenerContext().config().requestedUriDiscoveryContext().uriInfo(remotePeer().address().toString(), localPeer().address().toString(), 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 bbca8281788..681553144bb 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java @@ -21,6 +21,7 @@ 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. @@ -60,4 +61,14 @@ public interface ConnectionContext extends SocketContext { * @return rouer */ Router router(); + + /** + * Proxy protocol header data. + * + * @return header data or {@code null} if proxy protocol not enabled on socket + * @see SocketOptions#enableProxyProtocol() + */ + default ProxyProtocolData proxyProtocolData() { + return null; + } } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index c4bb7ab11b0..3d2027568c8 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -33,6 +33,7 @@ import io.helidon.common.socket.HelidonSocket; import io.helidon.common.socket.PeerInfo; import io.helidon.common.socket.PlainSocket; +import io.helidon.common.socket.SocketOptions; import io.helidon.common.socket.SocketWriter; import io.helidon.common.socket.TlsSocket; import io.helidon.common.task.InterruptableTask; @@ -64,11 +65,13 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { private final String serverChannelId; private final Router router; private final Tls tls; + private final SocketOptions connectionOptions; private ServerConnection connection; private HelidonSocket helidonSocket; private DataReader reader; private SocketWriter writer; + private ProxyProtocolData proxyProtocolData; ConnectionHandler(ListenerContext listenerContext, Semaphore connectionSemaphore, @@ -78,7 +81,8 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { Socket socket, String serverChannelId, Router router, - Tls tls) { + Tls tls, + SocketOptions connectionOptions) { this.listenerContext = listenerContext; this.connectionSemaphore = connectionSemaphore; this.requestSemaphore = requestSemaphore; @@ -89,6 +93,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { this.serverChannelId = serverChannelId; this.router = router; this.tls = tls; + this.connectionOptions = connectionOptions; } @Override @@ -100,6 +105,12 @@ public boolean canInterrupt() { public final void run() { String channelId = "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket)); + // proxy protocol before SSL handshake + if (connectionOptions.enableProxyProtocol()) { + ProxyProtocolHandler handler = new ProxyProtocolHandler(socket, channelId); + proxyProtocolData = handler.get(); + } + // handle SSL and init helidonSocket, reader and writer try { if (tls.enabled()) { @@ -226,6 +237,11 @@ public Router router() { return router; } + @Override + public ProxyProtocolData proxyProtocolData() { + return proxyProtocolData; + } + private ServerConnection identifyConnection() { try { reader.ensureAvailable(); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java new file mode 100644 index 00000000000..25201ede9b2 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver; + +/** + * Proxy protocol data parsed by {@link ProxyProtocolHandler}. + */ +public interface ProxyProtocolData { + + /** + * The protocol family options. + */ + enum ProtocolFamily { + /** + * TCP version 4. + */ + TCP4, + + /** + * TCP version 6. + */ + TCP6, + + /** + * Protocol family is unknown. + */ + UNKNOWN + } + + /** + * Protocol family from protocol header. + * + * @return protocol family + */ + ProtocolFamily protocolFamily(); + + /** + * Source address that is either IPv4 or IPv6 depending on {@link #protocolFamily()}}. + * + * @return source address + */ + String sourceAddress(); + + /** + * Destination address that is either IPv4 or IPv6 depending on {@link #protocolFamily()}}. + * + * @return source address + */ + String destAddress(); + + /** + * Source port number. + * + * @return source port. + */ + int sourcePort(); + + /** + * Destination port number. + * + * @return port number. + */ + int destPort(); +} + + diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java new file mode 100644 index 00000000000..138bd8115a2 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver; + +import java.io.IOException; +import java.io.PushbackInputStream; +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.net.Socket; +import java.util.Arrays; +import java.util.function.Supplier; + +import io.helidon.http.DirectHandler; +import io.helidon.http.RequestException; + +class ProxyProtocolHandler implements Supplier { + private static final System.Logger LOGGER = System.getLogger(ProxyProtocolHandler.class.getName()); + + private static final int MAX_V1_FIELD_LENGTH = 40; + + static final byte[] V1_PREFIX = { + (byte) 'P', + (byte) 'R', + (byte) 'O', + (byte) 'X', + (byte) 'Y', + }; + + static final byte[] V2_PREFIX = { + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x00, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x51, + (byte) 0x55, + (byte) 0x49, + (byte) 0x54, + (byte) 0x0A + }; + + static final RequestException BAD_PROTOCOL_EXCEPTION = RequestException.builder() + .type(DirectHandler.EventType.OTHER) + .message("Unable to parse proxy protocol header") + .build(); + + private final Socket socket; + private final String channelId; + + ProxyProtocolHandler(Socket socket, String channelId) { + this.socket = socket; + this.channelId = channelId; + } + + @Override + public ProxyProtocolData get() { + LOGGER.log(Level.DEBUG, "Reading proxy protocol data for channel %s", channelId); + + try { + byte[] prefix = new byte[V1_PREFIX.length]; + PushbackInputStream inputStream = new PushbackInputStream(socket.getInputStream(), 1); + int n = inputStream.read(prefix); + if (n < V1_PREFIX.length) { + throw BAD_PROTOCOL_EXCEPTION; + } + if (arrayEquals(prefix, V1_PREFIX, V1_PREFIX.length)) { + return handleV1Protocol(inputStream); + } else if (arrayEquals(prefix, V2_PREFIX, V1_PREFIX.length)) { + return handleV2Protocol(inputStream); + } else { + throw BAD_PROTOCOL_EXCEPTION; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throws IOException { + try { + int n; + byte[] buffer = new byte[MAX_V1_FIELD_LENGTH]; + + match(inputStream, (byte) ' '); + + // protocol and family + n = readUntil(inputStream, buffer, (byte) ' ', (byte) '\r'); + var family = ProxyProtocolData.ProtocolFamily.valueOf(new String(buffer, 0, n)); + byte b = (byte) inputStream.read(); + if (b == (byte) '\r') { + // special case for just UNKNOWN family + if (family == ProxyProtocolData.ProtocolFamily.UNKNOWN) { + return new ProxyProtocolDataImpl(ProxyProtocolData.ProtocolFamily.UNKNOWN, + null, null, -1, -1); + } + } + + match(b, (byte) ' '); + + // source address + n = readUntil(inputStream, buffer, (byte) ' '); + var sourceAddress = new String(buffer, 0, n); + match(inputStream, (byte) ' '); + + // destination address + n = readUntil(inputStream, buffer, (byte) ' '); + var destAddress = new String(buffer, 0, n); + match(inputStream, (byte) ' '); + + // source port + n = readUntil(inputStream, buffer, (byte) ' '); + int sourcePort = Integer.parseInt(new String(buffer, 0, n)); + match(inputStream, (byte) ' '); + + // destination port + n = readUntil(inputStream, buffer, (byte) '\r'); + int destPort = Integer.parseInt(new String(buffer, 0, n)); + match(inputStream, (byte) '\r'); + match(inputStream, (byte) '\n'); + + return new ProxyProtocolDataImpl(family, sourceAddress, destAddress, sourcePort, destPort); + } catch (IllegalArgumentException e) { + throw BAD_PROTOCOL_EXCEPTION; + } + } + + private static void match(byte a, byte b) { + if (a != b) { + throw BAD_PROTOCOL_EXCEPTION; + } + } + + private static void match(PushbackInputStream inputStream, byte b) throws IOException { + if (inputStream.read() != b) { + throw BAD_PROTOCOL_EXCEPTION; + } + } + + private static int readUntil(PushbackInputStream inputStream, byte[] buffer, byte... delims) throws IOException { + int n = 0; + do { + int b = inputStream.read(); + if (b < 0) { + throw BAD_PROTOCOL_EXCEPTION; + } + if (arrayContains(delims, (byte) b)) { + inputStream.unread(b); + return n; + } + buffer[n++] = (byte) b; + if (n >= buffer.length) { + throw BAD_PROTOCOL_EXCEPTION; + } + } while (true); + } + + static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throws IOException { + return null; + } + + private static boolean arrayEquals(byte[] array1, byte[] array2, int prefix) { + return Arrays.equals(array1, 0, prefix, array2, 0, prefix); + } + + private static boolean arrayContains(byte[] array, byte b) { + for (byte a : array) { + if (a == b) { + return true; + } + } + return false; + } + + record ProxyProtocolDataImpl(ProtocolFamily protocolFamily, + String sourceAddress, + String destAddress, + int sourcePort, + int destPort) implements ProxyProtocolData { + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java index c5e0761f7a1..7de4a5b6088 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java @@ -346,7 +346,8 @@ private void listen() { socket, serverChannelId, router, - tls); + tls, + connectionOptions); readerExecutor.execute(handler); } catch (RejectedExecutionException e) { LOGGER.log(ERROR, "Executor rejected handler for new connection"); 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 e9f6138e567..cb3b84bc455 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 @@ -17,12 +17,15 @@ package io.helidon.webserver.http; import java.io.InputStream; +import java.util.Optional; 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; +import io.helidon.webserver.ProxyProtocolData; /** * HTTP server request. @@ -110,4 +113,12 @@ public interface ServerRequest extends HttpRequest { * @param filterFunction the function to replace input stream of this request with a user provided one */ void streamFilter(UnaryOperator filterFunction); + + /** + * Access proxy protocol data for the connection on which this request was sent. + * + * @return proxy protocol data, if available + * @see SocketOptions#enableProxyProtocol() + */ + Optional proxyProtocolData(); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java index 0058084cdc2..9153b610bb1 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java @@ -16,6 +16,7 @@ package io.helidon.webserver.http1; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.function.Supplier; @@ -36,6 +37,7 @@ import io.helidon.http.encoding.ContentDecoder; import io.helidon.webserver.ConnectionContext; import io.helidon.webserver.ListenerContext; +import io.helidon.webserver.ProxyProtocolData; import io.helidon.webserver.http.HttpSecurity; import io.helidon.webserver.http.RoutingRequest; @@ -207,6 +209,11 @@ public UriInfo requestedUri() { return uriInfo.get(); } + @Override + public Optional proxyProtocolData() { + return Optional.ofNullable(ctx.proxyProtocolData()); + } + private UriInfo createUriInfo() { return ctx.listenerContext().config().requestedUriDiscoveryContext().uriInfo(remotePeer().address().toString(), localPeer().address().toString(), diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java new file mode 100644 index 00000000000..c3f889de27c --- /dev/null +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PushbackInputStream; +import java.nio.charset.StandardCharsets; + +import io.helidon.http.RequestException; +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; + +class ProxyProtocolHandlerTest { + + @Test + void basicV1Test() throws IOException { + String header = " TCP4 192.168.0.1 192.168.0.11 56324 443\r\n"; // excludes PROXY prefix + ProxyProtocolData data = ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header.getBytes(StandardCharsets.US_ASCII)))); + assertThat(data.protocolFamily(), is(ProxyProtocolData.ProtocolFamily.TCP4)); + assertThat(data.sourceAddress(), is("192.168.0.1")); + assertThat(data.destAddress(), is("192.168.0.11")); + assertThat(data.sourcePort(), is(56324)); + assertThat(data.destPort(), is(443)); + } + + @Test + void unknownV1Test() throws IOException { + String header = " UNKNOWN\r\n"; // excludes PROXY prefix + ProxyProtocolData data = ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header.getBytes(StandardCharsets.US_ASCII)))); + assertThat(data.protocolFamily(), is(ProxyProtocolData.ProtocolFamily.UNKNOWN)); + assertThat(data.sourceAddress(), nullValue()); + assertThat(data.destAddress(), nullValue()); + assertThat(data.sourcePort(), is(-1)); + assertThat(data.destPort(), is(-1)); + } + + @Test + void badV1Test() { + String header1 = " MYPROTOCOL 192.168.0.1 192.168.0.11 56324 443\r\n"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header1.getBytes(StandardCharsets.US_ASCII))))); + String header2 = " TCP4 192.168.0.1 192.168.0.11 56324\r\n"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header2.getBytes(StandardCharsets.US_ASCII))))); + String header3 = " TCP4 192.168.0.1 192.168.0.11 56324 443"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header3.getBytes(StandardCharsets.US_ASCII))))); + String header4 = " TCP4 192.168.0.1 56324 443\r\n"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( + new ByteArrayInputStream(header4.getBytes(StandardCharsets.US_ASCII))))); + } +} From 6028f66c57eb42ed262f15e22ed85ae2ed74ba6c Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 18 Oct 2023 11:41:25 -0400 Subject: [PATCH 2/8] Uses ListenerConfig for proxy protocol. Return Optional instead of null. --- .../common/socket/SocketOptionsBlueprint.java | 9 --------- .../helidon/webserver/http2/Http2ServerRequest.java | 2 +- .../io/helidon/webserver/ConnectionContext.java | 7 ++++--- .../io/helidon/webserver/ConnectionHandler.java | 13 +++++++------ .../helidon/webserver/ListenerConfigBlueprint.java | 9 +++++++++ .../java/io/helidon/webserver/ServerListener.java | 2 +- .../helidon/webserver/http1/Http1ServerRequest.java | 2 +- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java index f0e74e9b246..c487c778a29 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketOptionsBlueprint.java @@ -109,15 +109,6 @@ interface SocketOptionsBlueprint { @ConfiguredOption("false") boolean tcpNoDelay(); - /** - * Enable support for proxy protocol for this socket. - * Default is {@code false}. - * - * @return proxy support status - */ - @ConfiguredOption("false") - boolean enableProxyProtocol(); - /** * Configure socket with defined socket options. * diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java index 97fac2aefeb..ddb84736946 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerRequest.java @@ -224,7 +224,7 @@ public void streamFilter(UnaryOperator filterFunction) { @Override public Optional proxyProtocolData() { - return Optional.ofNullable(ctx.proxyProtocolData()); + return ctx.proxyProtocolData(); } private UriInfo createUriInfo() { 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 681553144bb..67a2d349176 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionContext.java @@ -16,6 +16,7 @@ package io.helidon.webserver; +import java.util.Optional; import java.util.concurrent.ExecutorService; import io.helidon.common.buffers.DataReader; @@ -65,10 +66,10 @@ public interface ConnectionContext extends SocketContext { /** * Proxy protocol header data. * - * @return header data or {@code null} if proxy protocol not enabled on socket + * @return protocol header data if proxy protocol is enabled on socket * @see SocketOptions#enableProxyProtocol() */ - default ProxyProtocolData proxyProtocolData() { - return null; + default Optional proxyProtocolData() { + return Optional.empty(); } } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index 3d2027568c8..9f16152e9bd 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -22,6 +22,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; @@ -65,7 +66,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { private final String serverChannelId; private final Router router; private final Tls tls; - private final SocketOptions connectionOptions; + private final ListenerConfig listenerConfig; private ServerConnection connection; private HelidonSocket helidonSocket; @@ -82,7 +83,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { String serverChannelId, Router router, Tls tls, - SocketOptions connectionOptions) { + ListenerConfig listenerConfig) { this.listenerContext = listenerContext; this.connectionSemaphore = connectionSemaphore; this.requestSemaphore = requestSemaphore; @@ -93,7 +94,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { this.serverChannelId = serverChannelId; this.router = router; this.tls = tls; - this.connectionOptions = connectionOptions; + this.listenerConfig = listenerConfig; } @Override @@ -106,7 +107,7 @@ public final void run() { String channelId = "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket)); // proxy protocol before SSL handshake - if (connectionOptions.enableProxyProtocol()) { + if (listenerConfig.enableProxyProtocol()) { ProxyProtocolHandler handler = new ProxyProtocolHandler(socket, channelId); proxyProtocolData = handler.get(); } @@ -238,8 +239,8 @@ public Router router() { } @Override - public ProxyProtocolData proxyProtocolData() { - return proxyProtocolData; + public Optional proxyProtocolData() { + return Optional.ofNullable(proxyProtocolData); } private ServerConnection identifyConnection() { diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java index 40056e5e0d2..ac0ebd84a47 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java @@ -327,6 +327,15 @@ interface ListenerConfigBlueprint { */ Optional listenerContext(); + /** + * Enable support for proxy protocol for this socket. + * Default is {@code false}. + * + * @return proxy support status + */ + @ConfiguredOption("false") + boolean enableProxyProtocol(); + /** * Requested URI discovery context. * diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java index 7de4a5b6088..9d34c8dbbb4 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java @@ -347,7 +347,7 @@ private void listen() { serverChannelId, router, tls, - connectionOptions); + listenerConfig); readerExecutor.execute(handler); } catch (RejectedExecutionException e) { LOGGER.log(ERROR, "Executor rejected handler for new connection"); diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java index 9153b610bb1..f0b03e811ee 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerRequest.java @@ -211,7 +211,7 @@ public UriInfo requestedUri() { @Override public Optional proxyProtocolData() { - return Optional.ofNullable(ctx.proxyProtocolData()); + return ctx.proxyProtocolData(); } private UriInfo createUriInfo() { From 688e8bf866786735c8bae7528409ee336a472625 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 18 Oct 2023 17:27:49 -0400 Subject: [PATCH 3/8] Initial support for proxy protocol version 2. --- .../helidon/webserver/ConnectionHandler.java | 1 - .../helidon/webserver/ProxyProtocolData.java | 77 ++++++++-- .../webserver/ProxyProtocolHandler.java | 139 +++++++++++++++--- .../webserver/ProxyProtocolHandlerTest.java | 40 ++++- 4 files changed, 223 insertions(+), 34 deletions(-) diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index 9f16152e9bd..0af7de7b5fe 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -34,7 +34,6 @@ import io.helidon.common.socket.HelidonSocket; import io.helidon.common.socket.PeerInfo; import io.helidon.common.socket.PlainSocket; -import io.helidon.common.socket.SocketOptions; import io.helidon.common.socket.SocketWriter; import io.helidon.common.socket.TlsSocket; import io.helidon.common.task.InterruptableTask; 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 25201ede9b2..4d2ba1ac566 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolData.java @@ -21,41 +21,92 @@ public interface ProxyProtocolData { /** - * The protocol family options. + * Protocol family. */ - enum ProtocolFamily { + enum Family { /** - * TCP version 4. + * Unknown family. */ - TCP4, + UNKNOWN, /** - * TCP version 6. + * IP version 4. */ - TCP6, + IPv4, /** - * Protocol family is unknown. + * IP version 6. */ - UNKNOWN + IPv6, + + /** + * Unix. + */ + UNIX; + + static Family fromString(String s) { + return switch (s) { + case "TCP4" -> IPv4; + case "TCP6" -> IPv6; + case "UNIX" -> UNIX; + case "UNKNOWN" -> UNKNOWN; + default -> throw new IllegalArgumentException("Unknown family " + s); + }; + } } /** - * Protocol family from protocol header. + * Protocol type. + */ + enum Protocol { + /** + * Unknown protocol. + */ + UNKNOWN, + + /** + * TCP streams protocol. + */ + TCP, + + /** + * UDP datagram protocol. + */ + UDP; + + static Protocol fromString(String s) { + return switch (s) { + case "TCP4", "TCP6" -> TCP; + case "UDP" -> UDP; + case "UNKNOWN" -> UNKNOWN; + default -> throw new IllegalArgumentException("Unknown protocol " + s); + }; + } + } + + /** + * Family from protocol header. + * + * @return family + */ + Family family(); + + /** + * Protocol from protocol header. * - * @return protocol family + * @return protocol */ - ProtocolFamily protocolFamily(); + Protocol protocol(); /** - * Source address that is either IPv4 or IPv6 depending on {@link #protocolFamily()}}. + * Source address that is either IP4 or IP6 depending on {@link #family()}. * * @return source address */ String sourceAddress(); /** - * Destination address that is either IPv4 or IPv6 depending on {@link #protocolFamily()}}. + * Destination address that is either IP4 or IP46 depending on {@link #family()}. * * @return source address */ 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 138bd8115a2..66285b1019a 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -16,6 +16,7 @@ package io.helidon.webserver; import java.io.IOException; +import java.io.InputStream; import java.io.PushbackInputStream; import java.io.UncheckedIOException; import java.lang.System.Logger.Level; @@ -25,11 +26,14 @@ import io.helidon.http.DirectHandler; import io.helidon.http.RequestException; +import io.helidon.webserver.ProxyProtocolData.Family; +import io.helidon.webserver.ProxyProtocolData.Protocol; class ProxyProtocolHandler implements Supplier { private static final System.Logger LOGGER = System.getLogger(ProxyProtocolHandler.class.getName()); private static final int MAX_V1_FIELD_LENGTH = 40; + private static final int MAX_V2_ADDRESS_LENGTH = 216; static final byte[] V1_PREFIX = { (byte) 'P', @@ -39,12 +43,15 @@ class ProxyProtocolHandler implements Supplier { (byte) 'Y', }; - static final byte[] V2_PREFIX = { + static final byte[] V2_PREFIX_1 = { (byte) 0x0D, (byte) 0x0A, (byte) 0x0D, (byte) 0x0A, (byte) 0x00, + }; + + static final byte[] V2_PREFIX_2 = { (byte) 0x0D, (byte) 0x0A, (byte) 0x51, @@ -80,7 +87,7 @@ public ProxyProtocolData get() { } if (arrayEquals(prefix, V1_PREFIX, V1_PREFIX.length)) { return handleV1Protocol(inputStream); - } else if (arrayEquals(prefix, V2_PREFIX, V1_PREFIX.length)) { + } else if (arrayEquals(prefix, V2_PREFIX_1, V2_PREFIX_1.length)) { return handleV2Protocol(inputStream); } else { throw BAD_PROTOCOL_EXCEPTION; @@ -99,12 +106,14 @@ static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throw // protocol and family n = readUntil(inputStream, buffer, (byte) ' ', (byte) '\r'); - var family = ProxyProtocolData.ProtocolFamily.valueOf(new String(buffer, 0, n)); - byte b = (byte) inputStream.read(); + String familyProtocol = new String(buffer, 0, n); + var family = Family.fromString(familyProtocol); + var protocol = Protocol.fromString(familyProtocol); + byte b = readNext(inputStream); if (b == (byte) '\r') { // special case for just UNKNOWN family - if (family == ProxyProtocolData.ProtocolFamily.UNKNOWN) { - return new ProxyProtocolDataImpl(ProxyProtocolData.ProtocolFamily.UNKNOWN, + if (family == ProxyProtocolData.Family.UNKNOWN) { + return new ProxyProtocolDataImpl(Family.UNKNOWN, Protocol.UNKNOWN, null, null, -1, -1); } } @@ -132,12 +141,103 @@ static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throw match(inputStream, (byte) '\r'); match(inputStream, (byte) '\n'); - return new ProxyProtocolDataImpl(family, sourceAddress, destAddress, sourcePort, destPort); + return new ProxyProtocolDataImpl(family, protocol, sourceAddress, destAddress, sourcePort, destPort); } catch (IllegalArgumentException e) { throw BAD_PROTOCOL_EXCEPTION; } } + static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throws IOException { + // match rest of prefix + match(inputStream, V2_PREFIX_2); + + // only accept version 2, ignore LOCAL/PROXY + int b = readNext(inputStream); + if (b >>> 4 != 0x02) { + throw BAD_PROTOCOL_EXCEPTION; + } + + // protocol and family + b = readNext(inputStream); + var family = switch (b >>> 4) { + case 0x1 -> Family.IPv4; + case 0x2 -> Family.IPv6; + case 0x3 -> Family.UNIX; + default -> Family.UNKNOWN; + }; + var protocol = switch (b & 0x0F) { + case 0x1 -> Protocol.TCP; + case 0x2 -> Protocol.UDP; + default -> Protocol.UNKNOWN; + }; + + // length + b = readNext(inputStream); + int headerLength = ((b << 8) & 0xFF00) | (readNext(inputStream) & 0xFF); + + // decode addresses and ports + String sourceAddress = null; + String destAddress = null; + int sourcePort = -1; + int destPort = -1; + byte[] buffer = new byte[MAX_V2_ADDRESS_LENGTH]; + switch (family) { + case IPv4 -> { + int n = inputStream.read(buffer, 0, 12); + if (n < 12) { + throw BAD_PROTOCOL_EXCEPTION; + } + sourceAddress = (buffer[0] & 0xFF) + + "." + (buffer[1] & 0xFF) + + "." + (buffer[2] & 0xFF) + + "." + (buffer[3] & 0xFF); + destAddress = (buffer[4] & 0xFF) + + "." + (buffer[5] & 0xFF) + + "." + (buffer[6] & 0xFF) + + "." + (buffer[7] & 0xFF); + sourcePort = buffer[9] & 0xFF + | ((buffer[8] << 8) & 0xFF00); + destPort = buffer[11] & 0xFF + | ((buffer[10] << 8) & 0xFF00); + headerLength -= 12; + } + case IPv6 -> { + int n = inputStream.read(buffer, 0, 36); + if (n < 36) { + throw BAD_PROTOCOL_EXCEPTION; + } + headerLength -= 36; + + } + case UNIX -> { + int n = inputStream.read(buffer, 0, 216); + if (n < 216) { + throw BAD_PROTOCOL_EXCEPTION; + } + headerLength -= 216; + } + default -> { + // falls through + } + } + + // skip any TLV vectors + while (headerLength > 0) { + headerLength -= (int) inputStream.skip(headerLength); + } + + return new ProxyProtocolDataImpl(family, protocol, sourceAddress, destAddress, + sourcePort, destPort); + } + + private static byte readNext(InputStream inputStream) throws IOException { + int b = inputStream.read(); + if (b < 0) { + throw BAD_PROTOCOL_EXCEPTION; + } + return (byte) b; + } + private static void match(byte a, byte b) { if (a != b) { throw BAD_PROTOCOL_EXCEPTION; @@ -150,28 +250,30 @@ private static void match(PushbackInputStream inputStream, byte b) throws IOExce } } + private static void match(PushbackInputStream inputStream, byte... bs) throws IOException { + for (byte b : bs) { + int c = inputStream.read(); + if (((byte) c) != b) { + throw BAD_PROTOCOL_EXCEPTION; + } + } + } + private static int readUntil(PushbackInputStream inputStream, byte[] buffer, byte... delims) throws IOException { int n = 0; do { - int b = inputStream.read(); - if (b < 0) { - throw BAD_PROTOCOL_EXCEPTION; - } - if (arrayContains(delims, (byte) b)) { + byte b = readNext(inputStream); + if (arrayContains(delims, b)) { inputStream.unread(b); return n; } - buffer[n++] = (byte) b; + buffer[n++] = b; if (n >= buffer.length) { throw BAD_PROTOCOL_EXCEPTION; } } while (true); } - static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throws IOException { - return null; - } - private static boolean arrayEquals(byte[] array1, byte[] array2, int prefix) { return Arrays.equals(array1, 0, prefix, array2, 0, prefix); } @@ -185,7 +287,8 @@ private static boolean arrayContains(byte[] array, byte b) { return false; } - record ProxyProtocolDataImpl(ProtocolFamily protocolFamily, + record ProxyProtocolDataImpl(Family family, + Protocol protocol, String sourceAddress, String destAddress, int sourcePort, 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 c3f889de27c..76db2d6daa4 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -30,12 +30,15 @@ class ProxyProtocolHandlerTest { + static final String V2_PREFIX_2 = "\0x0D\0x0A\0x51\0x55\0x49\0x54\0x0A"; + @Test void basicV1Test() throws IOException { String header = " TCP4 192.168.0.1 192.168.0.11 56324 443\r\n"; // excludes PROXY prefix ProxyProtocolData data = ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( new ByteArrayInputStream(header.getBytes(StandardCharsets.US_ASCII)))); - assertThat(data.protocolFamily(), is(ProxyProtocolData.ProtocolFamily.TCP4)); + assertThat(data.family(), is(ProxyProtocolData.Family.IPv4)); + assertThat(data.protocol(), is(ProxyProtocolData.Protocol.TCP)); assertThat(data.sourceAddress(), is("192.168.0.1")); assertThat(data.destAddress(), is("192.168.0.11")); assertThat(data.sourcePort(), is(56324)); @@ -47,7 +50,8 @@ void unknownV1Test() throws IOException { String header = " UNKNOWN\r\n"; // excludes PROXY prefix ProxyProtocolData data = ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( new ByteArrayInputStream(header.getBytes(StandardCharsets.US_ASCII)))); - assertThat(data.protocolFamily(), is(ProxyProtocolData.ProtocolFamily.UNKNOWN)); + assertThat(data.family(), is(ProxyProtocolData.Family.UNKNOWN)); + assertThat(data.protocol(), is(ProxyProtocolData.Protocol.UNKNOWN)); assertThat(data.sourceAddress(), nullValue()); assertThat(data.destAddress(), nullValue()); assertThat(data.sourcePort(), is(-1)); @@ -73,4 +77,36 @@ void badV1Test() { ProxyProtocolHandler.handleV1Protocol(new PushbackInputStream( new ByteArrayInputStream(header4.getBytes(StandardCharsets.US_ASCII))))); } + + @Test + void basicV2Test() throws IOException { + String header = V2_PREFIX_2 + + "\0x20\0x11\0x00\0x0C" // version, family/protocol, length + + "\0xC0\0xA8\0x00\0x01" // 192.168.0.1 + + "\0xC0\0xA8\0x00\0x0B" // 192.168.0.11 + + "\0xDC\0x04" // 56324 + + "\0x01\0xBB"; // 443 + ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + new ByteArrayInputStream(decodeHexString(header)))); + assertThat(data.family(), is(ProxyProtocolData.Family.IPv4)); + assertThat(data.protocol(), is(ProxyProtocolData.Protocol.TCP)); + assertThat(data.sourceAddress(), is("192.168.0.1")); + assertThat(data.destAddress(), is("192.168.0.11")); + assertThat(data.sourcePort(), is(56324)); + assertThat(data.destPort(), is(443)); + } + + private static byte[] decodeHexString(String s) { + assert !s.isEmpty() && s.length() % 4 == 0; + + byte[] bytes = new byte[s.length() / 4]; + for (int i = 0, j = 0; i < s.length(); i += 4) { + char c1 = s.charAt(i + 2); + byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10); + char c2 = s.charAt(i + 3); + byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10); + bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F)); + } + return bytes; + } } From 2bbaf4be037fa95d610b70b14ee80e657299396c Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 19 Oct 2023 11:58:13 -0400 Subject: [PATCH 4/8] Support for IPv4 in V2. --- .../webserver/ProxyProtocolHandler.java | 42 +++++++++++++------ .../webserver/ProxyProtocolHandlerTest.java | 22 +++++++++- 2 files changed, 51 insertions(+), 13 deletions(-) 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 66285b1019a..7337d4b49d9 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -20,7 +20,9 @@ import java.io.PushbackInputStream; import java.io.UncheckedIOException; import java.lang.System.Logger.Level; +import java.net.Inet6Address; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.function.Supplier; @@ -33,7 +35,6 @@ class ProxyProtocolHandler implements Supplier { private static final System.Logger LOGGER = System.getLogger(ProxyProtocolHandler.class.getName()); private static final int MAX_V1_FIELD_LENGTH = 40; - private static final int MAX_V2_ADDRESS_LENGTH = 216; static final byte[] V1_PREFIX = { (byte) 'P', @@ -180,11 +181,11 @@ static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throw String destAddress = null; int sourcePort = -1; int destPort = -1; - byte[] buffer = new byte[MAX_V2_ADDRESS_LENGTH]; switch (family) { case IPv4 -> { - int n = inputStream.read(buffer, 0, 12); - if (n < 12) { + byte[] buffer = new byte[12]; + int n = inputStream.read(buffer, 0, buffer.length); + if (n < buffer.length) { throw BAD_PROTOCOL_EXCEPTION; } sourceAddress = (buffer[0] & 0xFF) @@ -199,22 +200,39 @@ static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throw | ((buffer[8] << 8) & 0xFF00); destPort = buffer[11] & 0xFF | ((buffer[10] << 8) & 0xFF00); - headerLength -= 12; + headerLength -= buffer.length; } case IPv6 -> { - int n = inputStream.read(buffer, 0, 36); - if (n < 36) { + byte[] buffer = new byte[16]; + int n = inputStream.read(buffer, 0, buffer.length); + if (n < buffer.length) { throw BAD_PROTOCOL_EXCEPTION; } - headerLength -= 36; - + sourceAddress = Inet6Address.getByAddress(buffer).getHostAddress(); + n = inputStream.read(buffer, 0, buffer.length); + if (n < buffer.length) { + throw BAD_PROTOCOL_EXCEPTION; + } + destAddress = Inet6Address.getByAddress(buffer).getHostAddress(); + n = inputStream.read(buffer, 0, 4); + if (n < 4) { + throw BAD_PROTOCOL_EXCEPTION; + } + sourcePort = buffer[1] & 0xFF + | ((buffer[0] << 8) & 0xFF00); + destPort = buffer[3] & 0xFF + | ((buffer[2] << 8) & 0xFF00); + headerLength -= 2 * buffer.length + 4; } case UNIX -> { - int n = inputStream.read(buffer, 0, 216); - if (n < 216) { + byte[] buffer = new byte[216]; + int n = inputStream.read(buffer, 0, buffer.length); + if (n < buffer.length) { throw BAD_PROTOCOL_EXCEPTION; } - headerLength -= 216; + sourceAddress = new String(buffer, 0, 108, StandardCharsets.US_ASCII); + destAddress = new String(buffer, 108, buffer.length, StandardCharsets.US_ASCII); + headerLength -= buffer.length; } default -> { // falls through 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 76db2d6daa4..aec51364802 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -79,7 +79,7 @@ void badV1Test() { } @Test - void basicV2Test() throws IOException { + void basicV2TestIPv4() throws IOException { String header = V2_PREFIX_2 + "\0x20\0x11\0x00\0x0C" // version, family/protocol, length + "\0xC0\0xA8\0x00\0x01" // 192.168.0.1 @@ -96,6 +96,26 @@ void basicV2Test() throws IOException { assertThat(data.destPort(), is(443)); } + @Test + void basicV2TestIPv6() throws IOException { + String header = V2_PREFIX_2 + + "\0x20\0x21\0x00\0x0C" // version, family/protocol, length + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" // source + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" // dest + + "\0xDC\0x04" // 56324 + + "\0x01\0xBB"; // 443 + ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + new ByteArrayInputStream(decodeHexString(header)))); + assertThat(data.family(), is(ProxyProtocolData.Family.IPv6)); + assertThat(data.protocol(), is(ProxyProtocolData.Protocol.TCP)); + assertThat(data.sourceAddress(), is("aaaa:bbbb:cccc:dddd:aaaa:bbbb:cccc:dddd")); + assertThat(data.destAddress(), is("aaaa:bbbb:cccc:dddd:aaaa:bbbb:cccc:dddd")); + assertThat(data.sourcePort(), is(56324)); + assertThat(data.destPort(), is(443)); + } + private static byte[] decodeHexString(String s) { assert !s.isEmpty() && s.length() % 4 == 0; From db91e638cc3aff55c7bcc5c951c075e5fa6b0572 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 19 Oct 2023 14:14:39 -0400 Subject: [PATCH 5/8] Additional unit testing and a new integration test for V1 and V2 of the proxy protocol. --- .../testing/http/junit5/SocketHttpClient.java | 16 ++++ .../testing/junit5/HexStringDecoder.java | 44 +++++++++ .../webserver/tests/ProxyProtocolTest.java | 93 +++++++++++++++++++ .../webserver/ProxyProtocolHandler.java | 6 +- .../webserver/ProxyProtocolHandlerTest.java | 69 +++++++++++--- 5 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java create mode 100644 webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java diff --git a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java index 743bdf146c6..557aa1641ec 100644 --- a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java +++ b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java @@ -453,6 +453,22 @@ public void request(String method, String path, String protocol, String host, It } } + /** + * Write raw proxy protocol header before a request. + * + * @param header header to write + */ + public void writeProxyHeader(byte[] header) { + try { + if (socket == null) { + connect(); + } + socket.getOutputStream().write(header); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + /** * Disconnect from server socket. */ 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 new file mode 100644 index 00000000000..725f8084fc3 --- /dev/null +++ b/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.common.testing.junit5; + +public final class HexStringDecoder { + + private HexStringDecoder() { + } + + /** + * Utility method to decode hex strings. For example, "\0x0D\0x0A\0x0D\0x0A" is decoded + * as a 4-byte array with hex values 0D 0A 0D 0A. + * + * @param s string to decode + * @return decoded string as byte array + */ + public static byte[] decodeHexString(String s) { + if (s.isEmpty() || s.length() % 4 != 0) { + throw new IllegalArgumentException("Invalid hex string"); + } + byte[] bytes = new byte[s.length() / 4]; + for (int i = 0, j = 0; i < s.length(); i += 4) { + char c1 = s.charAt(i + 2); + byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10); + char c2 = s.charAt(i + 3); + byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10); + bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F)); + } + return bytes; + } +} 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 new file mode 100644 index 00000000000..9439edae9f6 --- /dev/null +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/ProxyProtocolTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.tests; + +import io.helidon.common.testing.http.junit5.SocketHttpClient; +import io.helidon.http.Method; +import io.helidon.http.Status; +import io.helidon.webserver.ProxyProtocolData; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; +import io.helidon.webserver.testing.junit5.SetUpServer; +import org.junit.jupiter.api.Test; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString; + +@ServerTest +class ProxyProtocolTest { + + static final String V2_PREFIX = "\0x0D\0x0A\0x0D\0x0A\0x00\0x0D\0x0A\0x51\0x55\0x49\0x54\0x0A"; + + private final SocketHttpClient socketHttpClient; + + ProxyProtocolTest(SocketHttpClient socketHttpClient) { + this.socketHttpClient = socketHttpClient; + } + + @SetUpServer + static void setupServer(WebServerConfig.Builder builder) { + builder.enableProxyProtocol(true); + } + + @SetUpRoute + static void routing(HttpRules routing) { + routing.get("/", (req, res) -> { + ProxyProtocolData data = req.proxyProtocolData().orElse(null); + if (data != null + && data.family() == ProxyProtocolData.Family.IPv4 + && data.protocol() == ProxyProtocolData.Protocol.TCP + && data.sourceAddress().equals("192.168.0.1") + && data.destAddress().equals("192.168.0.11") + && data.sourcePort() == 56324 + && data.destPort() == 443) { + res.status(Status.OK_200).send(); + return; + } + res.status(Status.INTERNAL_SERVER_ERROR_500).send(); + }); + } + + /** + * V1 encoding in this test was manually verified with Wireshark. + */ + @Test + void testProxyProtocolV1IPv4() { + socketHttpClient.writeProxyHeader("PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n".getBytes(US_ASCII)); + String s = socketHttpClient.sendAndReceive(Method.GET, ""); + assertThat(s, startsWith("HTTP/1.1 200 OK")); + } + + /** + * V2 encoding in this test was manually verified with Wireshark. + */ + @Test + void testProxyProtocolV2IPv4() { + String header = V2_PREFIX + + "\0x20\0x11\0x00\0x0C" // version, family/protocol, length + + "\0xC0\0xA8\0x00\0x01" // 192.168.0.1 + + "\0xC0\0xA8\0x00\0x0B" // 192.168.0.11 + + "\0xDC\0x04" // 56324 + + "\0x01\0xBB"; // 443 + socketHttpClient.writeProxyHeader(decodeHexString(header)); + String s = socketHttpClient.sendAndReceive(Method.GET, ""); + assertThat(s, startsWith("HTTP/1.1 200 OK")); + } +} 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 7337d4b49d9..6c081d3e186 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -35,6 +35,7 @@ class ProxyProtocolHandler implements Supplier { private static final System.Logger LOGGER = System.getLogger(ProxyProtocolHandler.class.getName()); private static final int MAX_V1_FIELD_LENGTH = 40; + private static final int MAX_TLV_BYTES_TO_SKIP = 128 * 4; // 128 entries static final byte[] V1_PREFIX = { (byte) 'P', @@ -239,7 +240,10 @@ static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throw } } - // skip any TLV vectors + // skip any TLV vectors up to our max for security reasons + if (headerLength > MAX_TLV_BYTES_TO_SKIP) { + throw BAD_PROTOCOL_EXCEPTION; + } while (headerLength > 0) { headerLength -= (int) inputStream.skip(headerLength); } 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 aec51364802..fdfebbce7ba 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -27,6 +27,7 @@ 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; class ProxyProtocolHandlerTest { @@ -116,17 +117,63 @@ void basicV2TestIPv6() throws IOException { assertThat(data.destPort(), is(443)); } - private static byte[] decodeHexString(String s) { - assert !s.isEmpty() && s.length() % 4 == 0; + @Test + void unknownV2Test() throws IOException { + String header = V2_PREFIX_2 + + "\0x20\0x00\0x00\0x40" // version, family/protocol, length=64 + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"; + ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + 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.sourcePort(), is(-1)); + assertThat(data.destPort(), is(-1)); + } - byte[] bytes = new byte[s.length() / 4]; - for (int i = 0, j = 0; i < s.length(); i += 4) { - char c1 = s.charAt(i + 2); - byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10); - char c2 = s.charAt(i + 3); - byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10); - bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F)); - } - return bytes; + @Test + void badV2Test() { + String header1 = V2_PREFIX_2 + + "\0x20\0x21\0x00\0x0C" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB" // bad source + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xDC\0x04" + + "\0x01\0xBB"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + new ByteArrayInputStream(decodeHexString(header1))))); + + String header2 = V2_PREFIX_2 + + "\0x20\0x21\0x0F\0xFF" // bad length, over our limit + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xDC\0x04" + + "\0x01\0xBB"; + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + new ByteArrayInputStream(decodeHexString(header2))))); + + String header3 = V2_PREFIX_2 + + "\0x20\0x21\0x00\0x0C" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" + + "\0xDC\0x04"; // missing dest port + assertThrows(RequestException.class, () -> + ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( + new ByteArrayInputStream(decodeHexString(header3))))); } } From 1e86c50f6d147beaa43a005184b12e06146e3e54 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 20 Oct 2023 15:37:01 -0400 Subject: [PATCH 6/8] 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)); } From 7d1f6c3c2699de688e0666c1c0c606cdc9444e3d Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 23 Oct 2023 15:00:00 -0400 Subject: [PATCH 7/8] Use explicit instead of default encoding. Signed-off-by: Santiago Pericasgeertsen --- .../io/helidon/webserver/ListenerConfigBlueprint.java | 2 +- .../io/helidon/webserver/ProxyProtocolHandler.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java index ac0ebd84a47..c9d5396ccef 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java @@ -333,7 +333,7 @@ interface ListenerConfigBlueprint { * * @return proxy support status */ - @ConfiguredOption("false") + @Option.Default("false") boolean enableProxyProtocol(); /** 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 70fa610a060..663d2d916ad 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java @@ -108,7 +108,7 @@ static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throw // protocol and family n = readUntil(inputStream, buffer, (byte) ' ', (byte) '\r'); - String familyProtocol = new String(buffer, 0, n); + String familyProtocol = new String(buffer, 0, n, StandardCharsets.US_ASCII); var family = Family.fromString(familyProtocol); var protocol = Protocol.fromString(familyProtocol); byte b = readNext(inputStream); @@ -124,22 +124,22 @@ static ProxyProtocolData handleV1Protocol(PushbackInputStream inputStream) throw // source address n = readUntil(inputStream, buffer, (byte) ' '); - var sourceAddress = new String(buffer, 0, n); + var sourceAddress = new String(buffer, 0, n, StandardCharsets.US_ASCII); match(inputStream, (byte) ' '); // destination address n = readUntil(inputStream, buffer, (byte) ' '); - var destAddress = new String(buffer, 0, n); + var destAddress = new String(buffer, 0, n, StandardCharsets.US_ASCII); match(inputStream, (byte) ' '); // source port n = readUntil(inputStream, buffer, (byte) ' '); - int sourcePort = Integer.parseInt(new String(buffer, 0, n)); + int sourcePort = Integer.parseInt(new String(buffer, 0, n, StandardCharsets.US_ASCII)); match(inputStream, (byte) ' '); // destination port n = readUntil(inputStream, buffer, (byte) '\r'); - int destPort = Integer.parseInt(new String(buffer, 0, n)); + int destPort = Integer.parseInt(new String(buffer, 0, n, StandardCharsets.US_ASCII)); match(inputStream, (byte) '\r'); match(inputStream, (byte) '\n'); From 98baf278e776f445a68f5c96f58013428ebe50f3 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Tue, 14 Nov 2023 13:27:25 -0500 Subject: [PATCH 8/8] Switch to using HexFormat. Improved new Javadoc. Cleanup constructor params. --- .../testing/junit5/HexStringDecoder.java | 47 -------- .../webserver/tests/ProxyProtocolTest.java | 19 ++-- .../helidon/webserver/ConnectionHandler.java | 5 +- .../webserver/ListenerConfigBlueprint.java | 8 +- .../io/helidon/webserver/ServerListener.java | 3 +- .../webserver/ProxyProtocolHandlerTest.java | 100 +++++++++--------- 6 files changed, 71 insertions(+), 111 deletions(-) delete mode 100644 common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java 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 deleted file mode 100644 index 04c174a133e..00000000000 --- a/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/HexStringDecoder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 io.helidon.common.testing.junit5; - -/** - * Utility class to decode hex strings. - */ -public final class HexStringDecoder { - - private HexStringDecoder() { - } - - /** - * Utility method to decode hex strings. For example, "\0x0D\0x0A\0x0D\0x0A" is decoded - * as a 4-byte array with hex values 0D 0A 0D 0A. - * - * @param s string to decode - * @return decoded string as byte array - */ - public static byte[] decodeHexString(String s) { - if (s.isEmpty() || s.length() % 4 != 0) { - throw new IllegalArgumentException("Invalid hex string"); - } - byte[] bytes = new byte[s.length() / 4]; - for (int i = 0, j = 0; i < s.length(); i += 4) { - char c1 = s.charAt(i + 2); - byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10); - char c2 = s.charAt(i + 3); - byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10); - bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F)); - } - return bytes; - } -} 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 1b1af17cfd2..281d65579a0 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 @@ -15,6 +15,8 @@ */ package io.helidon.webserver.tests; +import java.util.HexFormat; + import io.helidon.common.testing.http.junit5.SocketHttpClient; import io.helidon.http.HeaderNames; import io.helidon.http.Method; @@ -30,13 +32,14 @@ import static java.nio.charset.StandardCharsets.US_ASCII; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; -import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString; @ServerTest class ProxyProtocolTest { - static final String V2_PREFIX = "\0x0D\0x0A\0x0D\0x0A\0x00\0x0D\0x0A\0x51\0x55\0x49\0x54\0x0A"; + static final String V2_PREFIX = "0D:0A:0D:0A:00:0D:0A:51:55:49:54:0A"; + private final static HexFormat hexFormat = HexFormat.of().withUpperCase().withDelimiter(":"); + private final SocketHttpClient socketHttpClient; ProxyProtocolTest(SocketHttpClient socketHttpClient) { @@ -84,12 +87,12 @@ void testProxyProtocolV1IPv4() { @Test void testProxyProtocolV2IPv4() { String header = V2_PREFIX - + "\0x20\0x11\0x00\0x0C" // version, family/protocol, length - + "\0xC0\0xA8\0x00\0x01" // 192.168.0.1 - + "\0xC0\0xA8\0x00\0x0B" // 192.168.0.11 - + "\0xDC\0x04" // 56324 - + "\0x01\0xBB"; // 443 - socketHttpClient.writeProxyHeader(decodeHexString(header)); + + ":20:11:00:0C" // version, family/protocol, length + + ":C0:A8:00:01" // 192.168.0.1 + + ":C0:A8:00:0B" // 192.168.0.11 + + ":DC:04" // 56324 + + ":01:BB"; // 443 + socketHttpClient.writeProxyHeader(hexFormat.parseHex(header)); String s = socketHttpClient.sendAndReceive(Method.GET, ""); assertThat(s, startsWith("HTTP/1.1 200 OK")); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java index 0af7de7b5fe..e3c47720f99 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ConnectionHandler.java @@ -81,8 +81,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { Socket socket, String serverChannelId, Router router, - Tls tls, - ListenerConfig listenerConfig) { + Tls tls) { this.listenerContext = listenerContext; this.connectionSemaphore = connectionSemaphore; this.requestSemaphore = requestSemaphore; @@ -93,7 +92,7 @@ class ConnectionHandler implements InterruptableTask, ConnectionContext { this.serverChannelId = serverChannelId; this.router = router; this.tls = tls; - this.listenerConfig = listenerConfig; + this.listenerConfig = listenerContext.config(); } @Override diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java index c9d5396ccef..df40279f690 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ListenerConfigBlueprint.java @@ -328,8 +328,12 @@ interface ListenerConfigBlueprint { Optional listenerContext(); /** - * Enable support for proxy protocol for this socket. - * Default is {@code false}. + * Enable proxy protocol support for this socket. This protocol is supported by + * some load balancers/reverse proxies as a means to convey client information that + * would otherwise be lost. If enabled, the proxy protocol header must be present + * on every new connection established with your server. For more information, + * see + * the specification. Default is {@code false}. * * @return proxy support status */ diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java index 9d34c8dbbb4..c5e0761f7a1 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java @@ -346,8 +346,7 @@ private void listen() { socket, serverChannelId, router, - tls, - listenerConfig); + tls); readerExecutor.execute(handler); } catch (RejectedExecutionException e) { LOGGER.log(ERROR, "Executor rejected handler for new connection"); 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 988add3c9d8..a1dca086868 100644 --- a/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java +++ b/webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.PushbackInputStream; import java.nio.charset.StandardCharsets; +import java.util.HexFormat; import io.helidon.http.RequestException; import org.junit.jupiter.api.Test; @@ -26,11 +27,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString; class ProxyProtocolHandlerTest { - static final String V2_PREFIX_2 = "\0x0D\0x0A\0x51\0x55\0x49\0x54\0x0A"; + static final String V2_PREFIX_2 = "0D:0A:51:55:49:54:0A:"; + + private static final HexFormat hexFormat = HexFormat.of().withUpperCase().withDelimiter(":"); @Test void basicV1Test() throws IOException { @@ -81,13 +83,13 @@ void badV1Test() { @Test void basicV2TestIPv4() throws IOException { String header = V2_PREFIX_2 - + "\0x20\0x11\0x00\0x0C" // version, family/protocol, length - + "\0xC0\0xA8\0x00\0x01" // 192.168.0.1 - + "\0xC0\0xA8\0x00\0x0B" // 192.168.0.11 - + "\0xDC\0x04" // 56324 - + "\0x01\0xBB"; // 443 + + "20:11:00:0C:" // version, family/protocol, length + + "C0:A8:00:01:" // 192.168.0.1 + + "C0:A8:00:0B:" // 192.168.0.11 + + "DC:04:" // 56324 + + "01:BB"; // 443 ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header)))); + new ByteArrayInputStream(hexFormat.parseHex(header)))); assertThat(data.family(), is(ProxyProtocolData.Family.IPv4)); assertThat(data.protocol(), is(ProxyProtocolData.Protocol.TCP)); assertThat(data.sourceAddress(), is("192.168.0.1")); @@ -99,15 +101,15 @@ void basicV2TestIPv4() throws IOException { @Test void basicV2TestIPv6() throws IOException { String header = V2_PREFIX_2 - + "\0x20\0x21\0x00\0x0C" // version, family/protocol, length - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" // source - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" // dest - + "\0xDC\0x04" // 56324 - + "\0x01\0xBB"; // 443 + + "20:21:00:0C:" // version, family/protocol, length + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" // source + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" // dest + + "DC:04:" // 56324 + + "01:BB"; // 443 ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header)))); + new ByteArrayInputStream(hexFormat.parseHex(header)))); assertThat(data.family(), is(ProxyProtocolData.Family.IPv6)); assertThat(data.protocol(), is(ProxyProtocolData.Protocol.TCP)); assertThat(data.sourceAddress(), is("aaaa:bbbb:cccc:dddd:aaaa:bbbb:cccc:dddd")); @@ -119,17 +121,17 @@ void basicV2TestIPv6() throws IOException { @Test void unknownV2Test() throws IOException { String header = V2_PREFIX_2 - + "\0x20\0x00\0x00\0x40" // version, family/protocol, length=64 - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"; + + "20:00:00:40:" // version, family/protocol, length=64 + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD"; ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header)))); + new ByteArrayInputStream(hexFormat.parseHex(header)))); assertThat(data.family(), is(ProxyProtocolData.Family.UNKNOWN)); assertThat(data.protocol(), is(ProxyProtocolData.Protocol.UNKNOWN)); assertThat(data.sourceAddress(), is("")); @@ -141,38 +143,38 @@ void unknownV2Test() throws IOException { @Test void badV2Test() { String header1 = V2_PREFIX_2 - + "\0x20\0x21\0x00\0x0C" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB" // bad source - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xDC\0x04" - + "\0x01\0xBB"; + + "20:21:00:0C:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:" // bad source + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "DC:04:" + + "01:BB"; assertThrows(RequestException.class, () -> ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header1))))); + new ByteArrayInputStream(hexFormat.parseHex(header1))))); String header2 = V2_PREFIX_2 - + "\0x20\0x21\0x0F\0xFF" // bad length, over our limit - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xDC\0x04" - + "\0x01\0xBB"; + + "20:21:0F:FF:" // bad length, over our limit + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "DC:04:" + + "01:BB"; assertThrows(RequestException.class, () -> ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header2))))); + new ByteArrayInputStream(hexFormat.parseHex(header2))))); String header3 = V2_PREFIX_2 - + "\0x20\0x21\0x00\0x0C" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD" - + "\0xDC\0x04"; // missing dest port + + "20:21:00:0C:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "AA:AA:BB:BB:CC:CC:DD:DD:" + + "DC:04"; // missing dest port assertThrows(RequestException.class, () -> ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream( - new ByteArrayInputStream(decodeHexString(header3))))); + new ByteArrayInputStream(hexFormat.parseHex(header3))))); } }