0

I'm trying to create a web page where the user can authenticate to a remote server via ssh with username/password, and then interact with the remote server.

I'm not looking to create a full interactive terminal: the app server will execute a limited set of commands based on user input and then pass the responses back to the browser.

Different users should interact with different ssh sessions.

My app is built in Meteor 1.8.1, so the back end runs under Node JS, version 9.16.0. It's deployed to Ubuntu using Phusion Passenger.

I have looked at several packages that can create an interactive ssh session but I am missing something basic about how to use them.

For example https://github.com/mscdex/ssh2#start-an-interactive-shell-session

The example shows this code:

var Client = require('ssh2').Client;

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

This example connects to the remote server, executes a command 'ls' and then closes the session. It isn't 'interactive' in the sense I'm looking for. What I can't see is how to keep the session alive and send a new command?

This example of a complete terminal looks like overkill for my needs, and I won't be using Docker.

This example uses socket.io and I'm not sure how that would interact with my Meteor app? I'm currently using Meteor methods and publications to pass information between client and server, so I'd expect to need a "Meteor-type" solution using the Meteor infrastructure?

child_process.spawn works but will only send a single command, it doesn't maintain a session.

I know other people have asked similar questions but I don't see a solution for my particular case. Thank you for any help.

Little Brain
  • 2,647
  • 1
  • 30
  • 54

1 Answers1

3

I got this working by following these instructions for creating an interactive terminal in the browser and these instructions for using socket.io with Meteor.

Both sets of instructions needed some updating due to changes in packages:

  • meteor-node-stubs now uses stream-http instead of http-browserify https://github.com/meteor/node-stubs/issues/14 so don't use the hack for socket

  • xterm addons (fit) are now separate packages

  • xterm API has changed, use term.onData(...) instead of term.on('data'...)

I used these packages:

ssh2

xterm

xterm-addon-fit

socket.io

socket.io-client

and also had to uninstall meteor-mode-stubs and reinstall it to get a recent version that doesn't rely on the Buffer polyfill.

Here's my code.

Front end:

myterminal.html

<template name="myterminal">
    <div id="terminal-container"></div>
</template>

myterminal.js

import { Template } from 'meteor/templating';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';

import './xterm.css'; // copy of node_modules/xterm/css/xterm.css
// xterm css is not imported:
// https://github.com/xtermjs/xterm.js/issues/1418
// This is a problem in Meteor because Webpack won't import files from node_modules: https://github.com/meteor/meteor-feature-requests/issues/278

const io = require('socket.io-client');

Template.fileExplorer.onRendered(function () {
    // Socket io client
    const PORT = 8080;

    const terminalContainer = document.getElementById('terminal-container');
    const term = new Terminal({ 'cursorBlink': true });
    const fitAddon = new FitAddon();
    term.loadAddon(fitAddon);
    term.open(terminalContainer);
    fitAddon.fit();

    const socket = io(`http://localhost:${PORT}`);
    socket.on('connect', () => {
        console.log('socket connected');
        term.write('\r\n*** Connected to backend***\r\n');

        // Browser -> Backend
        term.onData((data) => {
            socket.emit('data', data);
        });

        // Backend -> Browser
        socket.on('data', (data) => {
            term.write(data);
        });

        socket.on('disconnect', () => {
            term.write('\r\n*** Disconnected from backend***\r\n');
        });
    });
});

Server:

server/main.js

const server = require('http').createServer();

// https://github.com/mscdex/ssh2
const io = require('socket.io')(server);
const SSHClient = require('ssh2').Client;

Meteor.startup(() => {
    io.on('connection', (socket) => {
        const conn = new SSHClient();
        conn.on('ready', () => {
            console.log('*** ready');
            socket.emit('data', '\r\n*** SSH CONNECTION ESTABLISHED ***\r\n');
            conn.shell((err, stream) => {
                if (err) {
                    return socket.emit('data', `\r\n*** SSH SHELL ERROR: ' ${err.message} ***\r\n`);
                }
                socket.on('data', (data) => {
                    stream.write(data);
                });
                stream.on('data', (d) => {
                    socket.emit('data', d.toString('binary'));
                }).on('close', () => {
                    conn.end();
                });
            });
        }).on('close', () => {
            socket.emit('data', '\r\n*** SSH CONNECTION CLOSED ***\r\n');
        }).on('error', (err) => {
            socket.emit('data', `\r\n*** SSH CONNECTION ERROR: ${err.message} ***\r\n`);
        }).connect({
            'host': process.env.URL,
            'username': process.env.USERNAME,
            'agent': process.env.SSH_AUTH_SOCK, // for server which uses private / public key
            // in my setup, already has working value /run/user/1000/keyring/ssh
        });
    });

    server.listen(8080);
});

Note that I am connecting from a machine that has ssh access via public key to the remote server. You may need different credentials depending on your setup. The environment variables are loaded from a file at Meteor runtime.

Little Brain
  • 2,647
  • 1
  • 30
  • 54
  • You might want to check if the websocket over HTTP fullfills your security needs. This is world-readable, even in a small world like `localhost` this might be unwanted, when connecting to remote machines. – jerch May 07 '20 at 09:20
  • That's good advice but I'm not sure how to check? I haven't found any useful resources. The real deployment will be on a web app server and the web page uses https. Users will authenticate with username, password on the remote server (the one the SSHClient connects to). – Little Brain May 07 '20 at 15:06
  • Maybe this might help https://www.freecodecamp.org/news/how-to-secure-your-websocket-connections-d0be0996c556/ Basic problem is the weak security measures the browser applies to websockets by default. See #0, #4 and #5 there to get a hold of it. – jerch May 07 '20 at 20:43
  • Thank you, I have read this article but am still struggling how to act on it. For example I can't find any instructions on how to check whether socket.io is using ws or wss? Or how to ensure it is secure when it falls back to older technology? The instructions linked are for WebSockets, not socket.io. Is socket.io basically unsecurable? – Little Brain May 08 '20 at 07:49
  • Im not used to socketIO, maybe look into their docs to get some auth running? Same regarding ws vs wss (Im not even sure if they always use websockets under the hook, or might fallback to long polling or other tricks). – jerch May 08 '20 at 11:32
  • Thanks, but I've looked at socketIO docs and tutorials and am finding the material confusing and out of date. I cannot find anything clear. – Little Brain May 09 '20 at 07:15
  • 1
    Then it might be better to resort to plain websockets with SSL and deal with the auth issue yourself (like creating a custom protocol, which always contains an auth token to be matched against the correct opened pty on server side). And if in doubt, whether you are doing the right thing with websockets - go with ajax long-polling, it will be slightly worse in throughput/latency, but give the biggest security coverage right from the start (also supporting good old session cookies). – jerch May 09 '20 at 16:13