Skip to content

Commit c0eb91c

Browse files
add upstream arguments validation + tests
1 parent 404e419 commit c0eb91c

File tree

3 files changed

+230
-8
lines changed

3 files changed

+230
-8
lines changed

src/main/java/com/testingbot/tunnel/App.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,36 @@ public void setClientSecret(String secret) {
569569
clientSecret = secret;
570570
}
571571

572+
/**
573+
* Sets the upstream proxy server address.
574+
*
575+
* @param p proxy address in format "host:port" or "host" (defaults to port 80)
576+
* @throws IllegalArgumentException if proxy format is invalid
577+
*/
572578
public void setProxy(String p) {
573-
proxy = p;
579+
if (p != null && !p.trim().isEmpty()) {
580+
// Validate proxy format: hostname:port or hostname or IP:port
581+
String trimmed = p.trim();
582+
if (trimmed.contains(":")) {
583+
String[] parts = trimmed.split(":", 2);
584+
if (parts.length != 2 || parts[0].isEmpty()) {
585+
throw new IllegalArgumentException("Invalid proxy format. Expected 'host:port' but got: " + p);
586+
}
587+
try {
588+
int port = Integer.parseInt(parts[1]);
589+
if (port < 1 || port > 65535) {
590+
throw new IllegalArgumentException("Invalid proxy port. Must be between 1-65535 but got: " + port);
591+
}
592+
} catch (NumberFormatException e) {
593+
throw new IllegalArgumentException("Invalid proxy port. Must be a number but got: " + parts[1]);
594+
}
595+
} else if (trimmed.isEmpty()) {
596+
throw new IllegalArgumentException("Proxy hostname cannot be empty");
597+
}
598+
proxy = trimmed;
599+
} else {
600+
proxy = p;
601+
}
574602
}
575603

