ssh2

repository·master·Indexed 26 days ago

https://github.com/mscdex/ssh2

Pure JavaScript SSH2 client and server modules for Node.js (version 16.0.0 or newer). It supports implementing SSH protocols, including client connections, server hosting, SFTP, port forwarding, X11 forwarding, and connection hopping. The library provides methods for executing remote commands via exec(), starting interactive shell sessions, and interacting with arbitrary subsystems via subsys().

Tokens
16.5K
Snippets
15
Records
87
Agent score
90%

What's inside ssh2

  1. Requirements for ssh2

    master

    To use ssh2, ensure your environment meets the following requirements:

    • Node.js: version 16.0.0 or newer.
    • cpu-features (Optional): This is an optional dependency that is automatically built and used if possible. It is used to help generate an optimal default cipher list.
  2. Import the ssh2 module

    master
    The ssh2 module provides a complete SSH1/SSH2 implementation for Node.js, including both Client and Server capabilities, SSH Agent support, and SFTP functionality. You can access the main components via the default export.
  3. Configure SSH2 Server identity and sockets

    master

    When setting up an SSH2 server, you can customize the server identifier and inject custom socket streams.

    • ident: A string representing the custom server software name/version identifier. Default: 'ssh2js' + moduleVersion + 'srv'.
    • injectSocket(socket): Injects a bidirectional DuplexStream as if it were a TCP socket. The provided socket should include net.Socket-like properties such as socket.remoteAddress, socket.remotePort, and socket.remoteFamily for maximum compatibility.
  4. Handle Keyboard-Interactive Authentication Prompts

    master

    If tryKeyboard is enabled, you must implement a prompt handler within the keyboard-interactive authentication configuration to respond to server challenges:

    {
      type: 'keyboard-interactive',
      username: 'foo',
      prompt: (name, instructions, instructionsLang, prompts, finish) => {
        // ... handle prompts and call finish(true/false) to proceed
      },
    }
    {
      type: 'keyboard-interactive',
      username: 'foo',
      // This works exactly the same way as a 'keyboard-interactive'
      // Client event handler
      prompt: (name, instructions, instructionsLang, prompts, finish) => {
        // ...
      },
    }
  5. Implement an SFTP-only SSH Server

    master

    To build a server dedicated to SFTP, handle the session event and then listen for the sftp event on the session object.

    Inside the sftp event handler, you can respond to file operations by listening to events like OPEN, WRITE, and CLOSE. Use sftp.status(reqid, STATUS_CODE.OK) or sftp.status(reqid, STATUS_CODE.FAILURE) to communicate the result of an operation to the client. For OPEN requests, you can return a handle using sftp.handle(reqid, handle).

    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) {
        allowed = input;
      }
      const isMatch = timingSafeEqual(input, allowed);
      return (!autoReject && isMatch);
    }
    
    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) => {
              if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.WRITE))
                return sftp.status(reqid, STATUS_CODE.FAILURE);
    
              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);
              }
    
              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);
    });
  6. Execute a command on a remote server using `exec()`

    master

    Use the Client.exec() method to run a single command on a remote server. The method accepts a command string and a callback that provides an error object and a readable/writable stream. You can listen to the data event for stdout and the stderr property of the stream for error output.

    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')
    });
  7. List directory contents via SFTP

    master

    Use Client.sftp(callback) to initiate an SFTP session. Once the SFTP client is available, use sftp.readdir(path, callback) to get a list of files and directories in the specified path. The list contains objects with filename, longname, and attrs (size, uid, gid, mode, etc.).

    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'
    });
  8. Forward remote X11 connections

    master

    To support X11 forwarding, listen for the x11 event on the Client instance. The event provides info, accept, and reject arguments. You can then pipe the accepted stream to a local X server socket.

    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);
      });
      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'
    });
  9. Start an interactive shell session using `shell()`

    master

    Use Client.shell() to request an interactive shell session. This returns a stream that allows you to send commands (e.g., via stream.end('command\n')) and receive interactive output. This is useful for simulating a terminal 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')
    });
  10. Implement connection hopping (SSH Tunneling)

    master

    You can chain SSH connections to hop through a jump server. Use forwardOut() on the first connection to create a stream, then pass that stream as the sock option in the .connect() method of a second Client instance.

    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');
      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', () => {
      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();
        }).on('data', (data) => {
          console.log(data.toString());
        });
      });
    });
  11. Perform remote port forwarding with `forwardOut()`

    master

    Use Client.forwardOut(host, port, remoteHost, remotePort, callback) to create a remote port forward. This allows you to tunnel a connection from the remote server to a specific destination (e.g., sending an HTTP request from the server to a local service).

    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'
    });