Skip to content

Commit 183f66e

Browse files
committed
Additional unit testing and a new integration test for V1 and V2 of the proxy protocol.
1 parent 754262b commit 183f66e

File tree

5 files changed

+216
-12
lines changed

5 files changed

+216
-12
lines changed

common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,22 @@ public void request(String method, String path, String protocol, String host, It
453453
}
454454
}
455455

456+
/**
457+
* Write raw proxy protocol header before a request.
458+
*
459+
* @param header header to write
460+
*/
461+
public void writeProxyHeader(byte[] header) {
462+
try {
463+
if (socket == null) {
464+
connect();
465+
}
466+
socket.getOutputStream().write(header);
467+
} catch (IOException e) {
468+
throw new UncheckedIOException(e);
469+
}
470+
}
471+
456472
/**
457473
* Disconnect from server socket.
458474
*/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.common.testing.junit5;
17+
18+
public final class HexStringDecoder {
19+
20+
private HexStringDecoder() {
21+
}
22+
23+
/**
24+
* Utility method to decode hex strings. For example, "\0x0D\0x0A\0x0D\0x0A" is decoded
25+
* as a 4-byte array with hex values 0D 0A 0D 0A.
26+
*
27+
* @param s string to decode
28+
* @return decoded string as byte array
29+
*/
30+
public static byte[] decodeHexString(String s) {
31+
if (s.isEmpty() || s.length() % 4 != 0) {
32+
throw new IllegalArgumentException("Invalid hex string");
33+
}
34+
byte[] bytes = new byte[s.length() / 4];
35+
for (int i = 0, j = 0; i < s.length(); i += 4) {
36+
char c1 = s.charAt(i + 2);
37+
byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10);
38+
char c2 = s.charAt(i + 3);
39+
byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10);
40+
bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F));
41+
}
42+
return bytes;
43+
}
44+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.webserver.tests;
17+
18+
import io.helidon.common.testing.http.junit5.SocketHttpClient;
19+
import io.helidon.http.Method;
20+
import io.helidon.http.Status;
21+
import io.helidon.webserver.ProxyProtocolData;
22+
import io.helidon.webserver.WebServerConfig;
23+
import io.helidon.webserver.http.HttpRules;
24+
import io.helidon.webserver.testing.junit5.ServerTest;
25+
import io.helidon.webserver.testing.junit5.SetUpRoute;
26+
import io.helidon.webserver.testing.junit5.SetUpServer;
27+
import org.junit.jupiter.api.Test;
28+
29+
import static java.nio.charset.StandardCharsets.US_ASCII;
30+
import static org.hamcrest.CoreMatchers.startsWith;
31+
import static org.hamcrest.MatcherAssert.assertThat;
32+
import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString;
33+
34+
@ServerTest
35+
class ProxyProtocolTest {
36+
37+
static final String V2_PREFIX = "\0x0D\0x0A\0x0D\0x0A\0x00\0x0D\0x0A\0x51\0x55\0x49\0x54\0x0A";
38+
39+
private final SocketHttpClient socketHttpClient;
40+
41+
ProxyProtocolTest(SocketHttpClient socketHttpClient) {
42+
this.socketHttpClient = socketHttpClient;
43+
}
44+
45+
@SetUpServer
46+
static void setupServer(WebServerConfig.Builder builder) {
47+
builder.enableProxyProtocol(true);
48+
}
49+
50+
@SetUpRoute
51+
static void routing(HttpRules routing) {
52+
routing.get("/", (req, res) -> {
53+
ProxyProtocolData data = req.proxyProtocolData().orElse(null);
54+
if (data != null
55+
&& data.family() == ProxyProtocolData.Family.IPv4
56+
&& data.protocol() == ProxyProtocolData.Protocol.TCP
57+
&& data.sourceAddress().equals("192.168.0.1")
58+
&& data.destAddress().equals("192.168.0.11")
59+
&& data.sourcePort() == 56324
60+
&& data.destPort() == 443) {
61+
res.status(Status.OK_200).send();
62+
return;
63+
}
64+
res.status(Status.INTERNAL_SERVER_ERROR_500).send();
65+
});
66+
}
67+
68+
/**
69+
* V1 encoding in this test was manually verified with Wireshark.
70+
*/
71+
@Test
72+
void testProxyProtocolV1IPv4() {
73+
socketHttpClient.writeProxyHeader("PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n".getBytes(US_ASCII));
74+
String s = socketHttpClient.sendAndReceive(Method.GET, "");
75+
assertThat(s, startsWith("HTTP/1.1 200 OK"));
76+
}
77+
78+
/**
79+
* V2 encoding in this test was manually verified with Wireshark.
80+
*/
81+
@Test
82+
void testProxyProtocolV2IPv4() {
83+
String header = V2_PREFIX
84+
+ "\0x20\0x11\0x00\0x0C" // version, family/protocol, length
85+
+ "\0xC0\0xA8\0x00\0x01" // 192.168.0.1
86+
+ "\0xC0\0xA8\0x00\0x0B" // 192.168.0.11
87+
+ "\0xDC\0x04" // 56324
88+
+ "\0x01\0xBB"; // 443
89+
socketHttpClient.writeProxyHeader(decodeHexString(header));
90+
String s = socketHttpClient.sendAndReceive(Method.GET, "");
91+
assertThat(s, startsWith("HTTP/1.1 200 OK"));
92+
}
93+
}

