mscdex / ssh2

SSH2 client and server modules written in pure JavaScript for node.js
MIT License
5.54k stars 669 forks source link

Description

SSH2 client and server modules written in pure JavaScript for node.js.

Development/testing is done against OpenSSH (8.7 currently).

Changes (breaking or otherwise) in v1.0.0 can be found here.

Table of Contents

Requirements

Installation

npm install ssh2

Client Examples

Execute 'uptime' on a server

const { readFileSync } = require('fs');

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  console.log('Client :: ready');
  conn.exec('uptime', (err, stream) => {
    if (err) throw err;
    stream.on('close', (code, signal) => {
      console.log('Stream :: close :: code: ' + code + ', signal: ' + signal);
      conn.end();
    }).on('data', (data) => {
      console.log('STDOUT: ' + data);
    }).stderr.on('data', (data) => {
      console.log('STDERR: ' + data);
    });
  });
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  privateKey: readFileSync('/path/to/my/key')
});

// example output:
// Client :: ready
// STDOUT:  17:41:15 up 22 days, 18:09,  1 user,  load average: 0.00, 0.01, 0.05
//
// Stream :: exit :: code: 0, signal: undefined
// Stream :: close

Start an interactive shell session

const { readFileSync } = require('fs');

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  console.log('Client :: ready');
  conn.shell((err, stream) => {
    if (err) throw err;
    stream.on('close', () => {
      console.log('Stream :: close');
      conn.end();
    }).on('data', (data) => {
      console.log('OUTPUT: ' + data);
    });
    stream.end('ls -l\nexit\n');
  });
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  privateKey: readFileSync('/path/to/my/key')
});

// example output:
// Client :: ready
// STDOUT: Last login: Sun Jun 15 09:37:21 2014 from 192.168.100.100
//
// STDOUT: ls -l
// exit
//
// STDOUT: frylock@athf:~$ ls -l
//
// STDOUT: total 8
//
// STDOUT: drwxr-xr-x 2 frylock frylock 4096 Nov 18  2012 mydir
//
// STDOUT: -rw-r--r-- 1 frylock frylock   25 Apr 11  2013 test.txt
//
// STDOUT: frylock@athf:~$ exit
//
// STDOUT: logout
//
// Stream :: close

Send a raw HTTP request to port 80 on the server

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  console.log('Client :: ready');
  conn.forwardOut('192.168.100.102', 8000, '127.0.0.1', 80, (err, stream) => {
    if (err) throw err;
    stream.on('close', () => {
      console.log('TCP :: CLOSED');
      conn.end();
    }).on('data', (data) => {
      console.log('TCP :: DATA: ' + data);
    }).end([
      'HEAD / HTTP/1.1',
      'User-Agent: curl/7.27.0',
      'Host: 127.0.0.1',
      'Accept: */*',
      'Connection: close',
      '',
      ''
    ].join('\r\n'));
  });
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  password: 'nodejsrules'
});

// example output:
// Client :: ready
// TCP :: DATA: HTTP/1.1 200 OK
// Date: Thu, 15 Nov 2012 13:52:58 GMT
// Server: Apache/2.2.22 (Ubuntu)
// X-Powered-By: PHP/5.4.6-1ubuntu1
// Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT
// Content-Encoding: gzip
// Vary: Accept-Encoding
// Connection: close
// Content-Type: text/html; charset=UTF-8
//
//
// TCP :: CLOSED

Forward local connections to port 8000 on the server to us

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  console.log('Client :: ready');
  conn.forwardIn('127.0.0.1', 8000, (err) => {
    if (err) throw err;
    console.log('Listening for connections on server on port 8000!');
  });
}).on('tcp connection', (info, accept, reject) => {
  console.log('TCP :: INCOMING CONNECTION:');
  console.dir(info);
  accept().on('close', () => {
    console.log('TCP :: CLOSED');
  }).on('data', (data) => {
    console.log('TCP :: DATA: ' + data);
  }).end([
    'HTTP/1.1 404 Not Found',
    'Date: Thu, 15 Nov 2012 02:07:58 GMT',
    'Server: ForwardedConnection',
    'Content-Length: 0',
    'Connection: close',
    '',
    ''
  ].join('\r\n'));
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  password: 'nodejsrules'
});

