Stream interactive shell session with socket.io - node.js

I have 3 components device, server and frontend (admin).
Server
Starts socket.io server with 2 namespaces /admin and /client.
If socket from /admin namespace sends data, server passes it along to /client namespace. If socket from /client namespace sends data, server passes it along to /admin namespace.
const io = require('socket.io');
const device = io.of('/device');
const admin = io.of('/admin');
device.on('connection', (socket) => {
socket.on('data', (data) => {
console.log("PASSING DATA FROM [DEVICE] TO [ADMIN]")
admin.emit('data', data);
})
});
admin.on('connection', (socket) => {
socket.on('data', (data) => {
console.log("PASSING DATA FROM [ADMIN] TO [DEVICE]")
device.emit('data', data);
});
});
io.listen(80);
Device
Uses socket.io-client to connect to socket.io server.
Starts interactive shell session using node-pty.
const io = require('socket.io-client');
const socket = io('http://localhost:80/client');
const os = require('os');
const pty = require('node-pty');
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 30
});
socket.on('connect', () => {
});
// INPUT DATA
socket.on('data', (data) => {
ptyProcess.write(data);
});
// OUTPUTING DATA
ptyProcess.onData = (data) => {
socket.emit('data', data)
}
Frontend
Finally I have the frontend which uses xterm.js to create a terminal inside the browser. I am using vue. The browser client as well connects to socket.io server on the /admin namespace. Basically I have this :
<template>
<div id="app">
<div id="terminal" ref="terminal"></div>
</div>
</template>
<script>
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { io } from 'socket.io-client';
export default {
mounted() {
const term = new Terminal({ cursorBlink : true });
term.open(this.$refs.terminal);
const socket = io('http://localhost:80/admin');
socket.on('connect', () => {
term.write('\r\n*** Connected to backend***\r\n');
term.onData((data) => {
socket.emit('data', data);
})
socket.on('data', (data) => {
term.write(data);
});
socket.on('disconnect', () => {
term.write('\r\n*** Disconnected from backend***\r\n');
});
});
}
}
</script>
Problem
❌ Starting the pty session seems to work, at least there are now errors reported. However it seems the onData listener callback is never fired, even when I ptyProcess.write() something.
❌ Getting input from xterm all the way to the device ptyProcess.write does not seem to work. I can see the data passed along through the socket.io sockets all the way to the device. But from there nothing happens. What do I miss ? Also I don't see my input in the xterm window as well.

After switching from child_process to using node-pty to create an interactive shell session I almost had it right. Following the node-pty documentation it marked the on('data') eventhandler as deprecated. Instead I should use .onData property of the process to register a callback. Like this:
ptyProcess.onData = function(data) {
socket.emit('data', data);
};
But that didn't do anything. So I switched back to the depracated way of adding an event listener:
ptyProcess.on('data', function(data) {
socket.emit('data', data);
});
Now I have a working interactive shell session forwarded from a remote device through websocket inside my browser ✅.
UPDATE
Did more digging for onData property. Realized it's not a property but a method so I used it wrong. This would be the prefered way :
ptyProcess.onData(function(data) {
socket.emit('data', data);
});
Which also works as expected 👍

Related

How to discard incoming event in Socket.io-client

