From 40fa58f2f54137cfd6b41217dbb7f1f902563792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Leurent?= <131.js@leurent.email> Date: Fri, 25 Jun 2021 13:55:16 +0200 Subject: [PATCH 1/4] With openssh_authAgent & tests --- README.md | 3 +++ lib/server.js | 9 +++++++++ test/test-openssh.js | 31 +++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91007b69..35bae173 100644 --- a/README.md +++ b/README.md @@ -1180,6 +1180,9 @@ You can find more examples in the `examples` directory of this repository. * **forwardOut**(< _string_ >boundAddr, < _integer_ >boundPort, < _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Alert the client of an incoming TCP connection on `boundAddr` on port `boundPort` from `remoteAddr` on port `remotePort`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. +* **openssh_authAgent**(< _function_ >callback) - _boolean_ - Alert the client of an incoming `ssh-agent` socket connection. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. + + * **openssh_forwardOutStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _(void)_ - Alert the client of an incoming UNIX domain socket connection on `socketPath`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. * **rekey**([< _function_ >callback]) - _(void)_ - Initiates a rekey with the client. If `callback` is supplied, it is added as a one-time handler for the `rekey` event. diff --git a/lib/server.js b/lib/server.js index 9137cdf9..c3a05f2b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1284,6 +1284,12 @@ class Client extends EventEmitter { return this; } + + openssh_authAgent(cb) { + openChannel(this, 'auth-agent@openssh.com', cb); + return this; + } + openssh_forwardOutStreamLocal(socketPath, cb) { const opts = { socketPath }; openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb); @@ -1341,6 +1347,9 @@ function openChannel(self, type, opts, cb) { case 'x11': self._protocol.x11(localChan, initWindow, maxPacket, opts); break; + case 'auth-agent@openssh.com': + self._protocol.openssh_authAgent(localChan, initWindow, maxPacket); + break; case 'forwarded-streamlocal@openssh.com': self._protocol.openssh_forwardedStreamLocal( localChan, initWindow, maxPacket, opts diff --git a/test/test-openssh.js b/test/test-openssh.js index 67e1b860..4c59f1ef 100644 --- a/test/test-openssh.js +++ b/test/test-openssh.js @@ -2,6 +2,7 @@ const assert = require('assert'); const { inspect } = require('util'); +const { spawn } = require('child_process'); const { fixture, @@ -12,16 +13,27 @@ const { const debug = false; +const test_forward = (process.platform !== 'win32'); + +if (!test_forward) + console.log('Skipping agent forwarding test on Windows'); + + const clientCfg = { username: 'foo', password: 'bar' }; const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; { + const agent_sock = '/tmp/nodejs-ssh2-test-' + process.pid; + let agent; + if (test_forward) + agent = spawn('ssh-agent', ['-d', '-a', agent_sock]); + const { client, server } = setup_( 'Exec with OpenSSH agent forwarding', { client: { ...clientCfg, - agent: '/path/to/agent', + agent: agent_sock, }, server: serverCfg, @@ -45,8 +57,23 @@ const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; const stream = accept(); stream.exit(100); stream.end(); - conn.end(); + + if (test_forward) { + conn.openssh_authAgent(function(err, stream) { + assert(!err, `Unexpected openssh_authAgent error: ${err}`); + assert(stream.type === 'auth-agent@openssh.com', + `Unexpected openssh_authAgent channel type : ${stream.type}`); + + conn.end(); + agent.kill(); + }); + + } else { + conn.end(); + } + })); + })); })); })); From c31182fc06db900839ed1e19aed32123b951616f Mon Sep 17 00:00:00 2001 From: Nathan Erickson Date: Fri, 10 Oct 2025 14:02:26 -0400 Subject: [PATCH 2/4] Adjusting test for openssh to use a server instead of spawning a full agent --- test/test-openssh.js | 60 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/test/test-openssh.js b/test/test-openssh.js index 4c59f1ef..3519e36d 100644 --- a/test/test-openssh.js +++ b/test/test-openssh.js @@ -3,6 +3,8 @@ const assert = require('assert'); const { inspect } = require('util'); const { spawn } = require('child_process'); +const net = require('net'); +const fs = require('fs'); const { fixture, @@ -13,27 +15,44 @@ const { const debug = false; -const test_forward = (process.platform !== 'win32'); - -if (!test_forward) - console.log('Skipping agent forwarding test on Windows'); - - const clientCfg = { username: 'foo', password: 'bar' }; const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; { - const agent_sock = '/tmp/nodejs-ssh2-test-' + process.pid; - let agent; - if (test_forward) - agent = spawn('ssh-agent', ['-d', '-a', agent_sock]); + + const agentSockPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\nodejs-ssh2-test-${process.pid}` + : `${os.tmpdir()}${ + path.sep + }nodejs-ssh2-test-${process.pid}.sock`; + + if (fs.existsSync(agentSockPath)) { + fs.unlinkSync(agentSockPath); + } + + const agent = net.createServer((agentSocket) => { + log.info('Agent proxy socket received connection', { + sessionId, + }); + assert(!agentSocket.readable, 'Agent socket not readable'); + }).listen(agentSockPath, () => { + const cleanup = () => { + server.close(() => { + if (fs.existsSync(agentSockPath)) { + fs.unlinkSync(agentSockPath); + } + }); + }; + process.on('exit', cleanup); + }); const { client, server } = setup_( 'Exec with OpenSSH agent forwarding', { client: { ...clientCfg, - agent: agent_sock, + agent: agentSockPath, }, server: serverCfg, @@ -58,19 +77,14 @@ const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; stream.exit(100); stream.end(); - if (test_forward) { - conn.openssh_authAgent(function(err, stream) { - assert(!err, `Unexpected openssh_authAgent error: ${err}`); - assert(stream.type === 'auth-agent@openssh.com', - `Unexpected openssh_authAgent channel type : ${stream.type}`); - - conn.end(); - agent.kill(); - }); + conn.openssh_authAgent(function(err, stream) { + assert(!err, `Unexpected openssh_authAgent error: ${err}`); + assert(stream.type === 'auth-agent@openssh.com', + `Unexpected openssh_authAgent channel type : ${stream.type}`); - } else { - conn.end(); - } + conn.end(); + agent.kill(); + }); })); From c6aff62af369c8e74861ce1c20e61e785d20422c Mon Sep 17 00:00:00 2001 From: Nathan Erickson Date: Fri, 10 Oct 2025 14:08:24 -0400 Subject: [PATCH 3/4] Removing test log line in openssh test --- test/test-openssh.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test-openssh.js b/test/test-openssh.js index 3519e36d..bd405852 100644 --- a/test/test-openssh.js +++ b/test/test-openssh.js @@ -32,9 +32,6 @@ const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; } const agent = net.createServer((agentSocket) => { - log.info('Agent proxy socket received connection', { - sessionId, - }); assert(!agentSocket.readable, 'Agent socket not readable'); }).listen(agentSockPath, () => { const cleanup = () => { From 303196913dbfe90a402a940d07486d10b1824ea4 Mon Sep 17 00:00:00 2001 From: Nathan Erickson Date: Tue, 7 Apr 2026 13:20:46 -0400 Subject: [PATCH 4/4] test: add test for server-initiated openssh_authAgent channel --- test/test-openssh.js | 117 ++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/test/test-openssh.js b/test/test-openssh.js index bd405852..e27ba71f 100644 --- a/test/test-openssh.js +++ b/test/test-openssh.js @@ -1,10 +1,9 @@ 'use strict'; const assert = require('assert'); -const { inspect } = require('util'); -const { spawn } = require('child_process'); -const net = require('net'); const fs = require('fs'); +const net = require('net'); +const { inspect } = require('util'); const { fixture, @@ -19,37 +18,12 @@ const clientCfg = { username: 'foo', password: 'bar' }; const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; { - - const agentSockPath = - process.platform === 'win32' - ? `\\\\.\\pipe\\nodejs-ssh2-test-${process.pid}` - : `${os.tmpdir()}${ - path.sep - }nodejs-ssh2-test-${process.pid}.sock`; - - if (fs.existsSync(agentSockPath)) { - fs.unlinkSync(agentSockPath); - } - - const agent = net.createServer((agentSocket) => { - assert(!agentSocket.readable, 'Agent socket not readable'); - }).listen(agentSockPath, () => { - const cleanup = () => { - server.close(() => { - if (fs.existsSync(agentSockPath)) { - fs.unlinkSync(agentSockPath); - } - }); - }; - process.on('exit', cleanup); - }); - const { client, server } = setup_( 'Exec with OpenSSH agent forwarding', { client: { ...clientCfg, - agent: agentSockPath, + agent: '/path/to/agent', }, server: serverCfg, @@ -73,18 +47,8 @@ const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; const stream = accept(); stream.exit(100); stream.end(); - - conn.openssh_authAgent(function(err, stream) { - assert(!err, `Unexpected openssh_authAgent error: ${err}`); - assert(stream.type === 'auth-agent@openssh.com', - `Unexpected openssh_authAgent channel type : ${stream.type}`); - - conn.end(); - agent.kill(); - }); - + conn.end(); })); - })); })); })); @@ -297,3 +261,76 @@ const serverCfg = { hostKeys: [ fixture('ssh_host_rsa_key') ] }; accept(); })); } + +{ + const { join } = require('path'); + const { tmpdir } = require('os'); + + const agentSockPath = process.platform === 'win32' + ? `\\\\.\\pipe\\ssh2-test-agent-${process.pid}` + : join(tmpdir(), `ssh2-test-agent-${process.pid}.sock`); + + const isWin = process.platform === 'win32'; + + if (!isWin && fs.existsSync(agentSockPath)) + fs.unlinkSync(agentSockPath); + + // Fake agent: echoes back whatever it receives + const agentServer = net.createServer(mustCall((sock) => { + sock.on('data', mustCallAtLeast((data) => { + sock.write(data); + })); + })); + + agentServer.listen(agentSockPath); + + const { client, server } = setup_( + 'Server-initiated openssh_authAgent channel', + { + client: { + ...clientCfg, + agent: agentSockPath, + }, + server: serverCfg, + debug, + }, + ); + + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + ctx.accept(); + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + accept().on('auth-agent', mustCall((accept, reject) => { + accept && accept(); + })).on('exec', mustCall((accept, reject, info) => { + const stream = accept(); + stream.exit(0); + stream.end(); + + conn.openssh_authAgent(mustCall((err, agentStream) => { + assert(!err, `Unexpected openssh_authAgent error: ${err}`); + + const testData = 'agent-ping'; + agentStream.on('data', mustCallAtLeast((data) => { + assert(data.toString() === testData, + `Expected '${testData}', got: '${data}'`); + conn.end(); + agentServer.close(); + if (!isWin && fs.existsSync(agentSockPath)) + fs.unlinkSync(agentSockPath); + })); + agentStream.write(testData); + })); + })); + })); + })); + })); + + client.on('ready', mustCall(() => { + client.exec('foo', { agentForward: true }, mustCall((err, stream) => { + assert(!err, `Unexpected exec error: ${err}`); + stream.resume(); + })); + })); +}