webserver/webserver/src/main/java/io/helidon/webserver/ProxyProtocolHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ProxyProtocolHandler implements Supplier<ProxyProtocolData> {
3535
private static final System.Logger LOGGER = System.getLogger(ProxyProtocolHandler.class.getName());
3636

3737
private static final int MAX_V1_FIELD_LENGTH = 40;
38+
private static final int MAX_TLV_BYTES_TO_SKIP = 128 * 4; // 128 entries
3839

3940
static final byte[] V1_PREFIX = {
4041
(byte) 'P',
@@ -239,7 +240,10 @@ static ProxyProtocolData handleV2Protocol(PushbackInputStream inputStream) throw
239240
}
240241
}
241242

242-
// skip any TLV vectors
243+
// skip any TLV vectors up to our max for security reasons
244+
if (headerLength > MAX_TLV_BYTES_TO_SKIP) {
245+
throw BAD_PROTOCOL_EXCEPTION;
246+
}
243247
while (headerLength > 0) {
244248
headerLength -= (int) inputStream.skip(headerLength);
245249
}

webserver/webserver/src/test/java/io/helidon/webserver/ProxyProtocolHandlerTest.java

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.hamcrest.CoreMatchers.nullValue;
2828
import static org.hamcrest.MatcherAssert.assertThat;
2929
import static org.junit.jupiter.api.Assertions.assertThrows;
30+
import static io.helidon.common.testing.junit5.HexStringDecoder.decodeHexString;
3031

3132
class ProxyProtocolHandlerTest {
3233

@@ -116,17 +117,63 @@ void basicV2TestIPv6() throws IOException {
116117
assertThat(data.destPort(), is(443));
117118
}
118119

119-
private static byte[] decodeHexString(String s) {
120-
assert !s.isEmpty() && s.length() % 4 == 0;
120+
@Test
121+
void unknownV2Test() throws IOException {
122+
String header = V2_PREFIX_2
123+
+ "\0x20\0x00\0x00\0x40" // version, family/protocol, length=64
124+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
125+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
126+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
127+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
128+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
129+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
130+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
131+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD";
132+
ProxyProtocolData data = ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream(
133+
new ByteArrayInputStream(decodeHexString(header))));
134+
assertThat(data.family(), is(ProxyProtocolData.Family.UNKNOWN));
135+
assertThat(data.protocol(), is(ProxyProtocolData.Protocol.UNKNOWN));
136+
assertThat(data.sourceAddress(), nullValue());
137+
assertThat(data.destAddress(), nullValue());
138+
assertThat(data.sourcePort(), is(-1));
139+
assertThat(data.destPort(), is(-1));
140+
}
121141

122-
byte[] bytes = new byte[s.length() / 4];
123-
for (int i = 0, j = 0; i < s.length(); i += 4) {
124-
char c1 = s.charAt(i + 2);
125-
byte b1 = (byte) (Character.isDigit(c1) ? c1 - '0' : c1 - 'A' + 10);
126-
char c2 = s.charAt(i + 3);
127-
byte b2 = (byte) (Character.isDigit(c2) ? c2 - '0' : c2 - 'A' + 10);
128-
bytes[j++] = (byte) (((b1 << 4) & 0xF0) | (b2 & 0x0F));
129-
}
130-
return bytes;
142+
@Test
143+
void badV2Test() {
144+
String header1 = V2_PREFIX_2
145+
+ "\0x20\0x21\0x00\0x0C"
146+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
147+
+ "\0xAA\0xAA\0xBB\0xBB" // bad source
148+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
149+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
150+
+ "\0xDC\0x04"
151+
+ "\0x01\0xBB";
152+
assertThrows(RequestException.class, () ->
153+
ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream(
154+
new ByteArrayInputStream(decodeHexString(header1)))));
155+
156+
String header2 = V2_PREFIX_2
157+
+ "\0x20\0x21\0x0F\0xFF" // bad length, over our limit
158+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
159+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
160+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
161+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
162+
+ "\0xDC\0x04"
163+
+ "\0x01\0xBB";
164+
assertThrows(RequestException.class, () ->
165+
ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream(
166+
new ByteArrayInputStream(decodeHexString(header2)))));
167+
168+
String header3 = V2_PREFIX_2
169+
+ "\0x20\0x21\0x00\0x0C"
170+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
171+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
172+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
173+
+ "\0xAA\0xAA\0xBB\0xBB\0xCC\0xCC\0xDD\0xDD"
174+
+ "\0xDC\0x04"; // missing dest port
175+
assertThrows(RequestException.class, () ->
176+
ProxyProtocolHandler.handleV2Protocol(new PushbackInputStream(
177+
new ByteArrayInputStream(decodeHexString(header3)))));
131178
}
132179
}

0 commit comments

Comments
 (0)