Skip to content

Commit 8b85a29

Browse files
improve basic auth handling
1 parent 108d1a0 commit 8b85a29

3 files changed

Lines changed: 119 additions & 2 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ private String[] getUserData() {
388388
try (BufferedReader br = Files.newBufferedReader(dataFile)) {
389389
String strLine = br.readLine();
390390
if (strLine != null) {
391-
return strLine.split(":");
391+
return strLine.split(":", 2);
392392
}
393393
} catch (IOException e) {
394394
Logger.getLogger(App.class.getName()).log(Level.WARNING, "Could not read credentials from .testingbot file", e);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ protected HttpClient newHttpClient() {
204204
if (basicAuthString != null) {
205205
final String[] basicAuth = basicAuthString.split(",");
206206
for (String authCredentials : basicAuth) {
207-
String[] credentials = authCredentials.split(":");
207+
String[] credentials = authCredentials.split(":", 4);
208208
if (credentials.length < 4) {
209209
Logger.getLogger(TunnelProxyServlet.class.getName()).log(Level.WARNING, "Invalid basic auth format, expected host:port:user:password");
210210
continue;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.testingbot.tunnel.proxy;
2+
3+
import jakarta.servlet.ServletConfig;
4+
import jakarta.servlet.ServletContext;
5+
6+
import org.eclipse.jetty.client.HttpClient;
7+
import org.eclipse.jetty.client.api.Authentication;
8+
import org.eclipse.jetty.client.api.AuthenticationStore;
9+
import org.eclipse.jetty.client.util.BasicAuthentication;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.lang.reflect.Field;
13+
import java.net.URI;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.when;
18+
19+
/**
20+
* --basic-auth uses host:port:user:password. The password is the rest of the
21+
* string after the third colon and must be allowed to contain colons itself.
22+
*/
23+
class BasicAuthTest {
24+
25+
private static class StubbedServlet extends TunnelProxyServlet {
26+
private final ServletConfig cfg;
27+
28+
StubbedServlet(ServletConfig cfg) {
29+
this.cfg = cfg;
30+
}
31+
32+
@Override
33+
public ServletConfig getServletConfig() {
34+
return cfg;
35+
}
36+
37+
HttpClient buildHttpClient() {
38+
return newHttpClient();
39+
}
40+
}
41+
42+
private static ServletConfig mockServletConfig(String basicAuth) {
43+
ServletConfig cfg = mock(ServletConfig.class);
44+
ServletContext ctx = mock(ServletContext.class);
45+
when(cfg.getServletContext()).thenReturn(ctx);
46+
when(cfg.getInitParameter("basicAuth")).thenReturn(basicAuth);
47+
when(cfg.getInitParameter("proxy")).thenReturn(null);
48+
when(cfg.getInitParameter("proxyAuth")).thenReturn(null);
49+
when(cfg.getInitParameter("blackList")).thenReturn(null);
50+
return cfg;
51+
}
52+
53+
private static BasicAuthentication findBasicAuth(HttpClient client, URI uri) {
54+
AuthenticationStore store = client.getAuthenticationStore();
55+
Authentication found = store.findAuthentication("Basic", uri, Authentication.ANY_REALM);
56+
assertThat(found)
57+
.as("AuthenticationStore should contain a Basic credential for %s", uri)
58+
.isInstanceOf(BasicAuthentication.class);
59+
return (BasicAuthentication) found;
60+
}
61+
62+
private static String readPrivate(Object target, String field) throws Exception {
63+
Field f = target.getClass().getDeclaredField(field);
64+
f.setAccessible(true);
65+
return (String) f.get(target);
66+
}
67+
68+
@Test
69+
void passwordContainingColons_isNotTruncated() throws Exception {
70+
String user = "alice";
71+
String password = "pa:ss:word";
72+
ServletConfig cfg = mockServletConfig("backend.example:8080:" + user + ":" + password);
73+
74+
StubbedServlet servlet = new StubbedServlet(cfg);
75+
HttpClient client = servlet.buildHttpClient();
76+
try {
77+
BasicAuthentication auth = findBasicAuth(client, URI.create("http://backend.example:8080/anything"));
78+
assertThat(readPrivate(auth, "user")).isEqualTo(user);
79+
assertThat(readPrivate(auth, "password")).isEqualTo(password);
80+
} finally {
81+
client.destroy();
82+
}
83+
}
84+
85+
@Test
86+
void multipleEntries_eachKeepsItsOwnColons() throws Exception {
87+
ServletConfig cfg = mockServletConfig(
88+
"host-a:80:user-a:pwd:with:colons,host-b:443:user-b:plain");
89+
90+
StubbedServlet servlet = new StubbedServlet(cfg);
91+
HttpClient client = servlet.buildHttpClient();
92+
try {
93+
BasicAuthentication a = findBasicAuth(client, URI.create("http://host-a:80/x"));
94+
assertThat(readPrivate(a, "password")).isEqualTo("pwd:with:colons");
95+
96+
BasicAuthentication b = findBasicAuth(client, URI.create("http://host-b:443/x"));
97+
assertThat(readPrivate(b, "password")).isEqualTo("plain");
98+
} finally {
99+
client.destroy();
100+
}
101+
}
102+
103+
@Test
104+
void underSpecifiedEntry_isSkipped() {
105+
ServletConfig cfg = mockServletConfig("only:three:fields");
106+
107+
StubbedServlet servlet = new StubbedServlet(cfg);
108+
HttpClient client = servlet.buildHttpClient();
109+
try {
110+
Authentication found = client.getAuthenticationStore()
111+
.findAuthentication("Basic", URI.create("http://only:80/"), Authentication.ANY_REALM);
112+
assertThat(found).isNull();
113+
} finally {
114+
client.destroy();
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)