// example output:
// Client :: ready
// Listening for connections on server on port 8000!
//  (.... then from another terminal on the server: `curl -I http://127.0.0.1:8000`)
// TCP :: INCOMING CONNECTION: { destIP: '127.0.0.1',
//  destPort: 8000,
//  srcIP: '127.0.0.1',
//  srcPort: 41969 }
// TCP DATA: HEAD / HTTP/1.1
// User-Agent: curl/7.27.0
// Host: 127.0.0.1:8000
// Accept: */*
//
//
// TCP :: CLOSED

Get a directory listing via SFTP

const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  console.log('Client :: ready');
  conn.sftp((err, sftp) => {
    if (err) throw err;
    sftp.readdir('foo', (err, list) => {
      if (err) throw err;
      console.dir(list);
      conn.end();
    });
  });
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  password: 'nodejsrules'
});

// example output:
// Client :: ready
// [ { filename: 'test.txt',
//     longname: '-rw-r--r--    1 frylock   frylock         12 Nov 18 11:05 test.txt',
//     attrs:
//      { size: 12,
//        uid: 1000,
//        gid: 1000,
//        mode: 33188,
//        atime: 1353254750,
//        mtime: 1353254744 } },
//   { filename: 'mydir',
//     longname: 'drwxr-xr-x    2 frylock   frylock       4096 Nov 18 15:03 mydir',
//     attrs:
//      { size: 1048576,
//        uid: 1000,
//        gid: 1000,
//        mode: 16877,
//        atime: 1353269007,
//        mtime: 1353269007 } } ]

Connection hopping

const { Client } = require('ssh2');

const conn1 = new Client();
const conn2 = new Client();

// Checks uptime on 10.1.1.40 via 192.168.1.1

conn1.on('ready', () => {
  console.log('FIRST :: connection ready');
  // Alternatively, you could use something like netcat or socat with exec()
  // instead of forwardOut(), depending on what the server allows
  conn1.forwardOut('127.0.0.1', 12345, '10.1.1.40', 22, (err, stream) => {
    if (err) {
      console.log('FIRST :: forwardOut error: ' + err);
      return conn1.end();
    }
    conn2.connect({
      sock: stream,
      username: 'user2',
      password: 'password2',
    });
  });
}).connect({
  host: '192.168.1.1',
  username: 'user1',
  password: 'password1',
});

conn2.on('ready', () => {
  // This connection is the one to 10.1.1.40

  console.log('SECOND :: connection ready');
  conn2.exec('uptime', (err, stream) => {
    if (err) {
      console.log('SECOND :: exec error: ' + err);
      return conn1.end();
    }
    stream.on('close', () => {
      conn1.end(); // close parent (and this) connection
    }).on('data', (data) => {
      console.log(data.toString());
    });
  });
});

Forward remote X11 connections

const { Socket } = require('net');

const { Client } = require('ssh2');

const conn = new Client();

conn.on('x11', (info, accept, reject) => {
  const xserversock = new net.Socket();
  xserversock.on('connect', () => {
    const xclientsock = accept();
    xclientsock.pipe(xserversock).pipe(xclientsock);
  });
  // connects to localhost:0.0
  xserversock.connect(6000, 'localhost');
});

conn.on('ready', () => {
  conn.exec('xeyes', { x11: true }, (err, stream) => {
    if (err) throw err;
    let code = 0;
    stream.on('close', () => {
      if (code !== 0)
        console.log('Do you have X11 forwarding enabled on your SSH server?');
      conn.end();
    }).on('exit', (exitcode) => {
      code = exitcode;
    });
  });
}).connect({
  host: '192.168.1.1',
  username: 'foo',
  password: 'bar'
});

Dynamic (1:1) port forwarding using a SOCKSv5 proxy (using socksv5)

const socks = require('socksv5');
const { Client } = require('ssh2');

const sshConfig = {
  host: '192.168.100.1',
  port: 22,
  username: 'nodejs',
  password: 'rules'
};

socks.createServer((info, accept, deny) => {
  // NOTE: you could just use one ssh2 client connection for all forwards, but
  // you could run into server-imposed limits if you have too many forwards open
  // at any given time
  const conn = new Client();
  conn.on('ready', () => {
    conn.forwardOut(info.srcAddr,
                    info.srcPort,
                    info.dstAddr,
                    info.dstPort,
                    (err, stream) => {
      if (err) {
        conn.end();
        return deny();
      }

      const clientSocket = accept(true);
      if (clientSocket) {
        stream.pipe(clientSocket).pipe(stream).on('close', () => {
          conn.end();
        });
      } else {
        conn.end();
      }
    });
  }).on('error', (err) => {
    deny();
  }).connect(sshConfig);
}).listen(1080, 'localhost', () => {
  console.log('SOCKSv5 proxy server started on port 1080');
}).useAuth(socks.auth.None());

