Version
7.5.0 (org.eclipse.jgit.ssh.apache 7.5.0.202512021534-r). The same code is also present on current master.
Operating System
Windows
Bug description
When tunneling SSH through an HTTP proxy (Proxy.Type.HTTP), HttpClientConnector.messageReceived(...) reads and parses all currently available bytes as the HTTP CONNECT response. Any bytes that follow the terminating \r\n\r\n of the proxy reply — i.e. the SSH server's identification banner (SSH-2.0-...) and the start of its KEX_INIT — are consumed into the HTTP string parsing and never handed back to the SSH transport layer.
If the proxy's HTTP/1.0 200 Connection established\r\n\r\n and the upstream server's banner arrive in the same Readable (the same TCP segment), the banner is lost and the SSH handshake stalls. This is timing-dependent: when the 200 reply and the banner happen to arrive in separate reads it works, which makes the failure intermittent and proxy/network dependent.
Root cause — org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector#messageReceived:
int length = buffer.available();
byte[] data = new byte[length];
buffer.getRawBytes(data, 0, length); // consumes ALL available bytes
String[] reply = new String(data, US_ASCII).split("\r\n");
handleMessage(session, Arrays.asList(reply)); // on 200 -> setDone(true)
It never stops at the end of the HTTP headers nor re-injects the bytes after them. After setDone(true), AbstractClientProxyConnector clears the proxy handler (session.setProxyHandler(null)) and fires the deferred sendIdentification queued by JGitClientSession#sendIdentification via runWhenDone(...). From then on JGitClientSession#messageReceived would correctly route bytes to ClientSessionImpl#messageReceived — but the server banner that was already pulled out of the same Readable is gone.
How to reproduce:
- Configure an
SshSessionFactory with an HTTP proxy (ProxyData, Proxy.Type.HTTP) and connect to any SSH/SFTP server through it.
- Use (or simulate) a proxy that writes the
200 status/headers and the first relayed upstream bytes in a single TCP segment.
Deterministic harness: a fake CONNECT proxy that, after accepting CONNECT host:port, performs a single write() of "HTTP/1.0 200 Connection established\r\n\r\n" immediately followed by the bytes it just read from the upstream server (the SSH-2.0-... banner). The connector then receives both in one messageReceived call and drops the banner 100% of the time.
Actual behavior
The bytes following the proxy's 200 response (the SSH server's SSH-2.0-... banner / KEX_INIT) are discarded by HttpClientConnector#messageReceived. The SSH transport never receives the server's identification string, so the protocol-version exchange / key exchange never completes and the connect (and subsequent auth) eventually times out.
Expected behavior
HttpClientConnector should consume only up to the end of the HTTP CONNECT response headers (\r\n\r\n) and re-inject any remaining bytes into the SSH layer, so the server banner that was coalesced into the same TCP segment is processed and the handshake proceeds normally.
Relevant log output
Other information
Suggested fix (sketch):
int length = buffer.available();
byte[] data = new byte[length];
buffer.getRawBytes(data, 0, length);
int bodyStart = endOfHeaders(data); // index just after "\r\n\r\n", or -1
int headerLen = (bodyStart < 0) ? length : bodyStart;
String[] reply = new String(data, 0, headerLen, US_ASCII).split("\r\n");
handleMessage(session, Arrays.asList(reply)); // on 200 -> setDone(true) clears proxyHandler
if (bodyStart >= 0 && bodyStart < length) {
byte[] leftover = Arrays.copyOfRange(data, bodyStart, length);
// proxyHandler is now null, so this is decoded by the normal SSH path
AbstractSession.getSession(session)
.messageReceived(new ByteArrayBuffer(leftover));
}
When \r\n\r\n is not yet present (headers split across reads), behavior should stay as it is today.
Open question: Socks5ClientConnector may share the same "consume everything currently in the buffer" pattern and could drop coalesced post-handshake bytes as well — worth auditing alongside this fix.
Environment: reproduced with org.eclipse.jgit.ssh.apache 7.5.0 + Apache MINA sshd 2.15.0 on JDK 21.
Version
7.5.0 (org.eclipse.jgit.ssh.apache 7.5.0.202512021534-r). The same code is also present on current master.
Operating System
Windows
Bug description
When tunneling SSH through an HTTP proxy (
Proxy.Type.HTTP),HttpClientConnector.messageReceived(...)reads and parses all currently available bytes as the HTTP CONNECT response. Any bytes that follow the terminating\r\n\r\nof the proxy reply — i.e. the SSH server's identification banner (SSH-2.0-...) and the start of itsKEX_INIT— are consumed into the HTTP string parsing and never handed back to the SSH transport layer.If the proxy's
HTTP/1.0 200 Connection established\r\n\r\nand the upstream server's banner arrive in the sameReadable(the same TCP segment), the banner is lost and the SSH handshake stalls. This is timing-dependent: when the200reply and the banner happen to arrive in separate reads it works, which makes the failure intermittent and proxy/network dependent.Root cause —
org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector#messageReceived:It never stops at the end of the HTTP headers nor re-injects the bytes after them. After
setDone(true),AbstractClientProxyConnectorclears the proxy handler (session.setProxyHandler(null)) and fires the deferredsendIdentificationqueued byJGitClientSession#sendIdentificationviarunWhenDone(...). From then onJGitClientSession#messageReceivedwould correctly route bytes toClientSessionImpl#messageReceived— but the server banner that was already pulled out of the sameReadableis gone.How to reproduce:
SshSessionFactorywith an HTTP proxy (ProxyData,Proxy.Type.HTTP) and connect to any SSH/SFTP server through it.200status/headers and the first relayed upstream bytes in a single TCP segment.Deterministic harness: a fake CONNECT proxy that, after accepting
CONNECT host:port, performs a singlewrite()of"HTTP/1.0 200 Connection established\r\n\r\n"immediately followed by the bytes it just read from the upstream server (theSSH-2.0-...banner). The connector then receives both in onemessageReceivedcall and drops the banner 100% of the time.Actual behavior
The bytes following the proxy's
200response (the SSH server'sSSH-2.0-...banner /KEX_INIT) are discarded byHttpClientConnector#messageReceived. The SSH transport never receives the server's identification string, so the protocol-version exchange / key exchange never completes and the connect (and subsequent auth) eventually times out.Expected behavior
HttpClientConnectorshould consume only up to the end of the HTTP CONNECT response headers (\r\n\r\n) and re-inject any remaining bytes into the SSH layer, so the server banner that was coalesced into the same TCP segment is processed and the handshake proceeds normally.Relevant log output
Other information
Suggested fix (sketch):
When
\r\n\r\nis not yet present (headers split across reads), behavior should stay as it is today.Open question:
Socks5ClientConnectormay share the same "consume everything currently in the buffer" pattern and could drop coalesced post-handshake bytes as well — worth auditing alongside this fix.Environment: reproduced with org.eclipse.jgit.ssh.apache 7.5.0 + Apache MINA sshd 2.15.0 on JDK 21.