Connecting to remote SSH server (via Node.js/html5 console)
Solution 1
This is easily doable with modules like ssh2
, xterm
, and socket.io
.
Here's an example:
npm install ssh2 xterm socket.io
- Create
index.html
:
<html>
<head>
<title>SSH Terminal</title>
<link rel="stylesheet" href="/src/xterm.css" />
<script src="/src/xterm.js"></script>
<script src="/addons/fit/fit.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
window.addEventListener('load', function() {
var terminalContainer = document.getElementById('terminal-container');
var term = new Terminal({ cursorBlink: true });
term.open(terminalContainer);
term.fit();
var socket = io.connect();
socket.on('connect', function() {
term.write('\r\n*** Connected to backend***\r\n');
// Browser -> Backend
term.on('data', function(data) {
socket.emit('data', data);
});
// Backend -> Browser
socket.on('data', function(data) {
term.write(data);
});
socket.on('disconnect', function() {
term.write('\r\n*** Disconnected from backend***\r\n');
});
});
}, false);
</script>
<style>
body {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
}
h1 {
text-align: center;
}
#terminal-container {
width: 960px;
height: 600px;
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #111;
color: #fafafa;
padding: 2px;
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
</style>
</head>
<body>
<div id="terminal-container"></div>
</body>
</html>
- Create
server.js
:
var fs = require('fs');
var path = require('path');
var server = require('http').createServer(onRequest);
var io = require('socket.io')(server);
var SSHClient = require('ssh2').Client;
// Load static files into memory
var staticFiles = {};
var basePath = path.join(require.resolve('xterm'), '..');
[ 'addons/fit/fit.js',
'src/xterm.css',
'src/xterm.js'
].forEach(function(f) {
staticFiles['/' + f] = fs.readFileSync(path.join(basePath, f));
});
staticFiles['/'] = fs.readFileSync('index.html');
// Handle static file serving
function onRequest(req, res) {
var file;
if (req.method === 'GET' && (file = staticFiles[req.url])) {
res.writeHead(200, {
'Content-Type': 'text/'
+ (/css$/.test(req.url)
? 'css'
: (/js$/.test(req.url) ? 'javascript' : 'html'))
});
return res.end(file);
}
res.writeHead(404);
res.end();
}
io.on('connection', function(socket) {
var conn = new SSHClient();
conn.on('ready', function() {
socket.emit('data', '\r\n*** SSH CONNECTION ESTABLISHED ***\r\n');
conn.shell(function(err, stream) {
if (err)
return socket.emit('data', '\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
socket.on('data', function(data) {
stream.write(data);
});
stream.on('data', function(d) {
socket.emit('data', d.toString('binary'));
}).on('close', function() {
conn.end();
});
});
}).on('close', function() {
socket.emit('data', '\r\n*** SSH CONNECTION CLOSED ***\r\n');
}).on('error', function(err) {
socket.emit('data', '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
}).connect({
host: '192.168.100.105',
username: 'foo',
password: 'barbaz'
});
});
server.listen(8000);
- Edit the SSH server configuration passed to
.connect()
inserver.js
node server.js
- Visit http://localhost:8000 in your browser
Solution 2
Just adding updated code to @mscdex great answer because the libraries have changed over the years.
Libraries:
npm install express socket.io ssh2 xterm xterm-addon-fit
index.html:
<html>
<head>
<title>SSH Terminal</title>
<link rel="stylesheet" href="/xterm.css" />
<script src="/xterm.js"></script>
<script src="/xterm-addon-fit.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
window.addEventListener('load', function() {
var terminalContainer = document.getElementById('terminal-container');
const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(terminalContainer);
fitAddon.fit();
var socket = io() //.connect();
socket.on('connect', function() {
term.write('\r\n*** Connected to backend ***\r\n');
});
// Browser -> Backend
term.onKey(function (ev) {
socket.emit('data', ev.key);
});
// Backend -> Browser
socket.on('data', function(data) {
term.write(data);
});
socket.on('disconnect', function() {
term.write('\r\n*** Disconnected from backend ***\r\n');
});
}, false);
</script>
<style>
body {
font-family: helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
}
h1 {
text-align: center;
}
#terminal-container {
width: 960px;
height: 600px;
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #111;
color: #fafafa;
padding: 2px;
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
</style>
</head>
<body>
<h3>WebSSH</h3>
<div id="terminal-container"></div>
</body>
</html>
server.js:
var fs = require('fs');
var path = require('path');
var server = require('http').createServer(onRequest);
var io = require('socket.io')(server);
var SSHClient = require('ssh2').Client;
// Load static files into memory
var staticFiles = {};
var basePath = path.join(require.resolve('xterm'), '..');
staticFiles['/xterm.css'] = fs.readFileSync(path.join(basePath, '../css/xterm.css'));
staticFiles['/xterm.js'] = fs.readFileSync(path.join(basePath, 'xterm.js'));
basePath = path.join(require.resolve('xterm-addon-fit'), '..');
staticFiles['/xterm-addon-fit.js'] = fs.readFileSync(path.join(basePath, 'xterm-addon-fit.js'));
staticFiles['/'] = fs.readFileSync('index.html');
// Handle static file serving
function onRequest(req, res) {
var file;
if (req.method === 'GET' && (file = staticFiles[req.url])) {
res.writeHead(200, {
'Content-Type': 'text/'
+ (/css$/.test(req.url)
? 'css'
: (/js$/.test(req.url) ? 'javascript' : 'html'))
});
return res.end(file);
}
res.writeHead(404);
res.end();
}
io.on('connection', function(socket) {
var conn = new SSHClient();
conn.on('ready', function() {
socket.emit('data', '\r\n*** SSH CONNECTION ESTABLISHED ***\r\n');
conn.shell(function(err, stream) {
if (err)
return socket.emit('data', '\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
socket.on('data', function(data) {
stream.write(data);
});
stream.on('data', function(d) {
socket.emit('data', d.toString('binary'));
}).on('close', function() {
conn.end();
});
});
}).on('close', function() {
socket.emit('data', '\r\n*** SSH CONNECTION CLOSED ***\r\n');
}).on('error', function(err) {
socket.emit('data', '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
}).connect({
host: 'domain.tld',
port: 22,
username: 'root',
privateKey: require('fs').readFileSync('path/to/keyfile')
});
});
let port = 8000;
console.log('Listening on port', port)
server.listen(port);
Solution 3
Same as the above answer but actually using express and modern syntax and libraries
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http, {
cors: {
origin: "*"
}
});
app.set('view engine', 'ejs');
app.use(express.urlencoded({
extended: false,
limit: '150mb'
}));
app.use(express.static(__dirname + '/public'));
app.use('/xterm.css', express.static(require.resolve('xterm/css/xterm.css')));
app.use('/xterm.js', express.static(require.resolve('xterm')));
app.use('/xterm-addon-fit.js', express.static(require.resolve('xterm-addon-fit')));
const SSHClient = require('ssh2').Client;
app.get('/', (req, res) => {
// res.sendFile(__dirname + '/index.html');
res.render('index');
// I am using ejs as my templating engine but HTML file work just fine.
});
io.on('connection', function(socket) {
var conn = new SSHClient();
conn.on('ready', function() {
socket.emit('data', '\r\n*** SSH CONNECTION ESTABLISHED ***\r\n');
conn.shell(function(err, stream) {
if (err)
return socket.emit('data', '\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
socket.on('data', function(data) {
stream.write(data);
});
stream.on('data', function(d) {
socket.emit('data', d.toString('binary'));
}).on('close', function() {
conn.end();
});
});
}).on('close', function() {
socket.emit('data', '\r\n*** SSH CONNECTION CLOSED ***\r\n');
}).on('error', function(err) {
socket.emit('data', '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
}).connect({
host: '192.168.0.103',
port: 22,
username: 'kali',
password: 'kali'
});
});
http.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
* {
padding: 0%;
margin: 0%;
box-sizing: border-box;
}
body {
font-family: Helvetica, sans-serif, arial;
font-size: 1em;
color: #111;
}
h1 {
text-align: center;
}
#terminal-container {
width: 960px;
height: 600px;
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #111;
color: #fafafa;
padding: 2px;
}
#terminal-container .terminal:focus .terminal-cursor {
background-color: #fafafa;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSH SERVER</title>
<link rel="stylesheet" href="/xterm.css" />
<script defer src="/xterm.js"></script>
<script defer src="/xterm-addon-fit.js"></script>
<script defer src="/socket.io/socket.io.js"></script>
<script defer src='/js/app.js'></script>
<link rel='stylesheet' href='/css/main.css'>
</head>
<body>
<h3>WebSSH</h3>
<div id="terminal-container"></div>
<script>
// PLEASE USE A SEPERATE FILE FOR THE JS and defer it
// like the above app.js file
window.addEventListener('load', function() {
const terminalContainer = document.getElementById('terminal-container');
const term = new Terminal({
cursorBlink: true
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(terminalContainer);
fitAddon.fit();
const socket = io() //.connect();
socket.on('connect', function() {
term.write('\r\n*** Connected to backend ***\r\n');
});
// Browser -> Backend
term.onKey(function(ev) {
socket.emit('data', ev.key);
});
// Backend -> Browser
socket.on('data', function(data) {
term.write(data);
});
socket.on('disconnect', function() {
term.write('\r\n*** Disconnected from backend ***\r\n');
});
}, false);
</script>
</body>
</html>
Solution 4
Try also noVnc.
However, a little dig within the page of xterm.js
reveals other solutions, like
Related videos on Youtube
Ethan
Updated on July 19, 2022Comments
-
Ethan almost 2 years
I have been scouring the face of the web looking to answer a question which I had thought would be simple. My goal is straight forward. I want to build out a simple web-based SSH client using Node.js module(s). I have found several options if I want to connect to the node server itself, but can't seem to find any examples of connecting to a REMOTE server.
Essentially the outcome I am looking for is a workflow like this : Connect to webserver -> Click on a server name in a list of servers -> Enter SSH session to the server I clicked on
The only thing I have found that's even remotely close to what I am looking for is guacamole. I do not want to use guacamole, however, as I want this application to be OS independent. Currently I am building it on a windows 10 platform, and will port it over to fedora when I am done.
I found this tutorial for creating an SSH terminal. However, all this does is creates (or attempts to create) an SSH connection to the local system.
Another options that looked absolutely fantastic was tty.js. Alas, the bottom-line is the same as the above tutorial. The module only allows you to connect to the node.js server, NOT to remote servers.
Anyone have information on a possible path to this goal?
-
Ethan over 7 yearsDude, you absolutely rock! :)
-
Ethan over 7 yearsDon't happen to have something similar for RDP & VNC do ya? :)
-
mscdex over 7 yearsA quick search for vnc turns up this. It's a bit old and could stand to be optimized a bit, but it might still work. As far as RDP goes, I have not seen an RDP implementation/binding for node yet.
-
Ethan over 7 yearsYep, that's the same thing I found for VNC. I'll mess with that eventually. For RDP, I think this'll work github.com/citronneur/node-rdpjs but I'm going to have to find some better documentation than what's on the GIT page.
-
AndrewVT over 7 years@mscdex When I go to localhost:8000 it doesn't allow the passing of GET variables (produces white screen). What modification would need to be made to the answer code above that would allow localhost:8000/?foo=bar and then to have access to foo in onRequest(req, res) ?
-
mscdex over 7 years@user2058037 You'd have to parse
req.url
likeurl.parse(req.url, true)
. Then use.pathname
from the resulting object instead ofreq.url
for thestaticFiles[]
lookup. -
mohammad amin almost 7 yearshi @mscdex, i need to record this terminal.how do i do that?
-
mohammad amin almost 7 years@mscdex i found some record solution like asciinema, but user can stop recording. i wanna record terminal in backend in main server (the server nodejs run, not remote one). please help.
-
gdm almost 7 yearssomething I am missing. Suppose I have a PC behind a NAT. And I want to access it from Internet. Are these guidelines useful for?
-
nimi over 4 yearsGreat piece of code. I have to warn others that this (and socket.io in general) will not work if you happen to use the express-status-monitor package. You will get a "Error during WebSocket handshake: Unexpected response code: 400" and it will drive you crazy. Cheers.
-
Allan Lewis almost 4 yearsCan you please specify the versions used here? With the latest versions as of 2020/04/30 and Node 12, this fails with:
Error: ENOENT: no such file or directory, open '/home/allan.lewis/git/stash-user/yvrc-ssh-term/node_modules/xterm/lib/addons/fit/fit.js'
-
Allan Lewis almost 4 yearsI think the root cause of my issue above is that the
fit
addon has moved into a separate package,xterm-addon-fit
. -
Little Brain almost 4 yearsThis is awesome. Note that the xterm API has changed, so instead of term.on('data'... I used term.onData(...
-
pab789 over 3 yearsIs there any way to do the same with python as backend?
-
Alok Deshwal about 3 yearsdoes this code work if i want to connect with my local machine instead of remote machine, i'm getting ECONNREFUSED error.
-
jerch about 3 yearsYou should not use the
onKey
event, it is only there for low level event access and rarely needed. Instead useonData
andonBinary
, these will nicely wrap everything into bytes meant for the IO sink (incl. mouse reports). -
mor222 about 3 yearsCan this solution be used with Java Spring Boot App? I have an Angular + Spring Boot app where I want to connect using SSH, RDP, and Telnet.
-
Shabbir Dhangot about 3 yearshow to pass pem here?
-
pphysch over 2 years@ShabbirDhangot use
privateKey
instead ofpassword
in the SSHClientconnect
arguments.