// test with cURL:
//   curl -i --socks5 localhost:1080 google.com

Make HTTP(S) connections easily using a custom http(s).Agent

const http = require('http');

const { Client, HTTPAgent, HTTPSAgent } = require('ssh2');

const sshConfig = {
  host: '192.168.100.1',
  port: 22,
  username: 'nodejs',
  password: 'rules'
};

// Use `HTTPSAgent` instead for an HTTPS request
const agent = new HTTPAgent(sshConfig);
http.get({
  host: '192.168.200.1',
  agent,
  headers: { Connection: 'close' }
}, (res) => {
  console.log(res.statusCode);
  console.dir(res.headers);
  res.resume();
});

Invoke an arbitrary subsystem

const { Client } = require('ssh2');

const xmlhello = `
  <?xml version="1.0" encoding="UTF-8"?>
  <hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
    <capabilities>
      <capability>urn:ietf:params:netconf:base:1.0</capability>
    </capabilities>
  </hello>]]>]]>`;

const conn = new Client();

conn.on('ready', () => {
  console.log('Client :: ready');
  conn.subsys('netconf', (err, stream) => {
    if (err) throw err;
    stream.on('data', (data) => {
      console.log(data);
    }).write(xmlhello);
  });
}).connect({
  host: '1.2.3.4',
  port: 22,
  username: 'blargh',
  password: 'honk'
});

Server Examples

Password and public key authentication and non-interactive (exec) command execution

const { timingSafeEqual } = require('crypto');
const { readFileSync } = require('fs');
const { inspect } = require('util');

const { utils: { parseKey }, Server } = require('ssh2');

const allowedUser = Buffer.from('foo');
const allowedPassword = Buffer.from('bar');
const allowedPubKey = parseKey(readFileSync('foo.pub'));

function checkValue(input, allowed) {
  const autoReject = (input.length !== allowed.length);
  if (autoReject) {
    // Prevent leaking length information by always making a comparison with the
    // same input when lengths don't match what we expect ...
    allowed = input;
  }
  const isMatch = timingSafeEqual(input, allowed);
  return (!autoReject && isMatch);
}

new Server({
  hostKeys: [readFileSync('host.key')]
}, (client) => {
  console.log('Client connected!');

  client.on('authentication', (ctx) => {
    let allowed = true;
    if (!checkValue(Buffer.from(ctx.username), allowedUser))
      allowed = false;

    switch (ctx.method) {
      case 'password':
        if (!checkValue(Buffer.from(ctx.password), allowedPassword))
          return ctx.reject();
        break;
      case 'publickey':
        if (ctx.key.algo !== allowedPubKey.type
            || !checkValue(ctx.key.data, allowedPubKey.getPublicSSH())
            || (ctx.signature && allowedPubKey.verify(ctx.blob, ctx.signature, ctx.hashAlgo) !== true)) {
          return ctx.reject();
        }
        break;
      default:
        return ctx.reject();
    }

    if (allowed)
      ctx.accept();
    else
      ctx.reject();
  }).on('ready', () => {
    console.log('Client authenticated!');

    client.on('session', (accept, reject) => {
      const session = accept();
      session.once('exec', (accept, reject, info) => {
        console.log('Client wants to execute: ' + inspect(info.command));
        const stream = accept();
        stream.stderr.write('Oh no, the dreaded errors!\n');
        stream.write('Just kidding about the errors!\n');
        stream.exit(0);
        stream.end();
      });
    });
  }).on('close', () => {
    console.log('Client disconnected');
  });
}).listen(0, '127.0.0.1', function() {
  console.log('Listening on port ' + this.address().port);
});

SFTP-only server

const { timingSafeEqual } = require('crypto');
const { readFileSync } = require('fs');
const { inspect } = require('util');

const {
  Server,
  sftp: {
    OPEN_MODE,
    STATUS_CODE,
  },
} = require('ssh2');

const allowedUser = Buffer.from('foo');
const allowedPassword = Buffer.from('bar');

function checkValue(input, allowed) {
  const autoReject = (input.length !== allowed.length);
  if (autoReject) {
    // Prevent leaking length information by always making a comparison with the
    // same input when lengths don't match what we expect ...
    allowed = input;
  }
  const isMatch = timingSafeEqual(input, allowed);
  return (!autoReject && isMatch);
}

