1

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.

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

7 Comments

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.
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).
Maybe this might help freecodecamp.org/news/… 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.
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?
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).
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.