576604
public String getProxy() {
@@ -629,10 +657,27 @@ public String getProxyAuth() {
629657
return proxyAuth;
630658
}
631659

660+
/**
661+
* Sets the proxy authentication credentials.
662+
*
663+
* @param proxyAuth credentials in format "username:password"
664+
* @throws IllegalArgumentException if format is invalid
665+
*/
632666
public void setProxyAuth(String proxyAuth) {
633-
this.proxyAuth = proxyAuth;
634-
String[] splitted = proxyAuth.split(":");
635-
Authenticator.setDefault(new ProxyAuth(splitted[0], splitted[1]));
667+
if (proxyAuth != null && !proxyAuth.trim().isEmpty()) {
668+
String trimmed = proxyAuth.trim();
669+
if (!trimmed.contains(":")) {
670+
throw new IllegalArgumentException("Invalid proxy auth format. Expected 'username:password' but got: " + proxyAuth);
671+
}
672+
String[] splitted = trimmed.split(":", 2);
673+
if (splitted.length != 2 || splitted[0].isEmpty()) {
674+
throw new IllegalArgumentException("Invalid proxy auth format. Username cannot be empty");
675+
}
676+
this.proxyAuth = trimmed;
677+
Authenticator.setDefault(new ProxyAuth(splitted[0], splitted[1]));
678+
} else {
679+
this.proxyAuth = proxyAuth;
680+
}
636681
}
637682

638683
/**

src/main/java/com/testingbot/tunnel/proxy/CustomConnectHandler.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,22 @@ private void connectToProxy(HttpServletRequest request, String host, int port, P
142142
response.append((char) buffer.get());
143143
}
144144

145-
if (!response.toString().contains("200"))
146-
throw new IOException("Channel error response:\n" + response);
145+
String responseStr = response.toString();
146+
if (!responseStr.contains("200")) {
147+
String errorMsg = String.format("Upstream proxy (%s:%d) rejected CONNECT request to %s:%d. Response: %s",
148+
proxyHost, proxyPort, host, port, responseStr.split("\r\n")[0]);
149+
throw new IOException(errorMsg);
150+
}
151+
152+
if (debugMode) {
153+
LOG.info("Successfully established CONNECT tunnel through upstream proxy {}:{} to {}:{}",
154+
proxyHost, proxyPort, host, port);
155+
}
147156

148157
try {
149158
selector.close();
150159
} catch (final IOException e) {
151-
LOG.error(e.getMessage(), e);
160+
LOG.error("Error closing selector: {}", e.getMessage(), e);
152161
}
153162

154163
promise.succeeded(channel);
@@ -157,11 +166,13 @@ private void connectToProxy(HttpServletRequest request, String host, int port, P
157166
}
158167
}
159168
} catch (IOException x) {
169+
LOG.error("Failed to establish CONNECT tunnel through upstream proxy {}:{} to {}:{}: {}",
170+
proxyHost, proxyPort, host, port, x.getMessage());
160171
if (channel != null) {
161172
try {
162173
channel.close();
163174
} catch (IOException t) {
164-
LOG.error(t.getMessage(), t);
175+
LOG.error("Error closing channel after failure: {}", t.getMessage(), t);
165176
}
166177
}
167178
promise.failed(x);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.testingbot.tunnel;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
8+
9+
/**
10+
* Tests for proxy configuration validation
11+
*/
12+
class ProxyValidationTest {
13+
14+
private App app;
15+
16+
@BeforeEach
17+
void setUp() {
18+
app = new App();
19+
app.setClientKey("test");
20+
app.setClientSecret("test");
21+
}
22+
23+
@Test
24+
void setProxy_withValidHostAndPort_shouldSucceed() {
25+
// Given: Valid proxy configuration
26+
// When: Setting proxy
27+
app.setProxy("proxy.example.com:8080");
28+
29+
// Then: Should be set
30+
assertThat(app.getProxy()).isEqualTo("proxy.example.com:8080");
31+
}
32+
33+
@Test
34+
void setProxy_withValidHostOnly_shouldSucceed() {
35+
// Given: Valid proxy hostname without port
36+
// When: Setting proxy
37+
app.setProxy("proxy.example.com");
38+
39+
// Then: Should be set
40+
assertThat(app.getProxy()).isEqualTo("proxy.example.com");
41+
}
42+
43+
@Test
44+
void setProxy_withIPAddress_shouldSucceed() {
45+
// Given: Valid IP address
46+
// When: Setting proxy
47+
app.setProxy("192.168.1.1:3128");
48+
49+
// Then: Should be set
50+
assertThat(app.getProxy()).isEqualTo("192.168.1.1:3128");
51+
}
52+
53+
@Test
54+
void setProxy_withInvalidPort_shouldThrowException() {
55+
// Given: Invalid port number
56+
// When/Then: Setting proxy should throw exception
57+
assertThatThrownBy(() -> app.setProxy("proxy.example.com:99999"))
58+
.isInstanceOf(IllegalArgumentException.class)
59+
.hasMessageContaining("Invalid proxy port");
60+
}
61+
62+
@Test
63+
void setProxy_withNegativePort_shouldThrowException() {
64+
// Given: Negative port number
65+
// When/Then: Setting proxy should throw exception
66+
assertThatThrownBy(() -> app.setProxy("proxy.example.com:-1"))
67+
.isInstanceOf(IllegalArgumentException.class)
68+
.hasMessageContaining("Invalid proxy port");
69+
}
70+
71+
@Test
72+
void setProxy_withNonNumericPort_shouldThrowException() {
73+
// Given: Non-numeric port
74+
// When/Then: Setting proxy should throw exception
75+
assertThatThrownBy(() -> app.setProxy("proxy.example.com:abc"))
76+
.isInstanceOf(IllegalArgumentException.class)
77+
.hasMessageContaining("Invalid proxy port");
78+
}
79+
80+
@Test
81+
void setProxy_withEmptyHost_shouldThrowException() {
82+
// Given: Empty hostname
83+
// When/Then: Setting proxy should throw exception
84+
assertThatThrownBy(() -> app.setProxy(":8080"))
85+
.isInstanceOf(IllegalArgumentException.class)
86+
.hasMessageContaining("Invalid proxy format");
87+
}
88+
89+
@Test
90+
void setProxy_withWhitespace_shouldTrim() {
91+
// Given: Proxy with whitespace
92+
// When: Setting proxy
93+
app.setProxy(" proxy.example.com:8080 ");
94+
95+
// Then: Should be trimmed
96+
assertThat(app.getProxy()).isEqualTo("proxy.example.com:8080");
97+
}
98+
99+
@Test
100+
void setProxyAuth_withValidCredentials_shouldSucceed() {
101+
// Given: Valid credentials
102+
// When: Setting proxy auth
103+
app.setProxyAuth("username:password");
104+
105+
// Then: Should be set
106+
assertThat(app.getProxyAuth()).isEqualTo("username:password");
107+
}
108+
109+
@Test
110+
void setProxyAuth_withPasswordContainingColon_shouldSucceed() {
111+
// Given: Password containing colon
112+
// When: Setting proxy auth
113+
app.setProxyAuth("user:pass:word");
114+
115+
// Then: Should be set (split on first colon only)
116+
assertThat(app.getProxyAuth()).isEqualTo("user:pass:word");
117+
}
118+
119+
@Test
120+
void setProxyAuth_withoutColon_shouldThrowException() {
121+
// Given: Credentials without colon
122+
// When/Then: Setting proxy auth should throw exception
123+
assertThatThrownBy(() -> app.setProxyAuth("usernamepassword"))
124+
.isInstanceOf(IllegalArgumentException.class)
125+
.hasMessageContaining("Invalid proxy auth format");
126+
}
127+
128+
@Test
129+
void setProxyAuth_withEmptyUsername_shouldThrowException() {
130+
// Given: Empty username
131+
// When/Then: Setting proxy auth should throw exception
132+
assertThatThrownBy(() -> app.setProxyAuth(":password"))
133+
.isInstanceOf(IllegalArgumentException.class)
134+
.hasMessageContaining("Username cannot be empty");
135+
}
136+
137+
@Test
138+
void setProxyAuth_withWhitespace_shouldTrim() {
139+
// Given: Credentials with whitespace
140+
// When: Setting proxy auth
141+
app.setProxyAuth(" user:pass ");
142+
143+
// Then: Should be trimmed
144+
assertThat(app.getProxyAuth()).isEqualTo("user:pass");
145+
}
146+
147+
@Test
148+
void setProxyAuth_withNull_shouldAcceptNull() {
149+
// Given: Null credentials
150+
// When: Setting null
151+
app.setProxyAuth(null);
152+
153+
// Then: Should be null
154+
assertThat(app.getProxyAuth()).isNull();
155+
}
156+
157+
@Test
158+
void setProxy_withNull_shouldAcceptNull() {
159+
// Given: Null proxy
160+
// When: Setting null
161+
app.setProxy(null);
162+
163+
// Then: Should be null
164+
assertThat(app.getProxy()).isNull();
165+
}
166+
}

0 commit comments

Comments
 (0)