// This simple SFTP server implements file uploading where the contents get
// ignored ...

new ssh2.Server({
  hostKeys: [readFileSync('host.key')]
}, (client) => {
  console.log('Client connected!');

  client.on('authentication', (ctx) => {
    let allowed = true;
    if (!checkValue(Buffer.from(ctx.username), allowedUser))
      allowed = false;

    switch (ctx.method) {
      case 'password':
        if (!checkValue(Buffer.from(ctx.password), allowedPassword))
          return ctx.reject();
        break;
      default:
        return ctx.reject();
    }

    if (allowed)
      ctx.accept();
    else
      ctx.reject();
  }).on('ready', () => {
    console.log('Client authenticated!');

    client.on('session', (accept, reject) => {
      const session = accept();
      session.on('sftp', (accept, reject) => {
        console.log('Client SFTP session');
        const openFiles = new Map();
        let handleCount = 0;
        const sftp = accept();
        sftp.on('OPEN', (reqid, filename, flags, attrs) => {
          // Only allow opening /tmp/foo.txt for writing
          if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.WRITE))
            return sftp.status(reqid, STATUS_CODE.FAILURE);

          // Create a fake handle to return to the client, this could easily
          // be a real file descriptor number for example if actually opening
          // a file on disk
          const handle = Buffer.alloc(4);
          openFiles.set(handleCount, true);
          handle.writeUInt32BE(handleCount++, 0);

          console.log('Opening file for write')
          sftp.handle(reqid, handle);
        }).on('WRITE', (reqid, handle, offset, data) => {
          if (handle.length !== 4
              || !openFiles.has(handle.readUInt32BE(0))) {
            return sftp.status(reqid, STATUS_CODE.FAILURE);
          }

          // Fake the write operation
          sftp.status(reqid, STATUS_CODE.OK);

          console.log('Write to file at offset ${offset}: ${inspect(data)}');
        }).on('CLOSE', (reqid, handle) => {
          let fnum;
          if (handle.length !== 4
              || !openFiles.has(fnum = handle.readUInt32BE(0))) {
            return sftp.status(reqid, STATUS_CODE.FAILURE);
          }

          console.log('Closing file');
          openFiles.delete(fnum);

          sftp.status(reqid, STATUS_CODE.OK);
        });
      });
    });
  }).on('close', () => {
    console.log('Client disconnected');
  });
}).listen(0, '127.0.0.1', function() {
  console.log('Listening on port ' + this.address().port);
});

Other Examples

Generate an SSH key

const { utils: { generateKeyPair, generateKeyPairSync } } = require('ssh2');

// Generate unencrypted ED25519 SSH key synchronously
let keys = generateKeyPairSync('ed25519');
// ... use `keys.public` and `keys.private`

// Generate unencrypted ECDSA SSH key synchronously with a comment set
keys = generateKeyPairSync('ecdsa', { bits: 256, comment: 'node.js rules!' });
// ... use `keys.public` and `keys.private`

// Generate encrypted RSA SSH key asynchronously
generateKeyPair(
  'rsa',
  { bits: 2048, passphrase: 'foobarbaz', cipher: 'aes256-cbc' },
  (err, keys) => {
    if (err) throw err;
    // ... use `keys.public` and `keys.private`
  }
);

You can find more examples in the examples directory of this repository.

API

require('ssh2').Client is the Client constructor.

require('ssh2').Server is the Server constructor.

require('ssh2').utils is an object containing some useful utilities.

require('ssh2').HTTPAgent is an http.Agent constructor.

require('ssh2').HTTPSAgent is an https.Agent constructor. Its API is the same as HTTPAgent except it's for HTTPS connections.

Agent-related

require('ssh2').AgentProtocol is a Duplex stream class that aids in communicating over the OpenSSH agent protocol.

require('ssh2').BaseAgent is a base class for creating custom authentication agents.

require('ssh2').createAgent is a helper function that creates a new agent instance using the same logic as the agent configuration option: if the platform is Windows and it's the value "pageant", it creates a PageantAgent, otherwise if it's not a path to a Windows pipe it creates a CygwinAgent. In all other cases, it creates an OpenSSHAgent.

require('ssh2').CygwinAgent is an agent class implementation that communicates with agents in a Cygwin environment.

require('ssh2').OpenSSHAgent is an agent class implementation that communicates with OpenSSH agents over a UNIX socket.

require('ssh2').PageantAgent is an agent class implementation that communicates with Pageant agent processes.

