Cian Butler
6 years ago
committed by
GitHub
35 changed files with 4487 additions and 2991 deletions
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"@babel/preset-typescript", |
||||
|
["@babel/env"] |
||||
|
], |
||||
|
"compact": true, |
||||
|
"plugins": [ |
||||
|
"lodash" |
||||
|
] |
||||
|
} |
@ -1,3 +1,5 @@ |
|||||
node_modules/ |
node_modules/ |
||||
.esm-cache |
.esm-cache |
||||
dist |
dist |
||||
|
public/ |
||||
|
*hterm* |
||||
|
@ -1,16 +1,32 @@ |
|||||
module.exports = { |
module.exports = { |
||||
|
parser: 'eslint-plugin-typescript/parser', |
||||
|
plugins: ['typescript', 'prettier'], |
||||
env: { |
env: { |
||||
es6: true, |
es6: true, |
||||
node: true, |
node: true, |
||||
browser: true |
browser: true, |
||||
}, |
}, |
||||
root: true, |
root: true, |
||||
extends: ["airbnb-base", "plugin:prettier/recommended"], |
extends: [ |
||||
|
'airbnb-base', |
||||
|
'plugin:typescript/recommended', |
||||
|
'plugin:prettier/recommended', |
||||
|
], |
||||
rules: { |
rules: { |
||||
"linebreak-style": ["error", "unix"], |
'typescript/indent': 'off', |
||||
"arrow-parens": ["error", "as-needed"], |
'linebreak-style': ['error', 'unix'], |
||||
"no-param-reassign": ["error", { props: false }], |
'arrow-parens': ['error', 'as-needed'], |
||||
"func-style": ["error", "declaration", { allowArrowFunctions: true }], |
'no-param-reassign': ['error', { props: false }], |
||||
"no-use-before-define": ["error", { functions: false }] |
'func-style': ['error', 'declaration', { allowArrowFunctions: true }], |
||||
} |
'no-use-before-define': ['error', { functions: false }], |
||||
|
'typescript/no-use-before-define': ['error', { functions: false }], |
||||
|
}, |
||||
|
settings: { |
||||
|
'import/resolver': { |
||||
|
'typescript-eslint-parser': ['.ts', '.tsx'], |
||||
|
node: { |
||||
|
extensions: ['.ts', '.js'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
}; |
}; |
||||
|
@ -0,0 +1,3 @@ |
|||||
|
FROM sickp/alpine-sshd:latest |
||||
|
RUN adduser -D -h /home/term -s /bin/sh term && \ |
||||
|
( echo "term:term" | chpasswd ) |
@ -1,12 +1,93 @@ |
|||||
/* eslint-disable */ |
/* eslint-disable typescript/no-var-requires */ |
||||
require = require('@std/esm')(module, { |
|
||||
cjs: 'true', |
const yargs = require('yargs'); |
||||
esm: 'js', |
const wetty = require('./dist').default; |
||||
}); |
|
||||
const wetty = require('./lib/index.mjs').default; |
|
||||
module.exports = wetty.wetty; |
module.exports = wetty.wetty; |
||||
|
|
||||
/** |
/** |
||||
* Check if being run by cli or require |
* Check if being run by cli or require |
||||
*/ |
*/ |
||||
if (require.main === module) wetty.init(); |
if (require.main === module) { |
||||
|
wetty.init( |
||||
|
yargs |
||||
|
.options({ |
||||
|
sslkey: { |
||||
|
demand: false, |
||||
|
type: 'string', |
||||
|
description: 'path to SSL key', |
||||
|
}, |
||||
|
sslcert: { |
||||
|
demand: false, |
||||
|
type: 'string', |
||||
|
description: 'path to SSL certificate', |
||||
|
}, |
||||
|
sshhost: { |
||||
|
demand: false, |
||||
|
description: 'ssh server host', |
||||
|
type: 'string', |
||||
|
default: process.env.SSHHOST || 'localhost', |
||||
|
}, |
||||
|
sshport: { |
||||
|
demand: false, |
||||
|
description: 'ssh server port', |
||||
|
type: 'number', |
||||
|
default: parseInt(process.env.SSHPORT, 10) || 22, |
||||
|
}, |
||||
|
sshuser: { |
||||
|
demand: false, |
||||
|
description: 'ssh user', |
||||
|
type: 'string', |
||||
|
default: process.env.SSHUSER || '', |
||||
|
}, |
||||
|
sshauth: { |
||||
|
demand: false, |
||||
|
description: |
||||
|
'defaults to "password", you can use "publickey,password" instead', |
||||
|
type: 'string', |
||||
|
default: process.env.SSHAUTH || 'password', |
||||
|
}, |
||||
|
sshpass: { |
||||
|
demand: false, |
||||
|
description: 'ssh password', |
||||
|
type: 'string', |
||||
|
default: process.env.SSHPASS || undefined, |
||||
|
}, |
||||
|
sshkey: { |
||||
|
demand: false, |
||||
|
description: |
||||
|
'path to an optional client private key (connection will be password-less and insecure!)', |
||||
|
type: 'string', |
||||
|
default: process.env.SSHKEY || undefined, |
||||
|
}, |
||||
|
base: { |
||||
|
demand: false, |
||||
|
alias: 'b', |
||||
|
description: 'base path to wetty', |
||||
|
type: 'string', |
||||
|
default: process.env.BASE || '/wetty/', |
||||
|
}, |
||||
|
port: { |
||||
|
demand: false, |
||||
|
alias: 'p', |
||||
|
description: 'wetty listen port', |
||||
|
type: 'number', |
||||
|
default: parseInt(process.env.PORT, 10) || 3000, |
||||
|
}, |
||||
|
command: { |
||||
|
demand: false, |
||||
|
alias: 'c', |
||||
|
description: 'command to run in shell', |
||||
|
type: 'string', |
||||
|
default: process.env.COMMAND || 'login', |
||||
|
}, |
||||
|
help: { |
||||
|
demand: false, |
||||
|
alias: 'h', |
||||
|
type: 'boolean', |
||||
|
description: 'Print help message', |
||||
|
}, |
||||
|
}) |
||||
|
.boolean('allow_discovery').argv |
||||
|
); |
||||
|
} |
||||
|
@ -1,30 +0,0 @@ |
|||||
const localhost = host => |
|
||||
process.getuid() === 0 && |
|
||||
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1'); |
|
||||
|
|
||||
export default ( |
|
||||
{ request: { headers }, client: { conn } }, |
|
||||
{ user, host, port, auth } |
|
||||
) => ({ |
|
||||
args: localhost(host) |
|
||||
? ['login', '-h', conn.remoteAddress.split(':')[3]] |
|
||||
: [ |
|
||||
'ssh', |
|
||||
address(headers, user, host), |
|
||||
'-p', |
|
||||
port, |
|
||||
'-o', |
|
||||
`PreferredAuthentications=${auth}`, |
|
||||
], |
|
||||
user: |
|
||||
localhost(host) || |
|
||||
user !== '' || |
|
||||
user.includes('@') || |
|
||||
address(headers, user, host).includes('@'), |
|
||||
}); |
|
||||
|
|
||||
function address(headers, user, host) { |
|
||||
const match = headers.referer.match('.+/ssh/([^/]+)$'); |
|
||||
const fallback = user ? `${user}@${host}` : host; |
|
||||
return match ? `${match[1]}@${host}` : fallback; |
|
||||
} |
|
@ -1,149 +0,0 @@ |
|||||
/** |
|
||||
* Create WeTTY server |
|
||||
* @module WeTTy |
|
||||
*/ |
|
||||
import EventEmitter from 'events'; |
|
||||
import server from './server.mjs'; |
|
||||
import command from './command.mjs'; |
|
||||
import term from './term.mjs'; |
|
||||
import loadSSL from './ssl.mjs'; |
|
||||
|
|
||||
class WeTTy extends EventEmitter { |
|
||||
/** |
|
||||
* Starts WeTTy Server |
|
||||
* @name start |
|
||||
* @async |
|
||||
* @param {Object} [ssh] SSH settings |
|
||||
* @param {string} [ssh.user=''] default user for ssh |
|
||||
* @param {string} [ssh.host=localhost] machine to ssh too |
|
||||
* @param {string} [ssh.auth=password] authtype to use |
|
||||
* @param {number} [ssh.port=22] port to connect to over ssh |
|
||||
* @param {number} [basePath=/wetty/] base part of URL |
|
||||
* @param {number} [serverPort=3000] Port to run server on |
|
||||
* @param {Object} [ssl] SSL settings |
|
||||
* @param {?string} [ssl.key] Path to ssl key |
|
||||
* @param {?string} [ssl.cert] Path to ssl cert |
|
||||
* @return {Promise} Promise resolves once server is running |
|
||||
*/ |
|
||||
start( |
|
||||
{ user = '', host = 'localhost', auth = 'password', port = 22 }, |
|
||||
basePath = '/wetty/', |
|
||||
serverPort = 3000, |
|
||||
{ key, cert } |
|
||||
) { |
|
||||
return loadSSL(key, cert).then(ssl => { |
|
||||
const io = server(basePath, serverPort, ssl); |
|
||||
/** |
|
||||
* Wetty server connected too |
|
||||
* @fires WeTTy#connnection |
|
||||
*/ |
|
||||
io.on('connection', socket => { |
|
||||
/** |
|
||||
* @event wetty#connection |
|
||||
* @name connection |
|
||||
* @type {object} |
|
||||
* @property {string} msg Message for logs |
|
||||
* @property {Date} date date and time of connection |
|
||||
*/ |
|
||||
this.emit('connection', { |
|
||||
msg: `Connection accepted.`, |
|
||||
date: new Date(), |
|
||||
}); |
|
||||
const { args, user: sshUser } = command(socket, { |
|
||||
user, |
|
||||
host, |
|
||||
auth, |
|
||||
port, |
|
||||
}); |
|
||||
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`); |
|
||||
if (sshUser) { |
|
||||
term.spawn(socket, args); |
|
||||
} else { |
|
||||
term |
|
||||
.login(socket) |
|
||||
.then(username => { |
|
||||
this.emit('debug', `username: ${username.trim()}`); |
|
||||
args[1] = `${username.trim()}@${args[1]}`; |
|
||||
this.emit('debug', `cmd : ${args.join(' ')}`); |
|
||||
return term.spawn(socket, args); |
|
||||
}) |
|
||||
.catch(() => this.disconnected()); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* terminal spawned |
|
||||
* |
|
||||
* @fires module:WeTTy#spawn |
|
||||
*/ |
|
||||
spawned(pid, address) { |
|
||||
/** |
|
||||
* Terminal process spawned |
|
||||
* @event WeTTy#spawn |
|
||||
* @name spawn |
|
||||
* @type {object} |
|
||||
* @property {string} msg Message containing pid info and status |
|
||||
* @property {number} pid Pid of the terminal |
|
||||
* @property {string} address address of connecting user |
|
||||
*/ |
|
||||
this.emit('spawn', { |
|
||||
msg: `PID=${pid} STARTED on behalf of ${address}`, |
|
||||
pid, |
|
||||
address, |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* terminal exited |
|
||||
* |
|
||||
* @fires WeTTy#exit |
|
||||
*/ |
|
||||
exited(code, pid) { |
|
||||
/** |
|
||||
* Terminal process exits |
|
||||
* @event WeTTy#exit |
|
||||
* @name exit |
|
||||
* @type {object} |
|
||||
* @property {number} code the exit code |
|
||||
* @property {string} msg Message containing pid info and status |
|
||||
*/ |
|
||||
this.emit('exit', { code, msg: `PID=${pid} ENDED` }); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Disconnect from WeTTY |
|
||||
* |
|
||||
* @fires WeTTy#disconnet |
|
||||
*/ |
|
||||
disconnected() { |
|
||||
/** |
|
||||
* @event WeTTY#disconnect |
|
||||
* @name disconnect |
|
||||
*/ |
|
||||
this.emit('disconnect'); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Wetty server started |
|
||||
* @fires WeTTy#server |
|
||||
*/ |
|
||||
server(port, connection) { |
|
||||
/** |
|
||||
* @event WeTTy#server |
|
||||
* @type {object} |
|
||||
* @name server |
|
||||
* @property {string} msg Message for logging |
|
||||
* @property {number} port port sever is on |
|
||||
* @property {string} connection connection type for web traffic |
|
||||
*/ |
|
||||
this.emit('server', { |
|
||||
msg: `${connection} on port ${port}`, |
|
||||
port, |
|
||||
connection, |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default new WeTTy(); |
|
@ -1,100 +0,0 @@ |
|||||
import optimist from 'optimist'; |
|
||||
import logger from './logger.mjs'; |
|
||||
import wetty from './emitter.mjs'; |
|
||||
|
|
||||
const opts = optimist |
|
||||
.options({ |
|
||||
sslkey: { |
|
||||
demand: false, |
|
||||
description: 'path to SSL key', |
|
||||
}, |
|
||||
sslcert: { |
|
||||
demand: false, |
|
||||
description: 'path to SSL certificate', |
|
||||
}, |
|
||||
sshhost: { |
|
||||
demand: false, |
|
||||
description: 'ssh server host', |
|
||||
}, |
|
||||
sshport: { |
|
||||
demand: false, |
|
||||
description: 'ssh server port', |
|
||||
}, |
|
||||
sshuser: { |
|
||||
demand: false, |
|
||||
description: 'ssh user', |
|
||||
}, |
|
||||
sshauth: { |
|
||||
demand: false, |
|
||||
description: |
|
||||
'defaults to "password", you can use "publickey,password" instead', |
|
||||
}, |
|
||||
base: { |
|
||||
demand: false, |
|
||||
alias: 'b', |
|
||||
description: 'base path to wetty', |
|
||||
}, |
|
||||
port: { |
|
||||
demand: false, |
|
||||
alias: 'p', |
|
||||
description: 'wetty listen port', |
|
||||
}, |
|
||||
help: { |
|
||||
demand: false, |
|
||||
alias: 'h', |
|
||||
description: 'Print help message', |
|
||||
}, |
|
||||
}) |
|
||||
.boolean('allow_discovery').argv; |
|
||||
|
|
||||
export default class { |
|
||||
static start({ |
|
||||
sshuser = process.env.SSHUSER || '', |
|
||||
sshhost = process.env.SSHHOST || 'localhost', |
|
||||
sshauth = process.env.SSHAUTH || 'password', |
|
||||
sshport = process.env.SSHPOST || 22, |
|
||||
base = process.env.BASE || '/wetty/', |
|
||||
port = process.env.PORT || 3000, |
|
||||
sslkey, |
|
||||
sslcert, |
|
||||
}) { |
|
||||
wetty |
|
||||
.on('exit', ({ code, msg }) => { |
|
||||
logger.info(`Exit with code: ${code} ${msg}`); |
|
||||
}) |
|
||||
.on('disconnect', () => { |
|
||||
logger.info('disconnect'); |
|
||||
}) |
|
||||
.on('spawn', ({ msg }) => logger.info(msg)) |
|
||||
.on('connection', ({ msg, date }) => logger.info(`${date} ${msg}`)) |
|
||||
.on('server', ({ msg }) => logger.info(msg)) |
|
||||
.on('debug', msg => logger.debug(msg)); |
|
||||
return wetty.start( |
|
||||
{ |
|
||||
user: sshuser, |
|
||||
host: sshhost, |
|
||||
auth: sshauth, |
|
||||
port: sshport, |
|
||||
}, |
|
||||
base, |
|
||||
port, |
|
||||
{ key: sslkey, cert: sslcert } |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
static get wetty() { |
|
||||
return wetty; |
|
||||
} |
|
||||
|
|
||||
static init() { |
|
||||
if (!opts.help) { |
|
||||
this.start(opts).catch(err => { |
|
||||
logger.error(err); |
|
||||
process.exitCode = 1; |
|
||||
}); |
|
||||
} else { |
|
||||
optimist.showHelp(); |
|
||||
process.exitCode = 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,46 +0,0 @@ |
|||||
import compression from 'compression'; |
|
||||
import express from 'express'; |
|
||||
import favicon from 'serve-favicon'; |
|
||||
import helmet from 'helmet'; |
|
||||
import http from 'http'; |
|
||||
import https from 'https'; |
|
||||
import path from 'path'; |
|
||||
import socket from 'socket.io'; |
|
||||
import { isUndefined } from 'lodash'; |
|
||||
import morgan from 'morgan'; |
|
||||
import logger from './logger.mjs'; |
|
||||
import events from './emitter.mjs'; |
|
||||
|
|
||||
const pubDir = path.join(__dirname, '..', 'public'); |
|
||||
const distDir = path.join(__dirname, '..', 'dist'); |
|
||||
|
|
||||
export default function createServer(base, port, { key, cert }) { |
|
||||
base = base.replace(/\/*$/, ""); |
|
||||
events.emit('debug', `key: ${key}, cert: ${cert}, port: ${port}, base: ${base}`); |
|
||||
const app = express(); |
|
||||
const html = (req, res) => res.sendFile(path.join(pubDir, 'index.html')); |
|
||||
const css = (req, res) => res.sendFile(path.join(distDir, 'main.css')); |
|
||||
const js = (req, res) => res.sendFile(path.join(distDir, 'main.js')); |
|
||||
app |
|
||||
.use(morgan('combined', { stream: logger.stream })) |
|
||||
.use(helmet()) |
|
||||
.use(compression()) |
|
||||
.use(favicon(path.join(pubDir, 'favicon.ico'))) |
|
||||
.get(`${base}/`, html) |
|
||||
.get(`${base}/main.css`, css) |
|
||||
.get(`${base}/main.js`, js) |
|
||||
.get(`${base}/ssh/main.css`, css) |
|
||||
.get(`${base}/ssh/main.js`, js) |
|
||||
.get(`${base}/ssh/:user`, html) |
|
||||
|
|
||||
return socket( |
|
||||
!isUndefined(key) && !isUndefined(cert) |
|
||||
? https.createServer({ key, cert }, app).listen(port, () => { |
|
||||
events.server(port, 'https'); |
|
||||
}) |
|
||||
: http.createServer(app).listen(port, () => { |
|
||||
events.server(port, 'http'); |
|
||||
}), |
|
||||
{ path: `${base}/socket.io` } |
|
||||
); |
|
||||
} |
|
@ -1,11 +0,0 @@ |
|||||
import fs from 'fs-extra'; |
|
||||
import path from 'path'; |
|
||||
import { isUndefined } from 'lodash'; |
|
||||
|
|
||||
export default (sslkey, sslcert) => |
|
||||
isUndefined(sslkey) || isUndefined(sslcert) |
|
||||
? Promise.resolve({}) |
|
||||
: Promise.all([ |
|
||||
fs.readFile(path.resolve(sslkey)), |
|
||||
fs.readFile(path.resolve(sslcert)), |
|
||||
]).then(([key, cert]) => ({ key, cert })); |
|
@ -1,20 +0,0 @@ |
|||||
<!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, user-scalable=no"> |
|
||||
<title>WeTTy - The Web Terminal Emulator</title> |
|
||||
<link rel="stylesheet" href="main.css" /> |
|
||||
</head> |
|
||||
<body> |
|
||||
<div id="overlay"> |
|
||||
<div class="error"> |
|
||||
<div id="msg"></div> |
|
||||
<input type="button" onclick="location.reload();" value="reconnect" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div id="terminal"></div> |
|
||||
<script src="main.js"></script> |
|
||||
</body> |
|
||||
</html> |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -1,39 +0,0 @@ |
|||||
import { isUndefined } from 'lodash'; |
|
||||
|
|
||||
export function proposeGeometry({ element, renderer }) { |
|
||||
if (!element.parentElement) return null; |
|
||||
|
|
||||
const parentElementStyle = window.getComputedStyle(element.parentElement); |
|
||||
|
|
||||
return { |
|
||||
cols: Math.floor( |
|
||||
Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10)) / |
|
||||
renderer.dimensions.actualCellWidth, |
|
||||
), |
|
||||
rows: Math.floor( |
|
||||
parseInt(parentElementStyle.getPropertyValue('height'), 10) / |
|
||||
renderer.dimensions.actualCellHeight, |
|
||||
), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function fit(term) { |
|
||||
const { rows, cols } = proposeGeometry(term); |
|
||||
if (!isUndefined(rows) && !isUndefined(cols)) { |
|
||||
// Force a full render
|
|
||||
if (term.rows !== rows || term.cols !== cols) { |
|
||||
term.renderer.clear(); |
|
||||
term.resize(cols, rows); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function apply({ prototype }) { |
|
||||
prototype.proposeGeometry = function proProposeGeometry() { |
|
||||
return proposeGeometry(this); |
|
||||
}; |
|
||||
|
|
||||
prototype.fit = function proFit() { |
|
||||
return fit(this); |
|
||||
}; |
|
||||
} |
|
@ -1,9 +1,9 @@ |
|||||
import rl from 'readline'; |
import { createInterface } from 'readline'; |
||||
|
|
||||
ask('Enter your username'); |
ask('Enter your username'); |
||||
|
|
||||
export default function ask(question) { |
export default function ask(question: string): Promise<string> { |
||||
const r = rl.createInterface({ |
const r = createInterface({ |
||||
input: process.stdin, |
input: process.stdin, |
||||
output: process.stdout, |
output: process.stdout, |
||||
}); |
}); |
@ -0,0 +1,94 @@ |
|||||
|
import * as url from 'url'; |
||||
|
import { Socket } from 'socket.io'; |
||||
|
import { SSH } from './interfaces'; |
||||
|
|
||||
|
const localhost = (host: string): boolean => |
||||
|
process.getuid() === 0 && |
||||
|
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1'); |
||||
|
|
||||
|
const urlArgs = ( |
||||
|
referer: string, |
||||
|
def: { [s: string]: string } |
||||
|
): { [s: string]: string } => |
||||
|
Object.assign(def, url.parse(referer, true).query); |
||||
|
|
||||
|
const getRemoteAddress = (remoteAddress: string): string => |
||||
|
remoteAddress.split(':')[3] === undefined |
||||
|
? 'localhost' |
||||
|
: remoteAddress.split(':')[3]; |
||||
|
|
||||
|
export default ( |
||||
|
{ |
||||
|
request: { |
||||
|
headers: { referer }, |
||||
|
}, |
||||
|
client: { |
||||
|
conn: { remoteAddress }, |
||||
|
}, |
||||
|
}: Socket, |
||||
|
{ user, host, port, auth, pass, key }: SSH, |
||||
|
command: string |
||||
|
): { args: string[]; user: boolean } => ({ |
||||
|
args: localhost(host) |
||||
|
? loginOptions(command, remoteAddress) |
||||
|
: sshOptions( |
||||
|
urlArgs(referer, { |
||||
|
host: address(referer, user, host), |
||||
|
port: `${port}`, |
||||
|
pass, |
||||
|
command, |
||||
|
auth, |
||||
|
}), |
||||
|
key |
||||
|
), |
||||
|
user: |
||||
|
localhost(host) || |
||||
|
user !== '' || |
||||
|
user.includes('@') || |
||||
|
address(referer, user, host).includes('@'), |
||||
|
}); |
||||
|
|
||||
|
function parseCommand(command: string, path?: string): string { |
||||
|
if (command === 'login' && path === undefined) return ''; |
||||
|
return path !== undefined |
||||
|
? `$SHELL -c "cd ${path};${command === 'login' ? '$SHELL' : command}"` |
||||
|
: command; |
||||
|
} |
||||
|
|
||||
|
function sshOptions( |
||||
|
{ pass, path, command, host, port, auth }: { [s: string]: string }, |
||||
|
key?: string |
||||
|
): string[] { |
||||
|
const cmd = parseCommand(command, path); |
||||
|
const sshRemoteOptsBase = [ |
||||
|
'ssh', |
||||
|
host, |
||||
|
'-t', |
||||
|
'-p', |
||||
|
port, |
||||
|
'-o', |
||||
|
`PreferredAuthentications=${auth}`, |
||||
|
]; |
||||
|
if (key) { |
||||
|
return sshRemoteOptsBase.concat(['-i', key, cmd]); |
||||
|
} |
||||
|
if (pass) { |
||||
|
return ['sshpass', '-p', pass].concat(sshRemoteOptsBase, [cmd]); |
||||
|
} |
||||
|
if (cmd === '') { |
||||
|
return sshRemoteOptsBase; |
||||
|
} |
||||
|
return sshRemoteOptsBase.concat([cmd]); |
||||
|
} |
||||
|
|
||||
|
function loginOptions(command: string, remoteAddress: string): string[] { |
||||
|
return command === 'login' |
||||
|
? [command, '-h', getRemoteAddress(remoteAddress)] |
||||
|
: [command]; |
||||
|
} |
||||
|
|
||||
|
function address(referer: string, user: string, host: string): string { |
||||
|
const match = referer.match('.+/ssh/([^/]+)$'); |
||||
|
const fallback = user ? `${user}@${host}` : host; |
||||
|
return match ? `${match[1]}@${host}` : fallback; |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
import WeTTy from './wetty'; |
||||
|
|
||||
|
export default new WeTTy(); |
@ -0,0 +1,80 @@ |
|||||
|
import * as yargs from 'yargs'; |
||||
|
import logger from './logger'; |
||||
|
import wetty from './emitter'; |
||||
|
import WeTTy from './wetty'; |
||||
|
|
||||
|
export interface Options { |
||||
|
sshhost: string; |
||||
|
sshport: number; |
||||
|
sshuser: string; |
||||
|
sshauth: string; |
||||
|
sshkey?: string; |
||||
|
sshpass?: string; |
||||
|
sslkey?: string; |
||||
|
sslcert?: string; |
||||
|
base: string; |
||||
|
port: number; |
||||
|
command?: string; |
||||
|
} |
||||
|
|
||||
|
interface CLI extends Options { |
||||
|
help: boolean; |
||||
|
} |
||||
|
|
||||
|
export default class Server { |
||||
|
public static start({ |
||||
|
sshuser, |
||||
|
sshhost, |
||||
|
sshauth, |
||||
|
sshport, |
||||
|
sshkey, |
||||
|
sshpass, |
||||
|
base, |
||||
|
port, |
||||
|
command, |
||||
|
sslkey, |
||||
|
sslcert, |
||||
|
}: Options): Promise<void> { |
||||
|
wetty |
||||
|
.on('exit', ({ code, msg }: { code: number; msg: string }) => { |
||||
|
logger.info(`Exit with code: ${code} ${msg}`); |
||||
|
}) |
||||
|
.on('disconnect', () => { |
||||
|
logger.info('disconnect'); |
||||
|
}) |
||||
|
.on('spawn', ({ msg }) => logger.info(msg)) |
||||
|
.on('connection', ({ msg, date }) => logger.info(`${date} ${msg}`)) |
||||
|
.on('server', ({ msg }) => logger.info(msg)) |
||||
|
.on('debug', (msg: string) => logger.debug(msg)); |
||||
|
return wetty.start( |
||||
|
{ |
||||
|
user: sshuser, |
||||
|
host: sshhost, |
||||
|
auth: sshauth, |
||||
|
port: sshport, |
||||
|
pass: sshpass, |
||||
|
key: sshkey, |
||||
|
}, |
||||
|
base, |
||||
|
port, |
||||
|
command, |
||||
|
{ key: sslkey, cert: sslcert } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public static get wetty(): WeTTy { |
||||
|
return wetty; |
||||
|
} |
||||
|
|
||||
|
public static init(opts: CLI): void { |
||||
|
if (!opts.help) { |
||||
|
this.start(opts).catch(err => { |
||||
|
logger.error(err); |
||||
|
process.exitCode = 1; |
||||
|
}); |
||||
|
} else { |
||||
|
yargs.showHelp(); |
||||
|
process.exitCode = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
export interface SSH { |
||||
|
user: string; |
||||
|
host: string; |
||||
|
auth: string; |
||||
|
port: number; |
||||
|
pass?: string; |
||||
|
key?: string; |
||||
|
} |
||||
|
|
||||
|
export interface SSL { |
||||
|
key?: string; |
||||
|
cert?: string; |
||||
|
} |
||||
|
|
||||
|
export interface SSLBuffer { |
||||
|
key?: Buffer; |
||||
|
cert?: Buffer; |
||||
|
} |
@ -0,0 +1,84 @@ |
|||||
|
import * as compression from 'compression'; |
||||
|
import * as express from 'express'; |
||||
|
import * as favicon from 'serve-favicon'; |
||||
|
import * as helmet from 'helmet'; |
||||
|
import * as http from 'http'; |
||||
|
import * as https from 'https'; |
||||
|
import * as path from 'path'; |
||||
|
import * as socket from 'socket.io'; |
||||
|
import { isUndefined } from 'lodash'; |
||||
|
import * as morgan from 'morgan'; |
||||
|
import logger from './logger'; |
||||
|
import events from './emitter'; |
||||
|
import { SSLBuffer } from './interfaces'; |
||||
|
|
||||
|
const distDir = path.join('./', 'dist', 'client'); |
||||
|
|
||||
|
const trim = (str: string): string => str.replace(/\/*$/, ''); |
||||
|
|
||||
|
export default function createServer( |
||||
|
base: string, |
||||
|
port: number, |
||||
|
{ key, cert }: SSLBuffer |
||||
|
): SocketIO.Server { |
||||
|
const basePath = trim(base); |
||||
|
events.emit( |
||||
|
'debug', |
||||
|
`key: ${key}, cert: ${cert}, port: ${port}, base: ${base}` |
||||
|
); |
||||
|
|
||||
|
const html = ( |
||||
|
req: express.Request, |
||||
|
res: express.Response |
||||
|
): express.Response => |
||||
|
res.send(`<!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, user-scalable=no"> |
||||
|
<title>WeTTy - The Web Terminal Emulator</title> |
||||
|
<link rel="stylesheet" href="${basePath}/public/index.css" /> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="overlay"> |
||||
|
<div class="error"> |
||||
|
<div id="msg"></div> |
||||
|
<input type="button" onclick="location.reload();" value="reconnect" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="terminal"></div> |
||||
|
<script src="${basePath}/public/index.js"></script> |
||||
|
</body> |
||||
|
</html>`);
|
||||
|
|
||||
|
const app = express(); |
||||
|
app |
||||
|
.use(morgan('combined', { stream: logger.stream })) |
||||
|
.use(helmet()) |
||||
|
.use(compression()) |
||||
|
.use(favicon(path.join(distDir, 'favicon.ico'))) |
||||
|
.use(`${basePath}/public`, express.static(distDir)) |
||||
|
.use((req, res, next) => { |
||||
|
if ( |
||||
|
req.url.substr(-1) === '/' && |
||||
|
req.url.length > 1 && |
||||
|
!/\?[^]*\//.test(req.url) |
||||
|
) |
||||
|
res.redirect(301, req.url.slice(0, -1)); |
||||
|
else next(); |
||||
|
}) |
||||
|
.get(basePath, html) |
||||
|
.get(`${basePath}/ssh/:user`, html); |
||||
|
|
||||
|
return socket( |
||||
|
!isUndefined(key) && !isUndefined(cert) |
||||
|
? https.createServer({ key, cert }, app).listen(port, () => { |
||||
|
events.server(port, 'https'); |
||||
|
}) |
||||
|
: http.createServer(app).listen(port, () => { |
||||
|
events.server(port, 'http'); |
||||
|
}), |
||||
|
{ path: `${basePath}/socket.io` } |
||||
|
); |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
import { readFile } from 'fs-extra'; |
||||
|
import { resolve } from 'path'; |
||||
|
import { isUndefined } from 'lodash'; |
||||
|
import { SSL, SSLBuffer } from './interfaces'; |
||||
|
|
||||
|
export default async function loadSSL(ssl: SSL): Promise<SSLBuffer> { |
||||
|
if (isUndefined(ssl.key) || isUndefined(ssl.cert)) return {}; |
||||
|
const files = [readFile(resolve(ssl.key)), readFile(resolve(ssl.cert))]; |
||||
|
const [key, cert]: Buffer[] = await Promise.all(files); |
||||
|
return { key, cert }; |
||||
|
} |
@ -0,0 +1,131 @@ |
|||||
|
/** |
||||
|
* Create WeTTY server |
||||
|
* @module WeTTy |
||||
|
*/ |
||||
|
import * as EventEmitter from 'events'; |
||||
|
import server from './server'; |
||||
|
import getCommand from './command'; |
||||
|
import term from './term'; |
||||
|
import loadSSL from './ssl'; |
||||
|
import { SSL, SSH, SSLBuffer } from './interfaces'; |
||||
|
|
||||
|
export default class WeTTy extends EventEmitter { |
||||
|
/** |
||||
|
* Starts WeTTy Server |
||||
|
* @name start |
||||
|
*/ |
||||
|
public start( |
||||
|
ssh: SSH = { user: '', host: 'localhost', auth: 'password', port: 22 }, |
||||
|
basePath: string = '/wetty/', |
||||
|
serverPort: number = 3000, |
||||
|
command: string = '', |
||||
|
ssl?: SSL |
||||
|
): Promise<void> { |
||||
|
return loadSSL(ssl).then((sslBuffer: SSLBuffer) => { |
||||
|
if (ssh.key) { |
||||
|
this.emit( |
||||
|
'warn', |
||||
|
`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
! Password-less auth enabled using private key from ${ssh.key}. |
||||
|
! This is dangerous, anything that reaches the wetty server |
||||
|
! will be able to run remote operations without authentication. |
||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!` |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const io = server(basePath, serverPort, sslBuffer); |
||||
|
/** |
||||
|
* Wetty server connected too |
||||
|
* @fires WeTTy#connnection |
||||
|
*/ |
||||
|
io.on('connection', (socket: SocketIO.Socket) => { |
||||
|
/** |
||||
|
* @event wetty#connection |
||||
|
* @name connection |
||||
|
*/ |
||||
|
this.emit('connection', { |
||||
|
msg: `Connection accepted.`, |
||||
|
date: new Date(), |
||||
|
}); |
||||
|
const { args, user: sshUser } = getCommand(socket, ssh, command); |
||||
|
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`); |
||||
|
if (sshUser) { |
||||
|
term.spawn(socket, args); |
||||
|
} else { |
||||
|
term |
||||
|
.login(socket) |
||||
|
.then((username: string) => { |
||||
|
this.emit('debug', `username: ${username.trim()}`); |
||||
|
args[1] = `${username.trim()}@${args[1]}`; |
||||
|
this.emit('debug', `cmd : ${args.join(' ')}`); |
||||
|
return term.spawn(socket, args); |
||||
|
}) |
||||
|
.catch(() => this.disconnected()); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* terminal spawned |
||||
|
* |
||||
|
* @fires module:WeTTy#spawn |
||||
|
*/ |
||||
|
public spawned(pid: number, address: string): void { |
||||
|
/** |
||||
|
* Terminal process spawned |
||||
|
* @event WeTTy#spawn |
||||
|
* @name spawn |
||||
|
* @type {object} |
||||
|
*/ |
||||
|
this.emit('spawn', { |
||||
|
msg: `PID=${pid} STARTED on behalf of ${address}`, |
||||
|
pid, |
||||
|
address, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* terminal exited |
||||
|
* |
||||
|
* @fires WeTTy#exit |
||||
|
*/ |
||||
|
public exited(code: number, pid: number): void { |
||||
|
/** |
||||
|
* Terminal process exits |
||||
|
* @event WeTTy#exit |
||||
|
* @name exit |
||||
|
*/ |
||||
|
this.emit('exit', { code, msg: `PID=${pid} ENDED` }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Disconnect from WeTTY |
||||
|
* |
||||
|
* @fires WeTTy#disconnet |
||||
|
*/ |
||||
|
private disconnected(): void { |
||||
|
/** |
||||
|
* @event WeTTY#disconnect |
||||
|
* @name disconnect |
||||
|
*/ |
||||
|
this.emit('disconnect'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wetty server started |
||||
|
* @fires WeTTy#server |
||||
|
*/ |
||||
|
public server(port: number, connection: string): void { |
||||
|
/** |
||||
|
* @event WeTTy#server |
||||
|
* @type {object} |
||||
|
* @name server |
||||
|
*/ |
||||
|
this.emit('server', { |
||||
|
msg: `${connection} on port ${port}`, |
||||
|
port, |
||||
|
connection, |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"module": "commonjs", |
||||
|
"outDir": "./dist", |
||||
|
"allowJs": true, |
||||
|
"esModuleInterop": false, |
||||
|
"target": "es6", |
||||
|
"noImplicitAny": true, |
||||
|
"moduleResolution": "node", |
||||
|
"sourceMap": true, |
||||
|
"baseUrl": ".", |
||||
|
"paths": { |
||||
|
"*": [ |
||||
|
"node_modules/", |
||||
|
"src/types/*" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"include": [ |
||||
|
"./src/**/*.ts" |
||||
|
] |
||||
|
} |
@ -0,0 +1,134 @@ |
|||||
|
import path from 'path'; |
||||
|
import webpack from 'webpack'; |
||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; |
||||
|
import nodeExternals from 'webpack-node-externals'; |
||||
|
|
||||
|
const template = override => |
||||
|
Object.assign( |
||||
|
{ |
||||
|
mode: process.env.NODE_ENV || 'development', |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'src'), 'node_modules'], |
||||
|
extensions: ['.ts', '.json', '.js', '.node'], |
||||
|
}, |
||||
|
|
||||
|
stats: { |
||||
|
colors: true, |
||||
|
}, |
||||
|
}, |
||||
|
override |
||||
|
); |
||||
|
|
||||
|
const entry = (folder, file) => |
||||
|
path.join(__dirname, 'src', folder, `${file}.ts`); |
||||
|
|
||||
|
const entries = (folder, files) => |
||||
|
Object.assign(...files.map(file => ({ [file]: entry(folder, file) }))); |
||||
|
|
||||
|
export default [ |
||||
|
template({ |
||||
|
entry: entries('server', ['index', 'buffer']), |
||||
|
target: 'node', |
||||
|
devtool: 'source-map', |
||||
|
output: { |
||||
|
path: path.resolve(__dirname, 'dist'), |
||||
|
libraryTarget: 'commonjs2', |
||||
|
filename: '[name].js', |
||||
|
}, |
||||
|
externals: [nodeExternals()], |
||||
|
module: { |
||||
|
rules: [ |
||||
|
{ |
||||
|
test: /\.ts$/, |
||||
|
use: { |
||||
|
loader: 'babel-loader', |
||||
|
options: { |
||||
|
presets: [ |
||||
|
'@babel/preset-typescript', |
||||
|
[ |
||||
|
'@babel/preset-env', |
||||
|
{ |
||||
|
targets: { |
||||
|
node: 'current', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
], |
||||
|
plugins: ['lodash'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.js$/, |
||||
|
use: ['source-map-loader'], |
||||
|
enforce: 'pre', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
plugins: [new webpack.IgnorePlugin(/uws/)], |
||||
|
}), |
||||
|
template({ |
||||
|
entry: entries('client', ['index']), |
||||
|
output: { |
||||
|
path: path.resolve(__dirname, 'dist', 'client'), |
||||
|
filename: '[name].js', |
||||
|
}, |
||||
|
module: { |
||||
|
rules: [ |
||||
|
{ |
||||
|
test: /\.ts$/, |
||||
|
use: { |
||||
|
loader: 'babel-loader', |
||||
|
options: { |
||||
|
presets: [ |
||||
|
'@babel/preset-typescript', |
||||
|
[ |
||||
|
'@babel/preset-env', |
||||
|
{ |
||||
|
targets: { |
||||
|
browsers: ['last 2 versions', 'safari >= 7'], |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
], |
||||
|
plugins: ['lodash'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.js$/, |
||||
|
use: ['source-map-loader'], |
||||
|
enforce: 'pre', |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.scss$/, |
||||
|
use: [ |
||||
|
{ |
||||
|
loader: MiniCssExtractPlugin.loader, |
||||
|
}, |
||||
|
{ |
||||
|
loader: 'css-loader', |
||||
|
}, |
||||
|
{ |
||||
|
loader: 'sass-loader', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.(jpg|jpeg|png|gif|mp3|svg|ico)$/, |
||||
|
loader: 'file-loader', |
||||
|
options: { |
||||
|
name: '[name].[ext]', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
plugins: [ |
||||
|
new MiniCssExtractPlugin({ |
||||
|
filename: '[name].css', |
||||
|
chunkFilename: '[id].css', |
||||
|
}), |
||||
|
], |
||||
|
devtool: 'source-map', |
||||
|
}), |
||||
|
]; |
@ -1,74 +0,0 @@ |
|||||
const webpack = require('webpack'); |
|
||||
const path = require('path'); |
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); |
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
|
||||
|
|
||||
const extractSass = new ExtractTextPlugin({ |
|
||||
filename: '[name].css', |
|
||||
disable: process.env.NODE_ENV === 'development', |
|
||||
}); |
|
||||
|
|
||||
const loader = new webpack.ProvidePlugin({ |
|
||||
fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch', |
|
||||
}); |
|
||||
|
|
||||
module.exports = { |
|
||||
entry: './src/index.js', |
|
||||
output: { |
|
||||
filename: '[name].js', |
|
||||
path: path.resolve(__dirname, 'dist'), |
|
||||
}, |
|
||||
module: { |
|
||||
loaders: [ |
|
||||
{ |
|
||||
test: /\.js$/, |
|
||||
loader: 'babel-loader', |
|
||||
exclude: /node_modules/, |
|
||||
query: { |
|
||||
plugins: ['lodash'], |
|
||||
presets: [ |
|
||||
[ |
|
||||
'env', |
|
||||
{ |
|
||||
targets: { |
|
||||
browsers: ['last 2 versions', 'safari >= 7'], |
|
||||
}, |
|
||||
}, |
|
||||
], |
|
||||
], |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
test: /\.scss$/, |
|
||||
use: extractSass.extract({ |
|
||||
use: [ |
|
||||
{ |
|
||||
loader: 'css-loader', |
|
||||
options: { minimize: true }, |
|
||||
}, |
|
||||
{ |
|
||||
loader: 'sass-loader', |
|
||||
}, |
|
||||
], |
|
||||
fallback: 'style-loader', |
|
||||
}), |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
plugins: |
|
||||
process.env.NODE_ENV !== 'development' |
|
||||
? [ |
|
||||
loader, |
|
||||
extractSass, |
|
||||
new UglifyJSPlugin({ |
|
||||
parallel: true, |
|
||||
uglifyOptions: { |
|
||||
ecma: 8, |
|
||||
}, |
|
||||
}), |
|
||||
] |
|
||||
: [loader, extractSass], |
|
||||
stats: { |
|
||||
colors: true, |
|
||||
}, |
|
||||
}; |
|
File diff suppressed because it is too large
Loading…
Reference in new issue