Skip to content

Commit 466d96a

Browse files
Strip sensitive headers from output
1 parent 8b85a29 commit 466d96a

9 files changed

Lines changed: 152 additions & 15 deletions

File tree

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,11 @@ private JsonNode _post(String url, List<NameValuePair> postData) throws Exceptio
181181
}
182182

183183
try {
184-
String jsonData = responseBody.replaceAll("\\\\", "");
185-
if (!jsonData.startsWith("{")) {
186-
jsonData = jsonData.substring(1, (jsonData.length() - 1));
184+
JsonNode root = objectMapper.readTree(responseBody);
185+
if (root.isTextual()) {
186+
root = objectMapper.readTree(root.asText());
187187
}
188-
189-
return objectMapper.readTree(jsonData);
188+
return root;
190189
}
191190
catch (Exception e) {
192191
throw new Exception("Json parse error: " + e.getMessage() + " for " + responseBody);
@@ -218,12 +217,11 @@ private JsonNode _get(String url) throws Exception {
218217
}
219218

220219
try {
221-
String jsonData = responseBody.replaceAll("\\\\", "");
222-
if (!jsonData.startsWith("{")) {
223-
jsonData = jsonData.substring(1, (jsonData.length() - 1));
220+
JsonNode root = objectMapper.readTree(responseBody);
221+
if (root.isTextual()) {
222+
root = objectMapper.readTree(root.asText());
224223
}
225-
226-
return objectMapper.readTree(jsonData);
224+
return root;
227225
}
228226
catch (Exception e) {
229227
throw new Exception("Json parse error: " + e.getMessage() + " for " + responseBody);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,12 @@ private void startProxies() {
522522
}
523523

524524
if (!this.noProxy) {
525-
this.httpProxy = new HttpProxy(this);
525+
try {
526+
this.httpProxy = new HttpProxy(this);
527+
} catch (HttpProxy.HttpProxyStartException ex) {
528+
Logger.getLogger(App.class.getName()).log(Level.SEVERE, ex.getMessage());
529+
System.exit(1);
530+
}
526531
if (this.getProxy() == null && !this.httpProxy.testProxy()) {
527532
Logger.getLogger(App.class.getName()).log(Level.INFO, "! Tunnel might not work properly, test failed");
528533
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,15 @@ public void start() {
125125
try {
126126
httpProxy.start();
127127
} catch (Exception ex) {
128-
Logger.getLogger(HttpProxy.class.getName()).log(Level.SEVERE, "Could not set up local http proxy. Please make sure this program can open port {0} on this computer.", Integer.toString(app.getJettyPort()));
129-
System.exit(1);
128+
throw new HttpProxyStartException(
129+
"Could not set up local http proxy. Please make sure this program can open port "
130+
+ app.getJettyPort() + " on this computer.", ex);
131+
}
132+
}
133+
134+
public static final class HttpProxyStartException extends RuntimeException {
135+
public HttpProxyStartException(String message, Throwable cause) {
136+
super(message, cause);
130137
}
131138
}
132139

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
144144

145145
while (headerNames.hasMoreElements()) {
146146
header = headerNames.nextElement();
147-
sb.append(header).append(": ").append(request.getHeader(header)).append(System.lineSeparator());
147+
sb.append(header).append(": ")
148+
.append(SensitiveHeaders.redactValue(header, request.getHeader(header)))
149+
.append(System.lineSeparator());
148150
}
149151
Logger.getLogger(CustomConnectHandler.class.getName()).log(Level.INFO, sb.toString());
150152
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.testingbot.tunnel.proxy;
2+
3+
import java.util.Locale;
4+
import java.util.Set;
5+
6+
final class SensitiveHeaders {
7+
static final String REDACTED = "<redacted>";
8+
9+
private static final Set<String> NAMES = Set.of(
10+
"authorization",
11+
"proxy-authorization",
12+
"cookie",
13+
"set-cookie",
14+
"x-api-key",
15+
"x-auth-token"
16+
);
17+
18+
private SensitiveHeaders() {
19+
}
20+
21+
static boolean isSensitive(String name) {
22+
return name != null && NAMES.contains(name.toLowerCase(Locale.ROOT));
23+
}
24+
25+
static String redactValue(String name, String value) {
26+
return isSensitive(name) ? REDACTED : value;
27+
}
28+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ public void onComplete(Result result) {
117117

118118
while (headerNames.hasMoreElements()) {
119119
header = headerNames.nextElement();
120-
sb.append(header).append(": ").append(request.getHeader(header)).append(System.lineSeparator());
120+
sb.append(header).append(": ")
121+
.append(SensitiveHeaders.redactValue(header, request.getHeader(header)))
122+
.append(System.lineSeparator());
121123
}
122124

123125
Logger.getLogger(TunnelProxyServlet.class.getName()).log(Level.INFO, sb.toString());

src/test/java/com/testingbot/tunnel/ApiTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,36 @@ void api_multipleSequentialCalls_shouldWork() throws Exception {
501501
assertThat(createResult.get("id").asText()).isEqualTo("seq-1");
502502
assertThat(pollResult.get("state").asText()).isEqualTo("RUNNING");
503503
}
504+
505+
@Test
506+
void createTunnel_preservesBackslashesInStringValues() throws Exception {
507+
wireMockServer.stubFor(post(urlPathEqualTo("/v1/tunnel/create"))
508+
.willReturn(aResponse()
509+
.withStatus(200)
510+
.withHeader("Content-Type", "application/json")
511+
.withBody("{\"id\":\"backslash\",\"path\":\"C:\\\\Users\\\\test\",\"message\":\"line1\\nline2\"}")));
512+
513+
api = createApiWithMockServer();
514+
515+
JsonNode result = api.createTunnel();
516+
517+
assertThat(result.get("path").asText()).isEqualTo("C:\\Users\\test");
518+
assertThat(result.get("message").asText()).isEqualTo("line1\nline2");
519+
}
520+
521+
@Test
522+
void createTunnel_handlesJsonEncodedAsString() throws Exception {
523+
wireMockServer.stubFor(post(urlPathEqualTo("/v1/tunnel/create"))
524+
.willReturn(aResponse()
525+
.withStatus(200)
526+
.withHeader("Content-Type", "application/json")
527+
.withBody("\"{\\\"id\\\":\\\"wrapped\\\",\\\"state\\\":\\\"READY\\\"}\"")));
528+
529+
api = createApiWithMockServer();
530+
531+
JsonNode result = api.createTunnel();
532+
533+
assertThat(result.get("id").asText()).isEqualTo("wrapped");
534+
assertThat(result.get("state").asText()).isEqualTo("READY");
535+
}
504536
}

src/test/java/com/testingbot/tunnel/HttpProxyTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import static org.assertj.core.api.Assertions.assertThat;
99
import static org.assertj.core.api.Assertions.assertThatCode;
10+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1011

1112
class HttpProxyTest {
1213

@@ -68,6 +69,16 @@ void testProxy_shouldValidateProxyFunctionality() {
6869
assertThat(result).isFalse(); // Expected in test environment
6970
}
7071

72+
@Test
73+
void start_whenPortAlreadyBound_throwsHttpProxyStartException() throws Exception {
74+
try (ServerSocket blocker = new ServerSocket(0)) {
75+
app.setJettyPort(blocker.getLocalPort());
76+
assertThatThrownBy(() -> new HttpProxy(app))
77+
.isInstanceOf(HttpProxy.HttpProxyStartException.class)
78+
.hasMessageContaining(Integer.toString(blocker.getLocalPort()));
79+
}
80+
}
81+
7182
private int findFreePort() {
7283
try (ServerSocket socket = new ServerSocket(0)) {
7384
return socket.getLocalPort();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.testingbot.tunnel.proxy;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
class SensitiveHeadersTest {
8+
9+
@Test
10+
void isSensitive_isCaseInsensitive() {
11+
assertThat(SensitiveHeaders.isSensitive("Authorization")).isTrue();
12+
assertThat(SensitiveHeaders.isSensitive("AUTHORIZATION")).isTrue();
13+
assertThat(SensitiveHeaders.isSensitive("authorization")).isTrue();
14+
}
15+
16+
@Test
17+
void isSensitive_recognizesKnownHeaders() {
18+
assertThat(SensitiveHeaders.isSensitive("Proxy-Authorization")).isTrue();
19+
assertThat(SensitiveHeaders.isSensitive("Cookie")).isTrue();
20+
assertThat(SensitiveHeaders.isSensitive("Set-Cookie")).isTrue();
21+
assertThat(SensitiveHeaders.isSensitive("X-Api-Key")).isTrue();
22+
assertThat(SensitiveHeaders.isSensitive("X-Auth-Token")).isTrue();
23+
}
24+
25+
@Test
26+
void isSensitive_returnsFalseForOrdinaryHeaders() {
27+
assertThat(SensitiveHeaders.isSensitive("User-Agent")).isFalse();
28+
assertThat(SensitiveHeaders.isSensitive("Content-Type")).isFalse();
29+
assertThat(SensitiveHeaders.isSensitive("Host")).isFalse();
30+
}
31+
32+
@Test
33+
void isSensitive_handlesNull() {
34+
assertThat(SensitiveHeaders.isSensitive(null)).isFalse();
35+
}
36+
37+
@Test
38+
void redactValue_masksSensitiveValue() {
39+
assertThat(SensitiveHeaders.redactValue("Authorization", "Bearer secret"))
40+
.isEqualTo(SensitiveHeaders.REDACTED);
41+
assertThat(SensitiveHeaders.redactValue("Cookie", "sid=abc"))
42+
.isEqualTo(SensitiveHeaders.REDACTED);
43+
}
44+
45+
@Test
46+
void redactValue_passesOrdinaryHeaderThrough() {
47+
assertThat(SensitiveHeaders.redactValue("User-Agent", "curl/8"))
48+
.isEqualTo("curl/8");
49+
assertThat(SensitiveHeaders.redactValue("Content-Type", "text/plain"))
50+
.isEqualTo("text/plain");
51+
}
52+
}

0 commit comments

Comments
 (0)