Client

Client events

    // In this particular case `mac` is empty because there is no separate MAC
    // because it's integrated into AES in GCM mode
    { kex: 'ecdh-sha2-nistp256',
      srvHostKey: 'rsa-sha2-512',
      cs: { // Client to server algorithms
        cipher: 'aes128-gcm',
        mac: '',
        compress: 'none',
        lang: ''
      },
      sc: { // Server to client algorithms
        cipher: 'aes128-gcm',
        mac: '',
        compress: 'none',
        lang: ''
      }
    }

Client methods

Server

Server events

Server methods

Connection events

    // In this particular case `mac` is empty because there is no separate MAC
    // because it's integrated into AES in GCM mode
    { kex: 'ecdh-sha2-nistp256',
      srvHostKey: 'rsa-sha2-512',
      cs: { // Client to server algorithms
        cipher: 'aes128-gcm',
        mac: '',
        compress: 'none',
        lang: ''
      },
      sc: { // Server to client algorithms
        cipher: 'aes128-gcm',
        mac: '',
        compress: 'none',
        lang: ''
      }
    }

Connection methods

Session events

Channel

This is a normal streams2 Duplex Stream (used both by clients and servers), with the following changes:

Pseudo-TTY settings

rows and cols override width and height when rows and cols are non-zero.

Pixel dimensions refer to the drawable area of the window.

Zero dimension parameters are ignored.

Terminal modes

Name Description
CS7 7 bit mode.
CS8 8 bit mode.
ECHOCTL Echo control characters as ^(Char).
ECHO Enable echoing.
ECHOE Visually erase chars.
ECHOKE Visual erase for line kill.
ECHOK Kill character discards current line.
ECHONL Echo NL even if ECHO is off.
ICANON Canonicalize input lines.
ICRNL Map CR to NL on input.
IEXTEN Enable extensions.
IGNCR Ignore CR on input.
IGNPAR The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, and 1 if it is TRUE.
IMAXBEL Ring bell on input queue full.
INLCR Map NL into CR on input.
INPCK Enable checking of parity errors.
ISIG Enable signals INTR, QUIT, [D]SUSP.
ISTRIP Strip 8th bit off characters.
IUCLC Translate uppercase characters to lowercase.
IXANY Any char will restart after stop.
IXOFF Enable input flow control.
IXON Enable output flow control.
NOFLSH Don't flush after interrupt.
OCRNL Translate carriage return to newline (output).
OLCUC Convert lowercase to uppercase.
ONLCR Map NL to CR-NL.
ONLRET Newline performs a carriage return (output).
ONOCR Translate newline to carriage return-newline (output).
OPOST Enable output processing.
PARENB Parity enable.
PARMRK Mark parity and framing errors.
PARODD Odd parity, else even.
PENDIN Retype pending input.
TOSTOP Stop background jobs from output.
TTY_OP_ISPEED Specifies the input baud rate in bits per second.
TTY_OP_OSPEED Specifies the output baud rate in bits per second.
VDISCARD Toggles the flushing of terminal output.
VDSUSP Another suspend character.
VEOF End-of-file character (sends EOF from the terminal).
VEOL2 Additional end-of-line character.
VEOL End-of-line character in addition to carriage return and/or linefeed.
VERASE Erase the character to left of the cursor.
VFLUSH Character to flush output.
VINTR Interrupt character; 255 if none. Similarly for the other characters. Not all of these characters are supported on all systems.
VKILL Kill the current input line.
VLNEXT Enter the next character typed literally, even if it is a special character
VQUIT The quit character (sends SIGQUIT signal on POSIX systems).
VREPRINT Reprints the current input line.
VSTART Continues paused output (normally control-Q).
VSTATUS Prints system status line (load, command, pid, etc).
VSTOP Pauses output (normally control-S).
VSUSP Suspends the current program.
VSWTCH Switch to a different shell layer.
VWERASE Erases a word left of cursor.
XCASE Enable input and output of uppercase characters by preceding their lowercase equivalents with "\".

HTTPAgent

HTTPAgent methods

HTTPSAgent

HTTPSAgent methods

Utilities

AgentProtocol

AgentProtocol events

AgentProtocol methods

BaseAgent

In order to create a custom agent, your class must:

Additionally your class may implement the following method in order to support agent forwarding on the client:

createAgent

CygwinAgent

CygwinAgent methods

OpenSSHAgent

OpenSSHAgent methods

PageantAgent

PageantAgent methods