Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/main/java/com/gpuflight/agent/publisher/HttpPublisher.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public boolean publish(String topic, String key, LogWrapper log) {
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
System.out.println("HTTP Connection error: " + e);
logConnectionError("HTTP event", e);
return false;
}
}
Expand All @@ -88,7 +88,7 @@ public boolean publishStream(String sessionId, List<String> ndjsonLines) {
+ " session=" + sessionId + " lines=" + ndjsonLines.size() + " gzipBytes=" + body.length);
return postGzStream(sessionId, body);
} catch (Exception e) {
System.out.println("HTTP stream connection error: " + e);
logConnectionError("HTTP stream", e);
return false;
}
}
Expand Down Expand Up @@ -149,7 +149,7 @@ private boolean postGzStream(String sessionId, byte[] gzBody) {
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
System.out.println("HTTP stream connection error: " + e);
logConnectionError("HTTP stream", e);
return false;
}
}
Expand Down Expand Up @@ -188,17 +188,30 @@ public boolean publishSessionComplete(String sessionId) {
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
System.out.println("HTTP session-complete connection error: " + e);
logConnectionError("HTTP session-complete", e);
return false;
}
}

private void addAuthHeader(HttpRequest.Builder requestBuilder) {
if (config.authToken() != null && !config.authToken().isBlank()) {
requestBuilder.header("Authorization", "Bearer " + config.authToken());
// Environment/config-file values can acquire surrounding CR/LF or
// whitespace while crossing shells. API keys never contain them,
// and passing them through makes HttpClient reject the header.
requestBuilder.header("Authorization", "Bearer " + config.authToken().trim());
}
}

private static void logConnectionError(String operation, Exception error) {
// HttpClient's IllegalArgumentException includes the rejected header
// value. Printing the exception/message here can therefore disclose a
// complete Authorization bearer token. The exception type is enough
// to distinguish malformed requests from transport failures without
// putting credentials in logs.
System.out.println(operation + " connection error ("
+ error.getClass().getSimpleName() + ")");
}

private static byte[] gzip(byte[] content) throws Exception {
var out = new ByteArrayOutputStream();
try (var gzip = new GZIPOutputStream(out)) {
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/com/gpuflight/agent/publisher/HttpPublisherTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
Expand Down Expand Up @@ -110,6 +112,43 @@ void publish_sendsAuthorizationHeader() throws InterruptedException {
assertEquals("Bearer gpfl_tok123", lastAuthHeader.get());
}

@Test
void publishStream_trimsTransportWhitespaceFromAuthorizationToken() {
HttpConfig config = new HttpConfig(hostUrl(), "v1", " \tgpfl_tok123\r\n", 5,
"stream", 100, 1_000_000L);
HttpPublisher pub = new HttpPublisher(config);

boolean ok = pub.publishStream("session-1",
List.of("{\"type\":\"kernel_event\",\"session_id\":\"session-1\"}"));

assertTrue(ok);
assertEquals("Bearer gpfl_tok123", lastAuthHeader.get());
}

@Test
void publishStream_malformedAuthorizationNeverLeaksTokenInLogs() throws IOException {
String secret = "gpfl_secret\rleak";
HttpConfig config = new HttpConfig(hostUrl(), "v1", secret, 5,
"stream", 100, 1_000_000L);
HttpPublisher pub = new HttpPublisher(config);
PrintStream originalOut = System.out;
var captured = new ByteArrayOutputStream();
boolean ok;
try {
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
ok = pub.publishStreamGz("session-1",
gzipBytes("{\"type\":\"kernel_event\"}\n"));
} finally {
System.setOut(originalOut);
}

String output = captured.toString(StandardCharsets.UTF_8);
assertFalse(ok);
assertTrue(output.contains("IllegalArgumentException"));
assertFalse(output.contains(secret));
assertFalse(output.contains("Bearer gpfl_secret"));
}

@Test
void publish_noToken_doesNotSendAuthHeader() throws InterruptedException {
HttpConfig config = new HttpConfig(hostUrl(), "v1", null, 5);
Expand Down
Loading