Hi I am trying to make a tracker app and I'm using socket.io for both server and client. On my client app, I want to disregard message event whenever my browser is not on focus. my code is like this for the client app :
const socket = io('http://localhost:4000');
const [browserState, setBrowserState] = useState('');
useEffect(() => {
socket.on('connect', () => {
console.log('connected');
socket.on("message", payload => {
//payload need to be passed to Map component
console.log(payload);
});
});
},[]);
useEffect(() => {
document.onvisibilitychange = function(){
setBrowserState(document.visibilityState)
}
if(browserState === 'hidden') socket.volatile.emit("message", payload => payload)
},[browserState]);
and on my server is just simply:
io.on('connection', socket => {
socket.on('message', (payload)=>{
console.log(payload)
io.emit('message', payload)
});
The problem is on the client-side for the code socket.volatile.emit("message", payload => payload). if I use socket.volatile.on it's working. but I still receive message event on the client. if I use socket.volatile.emit the server is crashing.
Additional Question: is it okay if my client side io.protocol = 5 and my server is io.protocol = 4?
I'm really new to this. Please advise. :) Thanks!
It can be discarded easily by not replying to incoming sockets
io.on('connection', socket => {
socket.on('message', (payload)=>{
console.log(payload)
// removed this line io.emit('message', payload)
});

How to ensure a single, private ssh connection using ssh2 with socket.io in Meteor

I am using ssh2 and socket.io to enable a real-time ssh connection to a remote server for users of my Meteor 1.8.1 app. The app runs on Ubuntu under Nginx and Phusion Passenger. Here is what the app needs to do:
Each authorised user already has an account on the remote server.
A user will start a session by entering their credentials and clicking a "connect" button in the app.
The user can browse directory listings within their home directory on the remote server.
No user should have access to another user's ssh session.
Their ssh session should be removed from the server when the user clicks a "disconnect" button.
I have the ssh connection working but I can't figure out how to destroy the ssh connection at the end of the user's session. Each time they press disconnect" then "connect", another ssh session is started and the old ssh session is still operational, so each ssh command that is sent is executed multiple times and multiple responses are sent to the browser.
I'm also concerned that the connection isn't secure; in development I'm creating the server with require('http').createServer();. In production, on my Ubuntu server with SSL configured, is it enough to use require('https').createServer(); or is there other configuration required, e.g. of Nginx? Socket.io falls back to older technologies when websocket isn't available; how is that secured?
Main question: why am I seeing duplicate SSH sessions every time the user disconnects and then connects?
Secondary question: where can I find up to date instructions on how to secure socket.io? Or should I give up on socket.io and use WebSocket?
I have read a lot of articles and stack overflow posts, but I'm finding this very confusing and most of the material is out of date. For example socketio-auth is not maintained. I can find almost nothing in the Socket.io documentation on authentication or authorization - there is a handshake entry but it's not clear to me from this whether it's the function I need or how to use it.
Here's my code.
Server
io.on('connection', (socket) => {
console.log('socket id', socket.id); // this shows a new id after disconnect / reconnect
const conn = new SSHClient();
socket.on('disconnect', () => {
console.log('disconnect on server');
conn.end();
});
conn.on('ready', () => {
socket.emit('message', '*** SSH CONNECTION ESTABLISHED ***');
socket.emit('ready', 'ready');
conn.shell((err, stream) => {
stream.write('stty -echo \n'); // don't echo our own command back, or the user's password...
if (err) {
return socket.emit('message', `*** SSH SHELL ERROR: ' ${err.message} ***`);
}
socket.on('path', (path) => {
// path is a request for a directory listing
if (typeof path === 'string') {
const bashCommand = `ls -l ${path} --time-style=full-iso`;
console.log('*** WRITE'); // if you disconnect and reconnect this runs twice. Disconnect and reconnect again, it runs 3 times.
console.log('socket id again', socket.id); // this shows the same new socket id each time
stream.write(`${bashCommand} \n`);
}
});
stream.on('data', (d) => {
socket.emit('data', response); // tell the browser!
}).on('close', () => {
conn.end();
});
});
}).on('close', () => {
socket.emit('message', '*** SSH CONNECTION CLOSED ***');
}).on('error', (err) => {
socket.emit('message', `*** SSH CONNECTION ERROR: ${err.message} ***`);
}).connect({
'host': hosturl,
'username': ausername,
'agent': anagent, // just for dev I'm using public / private key from my local machine but this will be replaced with the user's entered credentials
});
}).on('disconnect', () => {
console.log('user disconnected');
});
server.listen(8080);
Client:
const io = require('socket.io-client');
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {};
const myEmitter = new MyEmitter();
const PORT = 8080;
let socket;
myEmitter.on('connectClicked', () => {
if (socket) {
this.connected.set(socket.connected);
}
if (this.connected.get() === false) {
socket = io(`http://localhost:${PORT}`);
socket.on('connect', () => {
this.connected.set(true);
socket.on('ready', () => {
console.log('ready');
});
// Backend -> Browser
socket.on('message', (data) => {
console.log('socket on message', data);
});
// Backend -> Browser
socket.on('data', (data) => {
console.log('got data', data);
this.parseResponse(data); // client function to handle data, not shown here
});
// Browser -> Backend
myEmitter.on('selectDirectory', () => {
console.log('*** SELECT DIRECTORY');
socket.emit('path', pathArray.join('/')); // path array is set in client code, it is a simple array of directory names
});
socket.on('disconnect', () => {
console.log('\r\n*** Disconnected from backend***\r\n');
this.connected.set(false);
});
});
}
myEmitter.on('disconnectClicked', () => {
socket.disconnect();
});
});
The answer to keeping the ssh connections separate is to maintain a list of current ssh connections and rework the code so that received ssh data is sent only to the browser that corresponds to the incoming message.
I've also given up on socket.io because I can't be confident about security. I'm now using Meteor's inbuilt DDP messaging system via the Meteor Direct Stream Access package. I think this avoids opening up any new points of access to my web server.

how to create interactive ssh terminal and enter commands from the browser using Node JS in a Meteor app

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.
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.

socket io and mqtt nodejs duplicate entry

I am using mqttjs and socketio on my nodejs backend.
I am using angular as my frontend framework.
On my frontend there are 3 routes.
All requires socket connection for real time data.
So on ngOnInit i run client side socket io connection code and on ngOnDestroy I will run socket disconnect as well.
And in my server side code (index.js) there are mainly 3 actions that is happening.
const io = require('socket.io')(server)
mqtt.createConnection();
mqtt.mqttSubscriptions(io);
mqtt.mqttMessages(io);
These are the mqtt methods:
const createConnection = () => {
let options = {
protocol: 'mqtt',
clientId: process.env.MQTT_CLIENT_ID,
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
};
client = mqtt.connect(process.env.MQTT_HOST, options);
client.on('connect', function() {
winston.info('MQTT connected');
});
client.on('error', function(err) {
winston.error(err);
});
};
const mqttSubscriptions = io => {
winston.info(`Socket connected.`);
client.subscribe([TOPICS.DATA], function(error, granted) {
if (error) {
winston.error(error);
}
winston.info('Topics: ', granted);
});
};
const mqttMessages = io => {
io.sockets.on('connection', socket => {
winston.info(`Socket connected.`);
client.on('message', function(topic, message) {
let payload = JSON.parse(message.toString());
winston.info(topic);
winston.info(payload.id);
switch (topic) {
case TOPICS.DATA:
dataController.storeData(payload, io);
break;
default:
winston.error('Wrong topic');
break;
}
});
});
};
And on the datacontroller I am running
socket.emit()
My problem is everytime I navigate to a route and come back the dataController.storeData is called multiple times.
That is when I am at route A, and then navigate to route B and then back to A and then to C, the data is multiplied that many times of my route navigation. (In this case 4 times.)
I found that it is socket io and mqtt connection problem, but I don't know how to solve, since I am new to both of these.
Any help?

Socket.io: Other client only updates when being interacted

I'm trying to set up a realtime application using socket.io in Angular and node.js, which is not working as intended.
Whenever a client is making a new post, the other clients won't update until you interact with the client (e.g. clicking somewhere on the page, or clicking on the browsers tab).
However, having console open in the browser, I can see the new post in the console when I log the posts/objects - without the need to interact with the clients.
Angular:
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
posts: Post[] = [];
...
// Inside ngOnInit:
socket.on('data123', (res) => {
console.log('Updating list..', res);
this.postService.getPosts();
this.postsSub = this.postService.getPostUpdateListener()
.subscribe((posts: Post[]) => {
this.posts = posts;
});
});
Displaying in the template:
<... *ngFor="let item of posts">
Inside PostsService:
getPosts() {
this.http.get<{ message: string, posts: Post[] }>('http://localhost:3000/api/posts')
.subscribe((postData) => {
this.posts = postData.posts;
this.postsUpdate.next([...this.posts]);
});
}
Node.js - this socket.io solution is not yet sending the actual list:
const io = socket(server);
io.sockets.on('connection', (socket) => {
console.log(`new connection id: ${socket.id}`);
sendData(socket);
})
function sendData(socket){
socket.emit('data123', 'TODO: send the actual updated list');
setTimeout(() => {
console.log('sending to client');
sendData(socket);
}, 3000);
}
What worked as intended:
Using setInterval instead "socket.on(..)" on the front-end gave the intended result, meaning the clients will update automatically without the need of interacting. I'm fully aware this solution is horrible, but I assume this pinpointing that it's something wrong with socket solution above in Angular part.
wait, every time when socket.on('data123', (res) => {... you are creating new subscribe? it's wrong way...you must create subscribe in your socket connect feature

Resources