Browse Source
* switch too xterm and webpack * handle disconects clearner * stop browser from over rulling ctrl+shift+c * update pkg.json * replace log statements with events * restructure cli * reduce use of shared state * remove ssh wrapper * minify, uglify and tree shake code bundle * use seperate containers for building frontend * fit sizing errors * add helmet * fix term resize * add loger * use custom fix function * stop server crashing after disconnect * make better on mobile * use a pty as a buffer to handle all keyboard keys * unwrap var * clean up structure of code * update readme * add api * refactor event emitter * expand emmitter class * fix event emitter calls * format docs * fix docs * clean up webpackpull/126/head v1.0.0
Cian Butler
7 years ago
committed by
GitHub
35 changed files with 4674 additions and 20366 deletions
@ -1,11 +0,0 @@ |
|||
{ |
|||
"presets": [ |
|||
[ |
|||
"es2015", |
|||
{ |
|||
"modules": false |
|||
} |
|||
] |
|||
], |
|||
"compact": true, |
|||
} |
@ -1,2 +1,10 @@ |
|||
node_modules |
|||
.esm-cache |
|||
dist |
|||
*.yml |
|||
*.md |
|||
*.log |
|||
*.png |
|||
**/*.conf |
|||
**/*.service |
|||
Dockerfile |
|||
|
@ -1,4 +1,3 @@ |
|||
node_modules/ |
|||
public/ |
|||
.esm-cache |
|||
*hterm* |
|||
dist |
|||
|
@ -1,36 +1,16 @@ |
|||
module.exports = { |
|||
env: { |
|||
es6 : true, |
|||
es6: true, |
|||
node: true, |
|||
browser: true |
|||
}, |
|||
extends: ['airbnb'], |
|||
rules : { |
|||
'linebreak-style' : ['error', 'unix'], |
|||
'arrow-parens' : ['error', 'as-needed'], |
|||
'no-param-reassign' : ['error', { props: false }], |
|||
'func-style' : ['error', 'declaration', { allowArrowFunctions: true }], |
|||
'no-use-before-define': ['error', { functions: false }], |
|||
'no-shadow' : [ |
|||
'error', |
|||
{ |
|||
builtinGlobals: true, |
|||
hoist : 'functions', |
|||
allow : ['resolve', 'reject', 'err'], |
|||
}, |
|||
], |
|||
'no-console': [ |
|||
'error', |
|||
{ |
|||
allow: ['warn', 'trace', 'log', 'error'], |
|||
}, |
|||
], |
|||
'consistent-return': 0, |
|||
'key-spacing' : [ |
|||
'error', |
|||
{ |
|||
multiLine: { beforeColon: false, afterColon: true }, |
|||
align : { beforeColon: false, afterColon: true, on: 'colon', mode: 'strict' }, |
|||
}, |
|||
], |
|||
}, |
|||
root: true, |
|||
extends: ["airbnb-base", "plugin:prettier/recommended"], |
|||
rules: { |
|||
"linebreak-style": ["error", "unix"], |
|||
"arrow-parens": ["error", "as-needed"], |
|||
"no-param-reassign": ["error", { props: false }], |
|||
"func-style": ["error", "declaration", { allowArrowFunctions: true }], |
|||
"no-use-before-define": ["error", { functions: false }] |
|||
} |
|||
}; |
|||
|
@ -0,0 +1,13 @@ |
|||
module.exports = { |
|||
singleQuote: true, |
|||
trailingComma: 'all', |
|||
proseWrap: 'always', |
|||
overrides: [ |
|||
{ |
|||
files: ['*.js', '*.mjs'], |
|||
options: { |
|||
printWidth: 80, |
|||
}, |
|||
}, |
|||
], |
|||
}; |
@ -1,9 +1,19 @@ |
|||
FROM node:8-alpine |
|||
MAINTAINER butlerx@notthe.cloud |
|||
FROM node:alpine as builder |
|||
WORKDIR /usr/src/app |
|||
COPY . /usr/src/app |
|||
RUN apk add -U build-base python && \ |
|||
yarn && \ |
|||
yarn build && \ |
|||
yarn install --production --ignore-scripts --prefer-offline |
|||
|
|||
FROM node:alpine |
|||
LABEL maintainer="butlerx@notthe.cloud" |
|||
WORKDIR /app |
|||
RUN adduser -D -h /home/term -s /bin/sh term && \ |
|||
echo "term:term" | chpasswd |
|||
ENV NODE_ENV=production |
|||
RUN apk add -U openssh && \ |
|||
adduser -D -h /home/term -s /bin/sh term && \ |
|||
echo "term:term" | chpasswd |
|||
EXPOSE 3000 |
|||
COPY . /app |
|||
RUN apk add --update build-base python openssh && yarn |
|||
COPY --from=builder /usr/src/app /app |
|||
|
|||
CMD yarn start |
|||
|
@ -1,3 +0,0 @@ |
|||
#! /usr/bin/env node
|
|||
require = require('@std/esm')(module); // eslint-disable-line no-global-assign
|
|||
require('../cli.mjs'); |
@ -1,15 +0,0 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
userAtAddress="$1" |
|||
USER=$(echo "$userAtAddress" | cut -d"@" -f1); |
|||
HOST=$(echo "$userAtAddress" | cut -d"@" -f2); |
|||
|
|||
if [ "$USER" = "$HOST" ] |
|||
then |
|||
printf "Enter your username: " |
|||
read -r USER |
|||
USER=$(echo "${USER}" | tr -d '[:space:]') |
|||
ssh "$USER"@"$HOST" |
|||
else |
|||
ssh "$userAtAddress" |
|||
fi |
@ -1,95 +0,0 @@ |
|||
import fs from 'fs-extra'; |
|||
import path from 'path'; |
|||
import optimist from 'optimist'; |
|||
import wetty from './wetty'; |
|||
|
|||
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', |
|||
}, |
|||
port: { |
|||
demand : false, |
|||
alias : 'p', |
|||
description: 'wetty listen port', |
|||
}, |
|||
help: { |
|||
demand : false, |
|||
alias : 'h', |
|||
description: 'Print help message', |
|||
}, |
|||
}) |
|||
.boolean('allow_discovery').argv; |
|||
|
|||
if (opts.help) { |
|||
optimist.showHelp(); |
|||
process.exit(0); |
|||
} |
|||
|
|||
const sshuser = opts.sshuser || process.env.SSHUSER || ''; |
|||
const sshhost = opts.sshhost || process.env.SSHHOST || 'localhost'; |
|||
const sshauth = opts.sshauth || process.env.SSHAUTH || 'password'; |
|||
const sshport = opts.sshport || process.env.SSHPOST || 22; |
|||
const port = opts.port || process.env.PORT || 3000; |
|||
|
|||
loadSSL(opts) |
|||
.then(ssl => { |
|||
opts.ssl = ssl; |
|||
}) |
|||
.catch(err => { |
|||
console.error(`Error: ${err}`); |
|||
process.exit(1); |
|||
}); |
|||
|
|||
process.on('uncaughtException', err => { |
|||
console.error(`Error: ${err}`); |
|||
}); |
|||
|
|||
const tty = wetty(port, sshuser, sshhost, sshport, sshauth, opts.ssl); |
|||
tty.on('exit', code => { |
|||
console.log(`exit with code: ${code}`); |
|||
}); |
|||
tty.on('disconnect', () => { |
|||
console.log('disconnect'); |
|||
}); |
|||
|
|||
function loadSSL({ sslkey, sslcert }) { |
|||
return new Promise((resolve, reject) => { |
|||
const ssl = {}; |
|||
if (sslkey && sslcert) { |
|||
fs |
|||
.readFile(path.resolve(sslkey)) |
|||
.then(key => { |
|||
ssl.key = key; |
|||
}) |
|||
.then(fs.readFile(path.resolve(sslcert))) |
|||
.then(cert => { |
|||
ssl.cert = cert; |
|||
}) |
|||
.then(resolve(ssl)) |
|||
.catch(reject); |
|||
} |
|||
resolve(ssl); |
|||
}); |
|||
} |
@ -0,0 +1,93 @@ |
|||
<a name="module_WeTTy"></a> |
|||
|
|||
## WeTTy |
|||
|
|||
Create WeTTY server |
|||
|
|||
* [WeTTy](#module_WeTTy) |
|||
* [~start](#module_WeTTy..start) ⇒ <code>Promise</code> |
|||
* ["connection"](#event_connection) |
|||
* ["spawn"](#event_spawn) |
|||
* ["exit"](#event_exit) |
|||
* ["disconnect"](#event_disconnect) |
|||
* ["server"](#event_server) |
|||
|
|||
<a name="module_WeTTy..start"></a> |
|||
|
|||
### WeTTy~start ⇒ <code>Promise</code> |
|||
|
|||
Starts WeTTy Server |
|||
|
|||
**Kind**: inner property of [<code>WeTTy</code>](#module_WeTTy) |
|||
**Returns**: <code>Promise</code> - Promise resolves once server is running |
|||
|
|||
| Param | Type | Default | Description | |
|||
| ------------ | ------------------- | ------------------------------------- | --------------------------- | |
|||
| [ssh] | <code>Object</code> | | SSH settings | |
|||
| [ssh.user] | <code>string</code> | <code>"''"</code> | default user for ssh | |
|||
| [ssh.host] | <code>string</code> | <code>"localhost"</code> | machine to ssh too | |
|||
| [ssh.auth] | <code>string</code> | <code>"password"</code> | authtype to use | |
|||
| [ssh.port] | <code>number</code> | <code>22</code> | port to connect to over ssh | |
|||
| [serverPort] | <code>number</code> | <code>3000</code> | Port to run server on | |
|||
| [ssl] | <code>Object</code> | | SSL settings | |
|||
| [ssl.key] | <code>string</code> | | Path to ssl key | |
|||
| [ssl.cert] | <code>string</code> | | Path to ssl cert | |
|||
|
|||
<a name="event_connection"></a> |
|||
|
|||
### "connection" |
|||
|
|||
**Kind**: event emitted by [<code>WeTTy</code>](#module_WeTTy) |
|||
**Properties** |
|||
|
|||
| Name | Type | Description | |
|||
| ---- | ------------------- | --------------------------- | |
|||
| msg | <code>string</code> | Message for logs | |
|||
| date | <code>Date</code> | date and time of connection | |
|||
|
|||
<a name="event_spawn"></a> |
|||
|
|||
### "spawn" |
|||
|
|||
Terminal process spawned |
|||
|
|||
**Kind**: event emitted by [<code>WeTTy</code>](#module_WeTTy) |
|||
**Properties** |
|||
|
|||
| Name | Type | Description | |
|||
| ------- | ------------------- | -------------------------------------- | |
|||
| msg | <code>string</code> | Message containing pid info and status | |
|||
| pid | <code>number</code> | Pid of the terminal | |
|||
| address | <code>string</code> | address of connecting user | |
|||
|
|||
<a name="event_exit"></a> |
|||
|
|||
### "exit" |
|||
|
|||
Terminal process exits |
|||
|
|||
**Kind**: event emitted by [<code>WeTTy</code>](#module_WeTTy) |
|||
**Properties** |
|||
|
|||
| Name | Type | Description | |
|||
| ---- | ------------------- | -------------------------------------- | |
|||
| code | <code>number</code> | the exit code | |
|||
| msg | <code>string</code> | Message containing pid info and status | |
|||
|
|||
<a name="event_disconnect"></a> |
|||
|
|||
### "disconnect" |
|||
|
|||
**Kind**: event emitted by [<code>WeTTy</code>](#module_WeTTy) |
|||
<a name="event_server"></a> |
|||
|
|||
### "server" |
|||
|
|||
**Kind**: event emitted by [<code>WeTTy</code>](#module_WeTTy) |
|||
**Properties** |
|||
|
|||
| Name | Type | Description | |
|||
| ---------- | ------------------- | ------------------------------- | |
|||
| msg | <code>string</code> | Message for logging | |
|||
| port | <code>number</code> | port sever is on | |
|||
| connection | <code>string</code> | connection type for web traffic | |
@ -0,0 +1,24 @@ |
|||
# Docs |
|||
|
|||
## Getting started |
|||
|
|||
WeTTy is event driven. To Spawn a new server call `wetty.start()` with no |
|||
arguments. |
|||
|
|||
```javascript |
|||
const wetty = require('wetty.js'); |
|||
|
|||
wetty |
|||
.on('exit', ({ code, msg }) => { |
|||
console.log(`Exit with code: ${code} ${msg}`); |
|||
}) |
|||
.on('spawn', msg => console.log(msg)); |
|||
wetty.start(/* server settings, see Options */).then(() => { |
|||
console.log('server running'); |
|||
/* code you want to execute */ |
|||
}); |
|||
``` |
|||
|
|||
## API |
|||
|
|||
For WeTTy options and event details please refer to the [api docs](./api.md) |
@ -1,40 +0,0 @@ |
|||
const gulp = require('gulp'); |
|||
const concat = require('gulp-concat'); |
|||
const minify = require('gulp-minify'); |
|||
const babel = require('gulp-babel'); |
|||
const shell = require('gulp-shell'); |
|||
const del = require('del'); |
|||
|
|||
gulp.task('compress', [], () => |
|||
gulp |
|||
.src(['./src/hterm_all.js', './src/wetty.js']) |
|||
.pipe(concat('wetty.js')) |
|||
.pipe(babel()) |
|||
.pipe( |
|||
minify({ |
|||
ext: { |
|||
min: '.min.js', |
|||
}, |
|||
exclude : ['tasks'], |
|||
noSource : true, |
|||
ignoreFiles: ['.combo.js', '*.min.js'], |
|||
}), |
|||
) |
|||
.pipe(gulp.dest('./public/wetty')), |
|||
); |
|||
|
|||
gulp.task( |
|||
'hterm', |
|||
shell.task( |
|||
[ |
|||
'git clone https://chromium.googlesource.com/apps/libapps', |
|||
'LIBDOT_SEARCH_PATH=$(pwd)/libapps ./libapps/libdot/bin/concat.sh -i ./libapps/hterm/concat/hterm_all.concat -o ./src/hterm_all.js', |
|||
], |
|||
{ |
|||
verbose: true, |
|||
}, |
|||
), |
|||
); |
|||
|
|||
gulp.task('default', ['compress']); |
|||
gulp.task('upgrade', ['hterm', 'compress'], () => del(['./libapps'])); |
@ -1,2 +1,12 @@ |
|||
require = require('@std/esm')(module); // eslint-disable-line no-global-assign
|
|||
module.exports = require('./wetty.mjs').default; |
|||
/* eslint-disable */ |
|||
require = require('@std/esm')(module, { |
|||
cjs: 'true', |
|||
esm: 'js', |
|||
}); |
|||
const wetty = require('./lib/index.mjs').default; |
|||
module.exports = wetty.wetty; |
|||
|
|||
/** |
|||
* Check if being run by cli or require |
|||
*/ |
|||
if (require.main === module) wetty.init(); |
|||
|
@ -0,0 +1,15 @@ |
|||
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('@'), |
|||
}); |
|||
|
|||
function address(headers, user, host) { |
|||
const match = headers.referer.match('.+/ssh/.+$'); |
|||
const fallback = user ? `${user}@${host}` : host; |
|||
return match ? `${match[0].split('/ssh/').pop()}@${host}` : fallback; |
|||
} |
@ -0,0 +1,144 @@ |
|||
/** |
|||
* 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} [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 }, |
|||
serverPort = 3000, |
|||
{ key, cert }, |
|||
) { |
|||
return loadSSL(key, cert).then(ssl => { |
|||
const io = server(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, |
|||
}); |
|||
if (sshUser) { |
|||
term.spawn(socket, args); |
|||
} else { |
|||
term |
|||
.login(socket) |
|||
.then(username => { |
|||
args[1] = `${username.trim()}@${args[1]}`; |
|||
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(); |
@ -0,0 +1,92 @@ |
|||
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', |
|||
}, |
|||
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, |
|||
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)); |
|||
return wetty.start( |
|||
{ |
|||
user: sshuser, |
|||
host: sshhost, |
|||
auth: sshauth, |
|||
port: sshport, |
|||
}, |
|||
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; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
import { createLogger, format, transports } from 'winston'; |
|||
|
|||
const { combine, timestamp, label, printf, colorize } = format; |
|||
|
|||
const logger = createLogger({ |
|||
format: combine( |
|||
colorize({ all: true }), |
|||
label({ label: 'Wetty' }), |
|||
timestamp(), |
|||
printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`), |
|||
), |
|||
transports: [new transports.Console({ handleExceptions: true })], |
|||
}); |
|||
|
|||
export default logger; |
@ -0,0 +1,37 @@ |
|||
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 events from './emitter.mjs'; |
|||
|
|||
const pubDir = path.join(__dirname, '..', 'public'); |
|||
|
|||
export default function createServer(port, { key, cert }) { |
|||
const app = express(); |
|||
const wetty = (req, res) => res.sendFile(path.join(pubDir, 'index.html')); |
|||
app |
|||
.use(helmet()) |
|||
.use(compression()) |
|||
.use(favicon(path.join(pubDir, 'favicon.ico'))) |
|||
.get('/wetty/ssh/:user', wetty) |
|||
.get('/wetty/', wetty) |
|||
.use('/wetty', express.static(path.join(__dirname, '..', 'dist'))) |
|||
.get('/ssh/:user', wetty) |
|||
.get('/', wetty); |
|||
|
|||
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: '/wetty/socket.io' }, |
|||
); |
|||
} |
@ -0,0 +1,11 @@ |
|||
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 })); |
@ -0,0 +1,65 @@ |
|||
import { spawn } from 'node-pty'; |
|||
import { isUndefined } from 'lodash'; |
|||
import events from './emitter.mjs'; |
|||
|
|||
const xterm = { |
|||
name: 'xterm-256color', |
|||
cols: 80, |
|||
rows: 30, |
|||
}; |
|||
|
|||
export default class Term { |
|||
static spawn(socket, args) { |
|||
const term = spawn('/usr/bin/env', args, xterm); |
|||
const address = args[0] === 'ssh' ? args[1] : 'localhost'; |
|||
events.spawned(term.pid, address); |
|||
socket.emit('login'); |
|||
term.on('exit', code => { |
|||
events.exited(code, term.pid); |
|||
socket |
|||
.emit('logout') |
|||
.removeAllListeners('disconnect') |
|||
.removeAllListeners('resize') |
|||
.removeAllListeners('input'); |
|||
}); |
|||
term.on('data', data => { |
|||
socket.emit('data', data); |
|||
}); |
|||
socket |
|||
.on('resize', ({ cols, rows }) => { |
|||
term.resize(cols, rows); |
|||
}) |
|||
.on('input', input => { |
|||
if (!isUndefined(term)) term.write(input); |
|||
}) |
|||
.on('disconnect', () => { |
|||
term.end(); |
|||
term.destroy(); |
|||
events.exited(); |
|||
}); |
|||
} |
|||
|
|||
static login(socket) { |
|||
socket.emit('data', 'Enter your username: '); |
|||
const term = spawn('/usr/bin/env', ['sh', '-c', 'read'], xterm); |
|||
let buf = ''; |
|||
return new Promise((resolve, reject) => { |
|||
term.on('exit', () => { |
|||
resolve(buf); |
|||
}); |
|||
term.on('data', data => { |
|||
socket.emit('data', data); |
|||
}); |
|||
socket |
|||
.on('input', input => { |
|||
term.write(input); |
|||
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input; |
|||
}) |
|||
.on('disconnect', () => { |
|||
term.end(); |
|||
term.destroy(); |
|||
reject(); |
|||
}); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
<!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 WebTTY Terminal Emulator</title> |
|||
<link rel="stylesheet" href="/wetty/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="/wetty/main.js"></script> |
|||
</body> |
|||
</html> |
@ -1,41 +0,0 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>Wetty - The WebTTY Terminal Emulator</title> |
|||
<script src="/wetty/socket.io/socket.io.js"></script> |
|||
<style> |
|||
html, body { |
|||
height: 100%; |
|||
width: 100%; |
|||
margin: 0px; |
|||
} |
|||
#overlay { |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 100%; |
|||
background-color: rgba(0,0,0,0.75);; |
|||
display: none; |
|||
z-index: 100; |
|||
} |
|||
#overlay input { |
|||
display: block; |
|||
margin: auto; |
|||
position: relative; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
} |
|||
#terminal { |
|||
display: block; |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div id="overlay"><input type="button" onclick="location.reload();" value="reconnect" /></div> |
|||
<div id="terminal"></div> |
|||
<script src="/wetty/wetty.min.js"></script> |
|||
</body> |
|||
</html> |
File diff suppressed because one or more lines are too long
@ -1,37 +0,0 @@ |
|||
module.exports = { |
|||
env: { |
|||
es6 : true, |
|||
browser: true, |
|||
}, |
|||
globals: { |
|||
hterm: true, |
|||
lib : true, |
|||
io : true, |
|||
}, |
|||
extends: ['airbnb'], |
|||
rules : { |
|||
'no-underscore-dangle': 0, |
|||
'class-methods-use-this': 0, |
|||
'linebreak-style' : ['error', 'unix'], |
|||
'arrow-parens' : ['error', 'as-needed'], |
|||
'no-param-reassign' : ['error', { props: false }], |
|||
'func-style' : ['error', 'declaration', { allowArrowFunctions: true }], |
|||
'no-use-before-define': ['error', { functions: false }], |
|||
'no-shadow' : [ |
|||
'error', |
|||
{ |
|||
builtinGlobals: true, |
|||
hoist : 'functions', |
|||
allow : ['resolve', 'reject', 'err'], |
|||
}, |
|||
], |
|||
'consistent-return': 0, |
|||
'key-spacing' : [ |
|||
'error', |
|||
{ |
|||
multiLine: { beforeColon: false, afterColon: true }, |
|||
align : { beforeColon: false, afterColon: true, on: 'colon', mode: 'strict' }, |
|||
}, |
|||
], |
|||
}, |
|||
}; |
@ -0,0 +1,39 @@ |
|||
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); |
|||
}; |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,47 @@ |
|||
@import '~xterm/dist/xterm'; |
|||
|
|||
$black: #000; |
|||
$grey: rgba(0, 0, 0, 0.75); |
|||
$white: #fff; |
|||
|
|||
html, |
|||
body { |
|||
background-color: $black; |
|||
height: 100%; |
|||
margin: 0; |
|||
overflow: hidden; |
|||
|
|||
#overlay { |
|||
background-color: $grey; |
|||
display: none; |
|||
height: 100%; |
|||
position: absolute; |
|||
width: 100%; |
|||
z-index: 100; |
|||
|
|||
.error { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
justify-content: center; |
|||
width: 100%; |
|||
|
|||
#msg { |
|||
align-self: center; |
|||
color: $white; |
|||
} |
|||
|
|||
input { |
|||
align-self: center; |
|||
margin: 16px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
#terminal { |
|||
display: flex; |
|||
height: 100%; |
|||
position: relative; |
|||
width: 100%; |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
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/wetty.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, |
|||
}, |
|||
}; |
@ -1,82 +0,0 @@ |
|||
import express from 'express'; |
|||
import http from 'http'; |
|||
import https from 'https'; |
|||
import path from 'path'; |
|||
import server from 'socket.io'; |
|||
import pty from 'pty.js'; |
|||
import EventEmitter from 'events'; |
|||
import favicon from 'serve-favicon'; |
|||
|
|||
const app = express(); |
|||
app.use(favicon(`${__dirname}/public/favicon.ico`)); |
|||
// For using wetty at /wetty on a vhost
|
|||
app.get('/wetty/ssh/:user', (req, res) => { |
|||
res.sendFile(`${__dirname}/public/wetty/index.html`); |
|||
}); |
|||
app.get('/wetty/', (req, res) => { |
|||
res.sendFile(`${__dirname}/public/wetty/index.html`); |
|||
}); |
|||
// For using wetty on a vhost by itself
|
|||
app.get('/ssh/:user', (req, res) => { |
|||
res.sendFile(`${__dirname}/public/wetty/index.html`); |
|||
}); |
|||
app.get('/', (req, res) => { |
|||
res.sendFile(`${__dirname}/public/wetty/index.html`); |
|||
}); |
|||
// For serving css and javascript
|
|||
app.use('/', express.static(path.join(__dirname, 'public'))); |
|||
|
|||
function createServer(port, sslopts) { |
|||
return sslopts && sslopts.key && sslopts.cert |
|||
? https.createServer(sslopts, app).listen(port, () => { |
|||
console.log(`https on port ${port}`); |
|||
}) |
|||
: http.createServer(app).listen(port, () => { |
|||
console.log(`http on port ${port}`); |
|||
}); |
|||
} |
|||
|
|||
function getCommand(socket, sshuser, sshhost, sshport, sshauth) { |
|||
const request = socket.request; |
|||
const match = request.headers.referer.match('.+/ssh/.+$'); |
|||
const sshAddress = sshuser ? `${sshuser}@${sshhost}` : sshhost; |
|||
const ssh = match ? `${match[0].split('/ssh/').pop()}@${sshhost}` : sshAddress; |
|||
const args = |
|||
process.getuid() === 0 && sshhost === 'localhost' |
|||
? ['login', '-h', socket.client.conn.remoteAddress.split(':')[3]] |
|||
: ['bin/ssh', ssh, '-p', sshport, '-o', `PreferredAuthentications=${sshauth}`]; |
|||
return [args, ssh]; |
|||
} |
|||
|
|||
export default function start(port, sshuser, sshhost, sshport, sshauth, sslopts) { |
|||
const httpserv = createServer(port, sslopts); |
|||
const events = new EventEmitter(); |
|||
const io = server(httpserv, { path: '/wetty/socket.io' }); |
|||
io.on('connection', socket => { |
|||
console.log(`${new Date()} Connection accepted.`); |
|||
const [args, ssh] = getCommand(socket, sshuser, sshhost, sshport, sshauth); |
|||
const term = pty.spawn('/usr/bin/env', args, { |
|||
name: 'xterm-256color', |
|||
cols: 80, |
|||
rows: 30, |
|||
}); |
|||
|
|||
console.log(`${new Date()} PID=${term.pid} STARTED on behalf of user=${ssh}`); |
|||
term.on('data', data => { |
|||
socket.emit('output', data); |
|||
}); |
|||
term.on('exit', code => { |
|||
console.log(`${new Date()} PID=${term.pid} ENDED`); |
|||
socket.emit('logout'); |
|||
events.emit('exit', code); |
|||
}); |
|||
socket.on('resize', ({ col, row }) => term.resize(col, row)); |
|||
socket.on('input', input => term.write(input)); |
|||
socket.on('disconnect', () => { |
|||
term.end(); |
|||
term.destroy(); |
|||
events.emit('disconnect'); |
|||
}); |
|||
}); |
|||
return events; |
|||
} |
File diff suppressed because it is too large
Loading…
Reference in new issue