Cian Butler
6 years ago
committed by
GitHub
42 changed files with 5654 additions and 20641 deletions
@ -1,11 +1,10 @@ |
|||||
{ |
{ |
||||
"presets": [ |
"presets": [ |
||||
[ |
"@babel/preset-typescript", |
||||
"@babel/preset-env", |
["@babel/env"] |
||||
{ |
|
||||
"modules": false |
|
||||
} |
|
||||
] |
|
||||
], |
], |
||||
"compact": true |
"compact": true, |
||||
|
"plugins": [ |
||||
|
"lodash" |
||||
|
] |
||||
} |
} |
||||
|
@ -1,2 +1,10 @@ |
|||||
node_modules |
node_modules |
||||
.esm-cache |
.esm-cache |
||||
|
dist |
||||
|
*.yml |
||||
|
*.md |
||||
|
*.log |
||||
|
*.png |
||||
|
**/*.conf |
||||
|
**/*.service |
||||
|
Dockerfile |
||||
|
@ -1,4 +1,5 @@ |
|||||
node_modules/ |
node_modules/ |
||||
public/ |
|
||||
.esm-cache |
.esm-cache |
||||
|
dist |
||||
|
public/ |
||||
*hterm* |
*hterm* |
||||
|
@ -1,36 +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, |
||||
}, |
}, |
||||
extends: ['airbnb'], |
root: true, |
||||
rules : { |
extends: [ |
||||
'linebreak-style' : ['error', 'unix'], |
'airbnb-base', |
||||
'arrow-parens' : ['error', 'as-needed'], |
'plugin:typescript/recommended', |
||||
'no-param-reassign' : ['error', { props: false }], |
'plugin:prettier/recommended', |
||||
'func-style' : ['error', 'declaration', { allowArrowFunctions: true }], |
], |
||||
|
rules: { |
||||
|
'typescript/indent': 'off', |
||||
|
'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-use-before-define': ['error', { functions: false }], |
||||
'no-shadow' : [ |
'typescript/no-use-before-define': ['error', { functions: false }], |
||||
'error', |
}, |
||||
{ |
settings: { |
||||
builtinGlobals: true, |
'import/resolver': { |
||||
hoist : 'functions', |
'typescript-eslint-parser': ['.ts', '.tsx'], |
||||
allow : ['resolve', 'reject', 'err'], |
node: { |
||||
}, |
extensions: ['.ts', '.js'], |
||||
], |
|
||||
'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' }, |
|
||||
}, |
}, |
||||
], |
}, |
||||
}, |
}, |
||||
}; |
}; |
||||
|
@ -0,0 +1,13 @@ |
|||||
|
module.exports = { |
||||
|
singleQuote: true, |
||||
|
trailingComma: 'es5', |
||||
|
proseWrap: 'always', |
||||
|
overrides: [ |
||||
|
{ |
||||
|
files: ['*.js', '*.ts'], |
||||
|
options: { |
||||
|
printWidth: 80, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}; |
@ -1,17 +1,22 @@ |
|||||
FROM node:8-alpine as builder |
FROM node:boron-alpine as builder |
||||
|
RUN apk add -U build-base python |
||||
WORKDIR /usr/src/app |
WORKDIR /usr/src/app |
||||
RUN apk add --update build-base python |
|
||||
COPY . /usr/src/app |
COPY . /usr/src/app |
||||
RUN yarn |
RUN yarn && \ |
||||
FROM node:8-alpine |
yarn build && \ |
||||
MAINTAINER butlerx@notthe.cloud |
yarn install --production --ignore-scripts --prefer-offline |
||||
WORKDIR /app |
|
||||
RUN adduser -D -h /home/term -s /bin/sh term && \ |
FROM node:boron-alpine |
||||
( echo "term:term" | chpasswd ) && \ |
LABEL maintainer="butlerx@notthe.cloud" |
||||
apk add openssh-client && \ |
WORKDIR /usr/src/app |
||||
apk add sshpass |
ENV NODE_ENV=production |
||||
USER term |
RUN apk add -U openssh-client sshpass |
||||
EXPOSE 3000 |
EXPOSE 3000 |
||||
COPY --from=builder /usr/src/app /app |
COPY --from=builder /usr/src/app/dist /usr/src/app/dist |
||||
|
COPY --from=builder /usr/src/app/node_modules /usr/src/app/node_modules |
||||
|
COPY package.json /usr/src/app |
||||
|
COPY index.js /usr/src/app |
||||
RUN mkdir ~/.ssh |
RUN mkdir ~/.ssh |
||||
CMD ssh-keyscan -H wetty-ssh >> ~/.ssh/known_hosts && node bin |
RUN ssh-keyscan -H wetty-ssh >> ~/.ssh/known_hosts |
||||
|
|
||||
|
ENTRYPOINT [ "node", "." ] |
||||
|
@ -1,3 +1,3 @@ |
|||||
FROM sickp/alpine-sshd:latest |
FROM sickp/alpine-sshd:latest |
||||
RUN adduser -D -h /home/term -s /bin/sh term && \ |
RUN adduser -D -h /home/term -s /bin/sh term && \ |
||||
( echo "term:term" | chpasswd ) |
( echo "term:term" | chpasswd ) |
||||
|
@ -1,3 +0,0 @@ |
|||||
#! /usr/bin/env node
|
|
||||
require = require('esm')(module, { cjs: true }); // eslint-disable-line no-global-assign
|
|
||||
require('../cli.mjs'); |
|
@ -0,0 +1,69 @@ |
|||||
|
server { |
||||
|
listen ${NGINX_PORT}; |
||||
|
listen [::]:${NGINX_PORT}; |
||||
|
|
||||
|
server_name ${NGINX_DOMAIN}; |
||||
|
root /var/www/${NGINX_DOMAIN}/public; |
||||
|
|
||||
|
# $uri, index.html |
||||
|
location / { |
||||
|
try_files $uri $uri/ /index.html; |
||||
|
} |
||||
|
|
||||
|
# headers |
||||
|
add_header X-Frame-Options "SAMEORIGIN" always; |
||||
|
add_header X-XSS-Protection "1; mode=block" always; |
||||
|
add_header X-Content-Type-Options "nosniff" always; |
||||
|
add_header X-UA-Compatible "IE=Edge" always; |
||||
|
add_header Cache-Control "no-transform" always; |
||||
|
|
||||
|
# . files |
||||
|
location ~ /\. { |
||||
|
deny all; |
||||
|
} |
||||
|
|
||||
|
# assets, media |
||||
|
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ { |
||||
|
expires 7d; |
||||
|
access_log off; |
||||
|
} |
||||
|
|
||||
|
# svg, fonts |
||||
|
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff|woff2)$ { |
||||
|
add_header Access-Control-Allow-Origin "*"; |
||||
|
expires 7d; |
||||
|
access_log off; |
||||
|
} |
||||
|
|
||||
|
location ^~ /wetty { |
||||
|
proxy_pass http://${WETTY_HOST}:${WETTY_PORT}; |
||||
|
proxy_http_version 1.1; |
||||
|
proxy_set_header Upgrade $http_upgrade; |
||||
|
proxy_set_header Connection "upgrade"; |
||||
|
proxy_read_timeout 43200000; |
||||
|
|
||||
|
proxy_set_header X-Real-IP $remote_addr; |
||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
||||
|
proxy_set_header Host $http_host; |
||||
|
proxy_set_header X-NginX-Proxy true; |
||||
|
} |
||||
|
|
||||
|
# gzip |
||||
|
gzip on; |
||||
|
gzip_vary on; |
||||
|
gzip_proxied any; |
||||
|
gzip_comp_level 6; |
||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; |
||||
|
} |
||||
|
|
||||
|
# subdomains redirect |
||||
|
server { |
||||
|
listen ${NGINX_PORT}; |
||||
|
listen [::]:${NGINX_PORT}; |
||||
|
|
||||
|
server_name *.${NGINX_DOMAIN}; |
||||
|
|
||||
|
return 301 https://${NGINX_DOMAIN}$request_uri; |
||||
|
} |
||||
|
|
||||
|
# set ft=conf |
@ -1,15 +0,0 @@ |
|||||
#!/usr/bin/env sh |
|
||||
|
|
||||
userAtAddress="$1"; shift |
|
||||
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,124 +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', |
|
||||
}, |
|
||||
sshpass: { |
|
||||
demand: false, |
|
||||
description: 'ssh password', |
|
||||
}, |
|
||||
sshauth: { |
|
||||
demand: false, |
|
||||
description: 'defaults to "password", you can use "publickey,password" instead', |
|
||||
}, |
|
||||
sshkey: { |
|
||||
demand: false, |
|
||||
description: |
|
||||
'path to an optional client private key (connection will be password-less and insecure!)', |
|
||||
}, |
|
||||
port: { |
|
||||
demand: false, |
|
||||
alias: 'p', |
|
||||
description: 'wetty listen port', |
|
||||
}, |
|
||||
command: { |
|
||||
demand: false, |
|
||||
alias: 'c', |
|
||||
description: 'command to run in shell, defaults to /bin/login', |
|
||||
}, |
|
||||
help: { |
|
||||
demand: false, |
|
||||
alias: 'h', |
|
||||
description: 'Print help message', |
|
||||
}, |
|
||||
}) |
|
||||
.boolean('allow_discovery').argv; |
|
||||
|
|
||||
if (opts.help) { |
|
||||
optimist.showHelp(); |
|
||||
process.exit(0); |
|
||||
} |
|
||||
|
|
||||
const sshkey = opts.sshkey || process.env.SSHKEY || ''; |
|
||||
|
|
||||
loadSSL(opts) |
|
||||
.then(ssl => { |
|
||||
opts.ssl = ssl; |
|
||||
}) |
|
||||
.catch(err => { |
|
||||
console.error(`Error: ${err}`); |
|
||||
process.exit(1); |
|
||||
}); |
|
||||
|
|
||||
const sshkeyWarning = `!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||
! Password-less auth enabled using private key from ${sshkey}. |
|
||||
! This is dangerous, anything that reaches the wetty server |
|
||||
! will be able to run remote operations without authentication. |
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
|
|
||||
if (sshkey) { |
|
||||
console.warn(sshkeyWarning); |
|
||||
} |
|
||||
|
|
||||
process.on('uncaughtException', err => { |
|
||||
console.error(`Error: ${err}`); |
|
||||
}); |
|
||||
|
|
||||
const tty = wetty( |
|
||||
opts.port || process.env.PORT || 3000, |
|
||||
opts.sshuser || process.env.SSHUSER || '', |
|
||||
opts.sshpass || process.env.SSHPASS || '', |
|
||||
opts.sshhost || process.env.SSHHOST || 'localhost', |
|
||||
opts.sshport || process.env.SSHPOST || 22, |
|
||||
opts.sshauth || process.env.SSHAUTH || 'password,keyboard-interactive', |
|
||||
sshkey, |
|
||||
opts.ssl, |
|
||||
opts.command || process.env.COMMAND || '', |
|
||||
); |
|
||||
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,41 +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'); |
|
||||
|
|
||||
const 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('default', gulp.series(compress)); |
|
||||
|
|
||||
gulp.task( |
|
||||
'upgrade', |
|
||||
gulp.series( |
|
||||
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, |
|
||||
}, |
|
||||
), |
|
||||
() => del(['./libapps']), |
|
||||
), |
|
||||
); |
|
@ -1,2 +1,93 @@ |
|||||
require = require('esm')(module, { cjs: true }); // eslint-disable-line no-global-assign
|
/* eslint-disable typescript/no-var-requires */ |
||||
module.exports = require('./wetty.mjs').default; |
|
||||
|
const yargs = require('yargs'); |
||||
|
const wetty = require('./dist').default; |
||||
|
|
||||
|
module.exports = wetty.wetty; |
||||
|
|
||||
|
/** |
||||
|
* Check if being run by cli or require |
||||
|
*/ |
||||
|
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,41 +0,0 @@ |
|||||
<!doctype html> |
|
||||
<html lang="en"> |
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<title>Wetty - The WebTTY Terminal Emulator</title> |
|
||||
<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/socket.io/socket.io.js"></script> |
|
||||
<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' }, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
}; |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -0,0 +1,77 @@ |
|||||
|
import { Terminal } from 'xterm'; |
||||
|
import { isUndefined } from 'lodash'; |
||||
|
import * as io from 'socket.io-client'; |
||||
|
import { fit } from 'xterm/lib/addons/fit/fit'; |
||||
|
import './wetty.scss'; |
||||
|
import './favicon.ico'; |
||||
|
|
||||
|
const userRegex = new RegExp('ssh/[^/]+$'); |
||||
|
const trim = (str: string): string => str.replace(/\/*$/, ''); |
||||
|
const socketBase = trim(window.location.pathname).replace(userRegex, ''); |
||||
|
const socket = io(window.location.origin, { |
||||
|
path: `${trim(socketBase)}/socket.io`, |
||||
|
}); |
||||
|
|
||||
|
socket.on('connect', () => { |
||||
|
const term = new Terminal(); |
||||
|
term.open(document.getElementById('terminal')); |
||||
|
term.setOption('fontSize', 14); |
||||
|
document.getElementById('overlay').style.display = 'none'; |
||||
|
window.addEventListener('beforeunload', handler, false); |
||||
|
/* |
||||
|
term.scrollPort_.screen_.setAttribute('contenteditable', 'false'); |
||||
|
*/ |
||||
|
|
||||
|
term.attachCustomKeyEventHandler(e => { |
||||
|
// Ctrl + Shift + C
|
||||
|
if (e.ctrlKey && e.shiftKey && e.keyCode === 67) { |
||||
|
e.preventDefault(); |
||||
|
document.execCommand('copy'); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
function resize(): void { |
||||
|
fit(term); |
||||
|
socket.emit('resize', { cols: term.cols, rows: term.rows }); |
||||
|
} |
||||
|
window.onresize = resize; |
||||
|
resize(); |
||||
|
term.focus(); |
||||
|
|
||||
|
function kill(data: string): void { |
||||
|
disconnect(data); |
||||
|
} |
||||
|
|
||||
|
term.on('data', data => { |
||||
|
socket.emit('input', data); |
||||
|
}); |
||||
|
term.on('resize', size => { |
||||
|
socket.emit('resize', size); |
||||
|
}); |
||||
|
socket |
||||
|
.on('data', (data: string) => { |
||||
|
term.write(data); |
||||
|
}) |
||||
|
.on('login', () => { |
||||
|
term.writeln(''); |
||||
|
resize(); |
||||
|
}) |
||||
|
.on('logout', kill) |
||||
|
.on('disconnect', kill) |
||||
|
.on('error', (err: string | null) => { |
||||
|
if (err) disconnect(err); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function disconnect(reason: string): void { |
||||
|
document.getElementById('overlay').style.display = 'block'; |
||||
|
if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason; |
||||
|
window.removeEventListener('beforeunload', handler, false); |
||||
|
} |
||||
|
|
||||
|
function handler(e: { returnValue: string }): string { |
||||
|
e.returnValue = 'Are you sure?'; |
||||
|
return e.returnValue; |
||||
|
} |
@ -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%; |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,16 @@ |
|||||
|
import { createInterface } from 'readline'; |
||||
|
|
||||
|
ask('Enter your username'); |
||||
|
|
||||
|
export default function ask(question: string): Promise<string> { |
||||
|
const r = createInterface({ |
||||
|
input: process.stdin, |
||||
|
output: process.stdout, |
||||
|
}); |
||||
|
return new Promise(resolve => { |
||||
|
r.question(`${question}: `, answer => { |
||||
|
r.close(); |
||||
|
resolve(answer); |
||||
|
}); |
||||
|
}); |
||||
|
} |
@ -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,32 @@ |
|||||
|
import { createLogger, format, transports } from 'winston'; |
||||
|
|
||||
|
const { combine, timestamp, label, printf, colorize } = format; |
||||
|
|
||||
|
const logger = createLogger({ |
||||
|
format: |
||||
|
process.env.NODE_ENV === 'development' |
||||
|
? combine( |
||||
|
colorize({ all: true }), |
||||
|
label({ label: 'Wetty' }), |
||||
|
timestamp(), |
||||
|
printf( |
||||
|
info => |
||||
|
`${info.timestamp} [${info.label}] ${info.level}: ${info.message}` |
||||
|
) |
||||
|
) |
||||
|
: format.json(), |
||||
|
transports: [ |
||||
|
new transports.Console({ |
||||
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', |
||||
|
handleExceptions: true, |
||||
|
}), |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
logger.stream = { |
||||
|
write(message: string): void { |
||||
|
logger.info(message); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
export default logger; |
@ -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,65 @@ |
|||||
|
import { spawn } from 'node-pty'; |
||||
|
import { isUndefined } from 'lodash'; |
||||
|
import events from './emitter'; |
||||
|
|
||||
|
const xterm = { |
||||
|
name: 'xterm-256color', |
||||
|
cols: 80, |
||||
|
rows: 30, |
||||
|
cwd: process.cwd(), |
||||
|
env: process.env, |
||||
|
}; |
||||
|
|
||||
|
export default class Term { |
||||
|
public static spawn(socket: SocketIO.Socket, args: string[]): void { |
||||
|
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'); |
||||
|
socket |
||||
|
.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', () => { |
||||
|
const { pid } = term; |
||||
|
term.kill(); |
||||
|
events.exited(0, pid); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static login(socket: SocketIO.Socket): Promise<string> { |
||||
|
const term = spawn('/usr/bin/env', ['node', './dist/buffer.js'], 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: string) => { |
||||
|
term.write(input); |
||||
|
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input; |
||||
|
}) |
||||
|
.on('disconnect', () => { |
||||
|
term.kill(); |
||||
|
reject(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -1,84 +0,0 @@ |
|||||
const socket = io(location.origin, { path: '/wetty/socket.io' }); |
|
||||
let term; |
|
||||
let buf = ''; |
|
||||
|
|
||||
class Wetty { |
|
||||
constructor(argv) { |
|
||||
this.argv_ = argv; |
|
||||
this.io = null; |
|
||||
this.pid_ = -1; |
|
||||
} |
|
||||
|
|
||||
run() { |
|
||||
this.io = this.argv_.io.push(); |
|
||||
this.io.onVTKeystroke = this.sendString_.bind(this); |
|
||||
this.io.sendString = this.sendString_.bind(this); |
|
||||
this.io.onTerminalResize = this.onTerminalResize.bind(this); |
|
||||
} |
|
||||
|
|
||||
sendString_(str) { |
|
||||
socket.emit('input', str); |
|
||||
} |
|
||||
|
|
||||
onTerminalResize(col, row) { |
|
||||
socket.emit('resize', { col, row }); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
socket.on('connect', () => { |
|
||||
document.getElementById('overlay').style.display = 'none'; |
|
||||
window.addEventListener('beforeunload', handler, false); |
|
||||
lib.init(() => { |
|
||||
hterm.defaultStorage = new lib.Storage.Local(); |
|
||||
term = new hterm.Terminal(); |
|
||||
window.term = term; |
|
||||
term.decorate(document.getElementById('terminal')); |
|
||||
|
|
||||
term.setCursorPosition(0, 0); |
|
||||
term.setCursorVisible(true); |
|
||||
term.prefs_.set('ctrl-c-copy', true); |
|
||||
term.prefs_.set('ctrl-v-paste', true); |
|
||||
term.prefs_.set('use-default-window-copy', true); |
|
||||
term.prefs_.set('send-encoding', 'raw'); |
|
||||
term.prefs_.set('receive-encoding', 'raw'); |
|
||||
term.prefs_.set('font-size', 14); |
|
||||
term.scrollPort_.screen_.setAttribute('spellcheck', 'false'); |
|
||||
term.scrollPort_.screen_.setAttribute('autocorrect', 'false'); |
|
||||
term.scrollPort_.screen_.setAttribute('autocomplete', 'false'); |
|
||||
term.scrollPort_.screen_.setAttribute('contenteditable', 'false'); |
|
||||
|
|
||||
term.runCommandClass(Wetty, document.location.hash.substr(1)); |
|
||||
socket.emit('resize', { |
|
||||
col: term.screenSize.width, |
|
||||
row: term.screenSize.height, |
|
||||
}); |
|
||||
|
|
||||
if (buf && buf !== '') { |
|
||||
term.io.writeUTF8(buf); |
|
||||
buf = ''; |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
socket.on('output', data => { |
|
||||
if (!term) { |
|
||||
buf += data; |
|
||||
return; |
|
||||
} |
|
||||
term.io.writeUTF8(data); |
|
||||
}); |
|
||||
|
|
||||
socket.on('logout', () => { |
|
||||
document.getElementById('overlay').style.display = 'block'; |
|
||||
window.removeEventListener('beforeunload', handler, false); |
|
||||
}); |
|
||||
|
|
||||
socket.on('disconnect', () => { |
|
||||
document.getElementById('overlay').style.display = 'block'; |
|
||||
window.removeEventListener('beforeunload', handler, false); |
|
||||
}); |
|
||||
|
|
||||
function handler(e) { |
|
||||
e.returnValue = 'Are you sure?'; |
|
||||
return e.returnValue; |
|
||||
} |
|
@ -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,150 +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 'node-pty'; |
|
||||
import EventEmitter from 'events'; |
|
||||
import favicon from 'serve-favicon'; |
|
||||
import url from 'url'; |
|
||||
|
|
||||
const dirname = path.resolve(); |
|
||||
|
|
||||
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}`); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
const urlArgs = request => url.parse(request.headers.referer, true).query; |
|
||||
const getRemoteAddress = socket => |
|
||||
socket.client.conn.remoteAddress.split(':')[3] === undefined |
|
||||
? 'localhost' |
|
||||
: socket.client.conn.remoteAddress.split(':')[3]; |
|
||||
|
|
||||
function sshOptions(path, address, port, auth, key, query) { |
|
||||
const sshRemoteOptsBase = [ |
|
||||
path, |
|
||||
address, |
|
||||
'-t', |
|
||||
'-p', |
|
||||
port, |
|
||||
'-o', |
|
||||
`PreferredAuthentications=${auth}`, |
|
||||
query.command, |
|
||||
]; |
|
||||
if (key) { |
|
||||
return sshRemoteOptsBase.concat(['-i', key]); |
|
||||
} else if (query.sshpass) { |
|
||||
return ['sshpass', '-p', query.sshpass].concat(sshRemoteOptsBase); |
|
||||
} |
|
||||
return sshRemoteOptsBase; |
|
||||
} |
|
||||
|
|
||||
function getCommand(socket, sshuser, sshpass, sshhost, sshport, sshauth, sshkey, command) { |
|
||||
const { request } = socket; |
|
||||
const match = request.headers.referer.match('.+/ssh/.+$'); |
|
||||
const sshAddress = sshuser ? `${sshuser}@${sshhost}` : sshhost; |
|
||||
const query = urlArgs(request); |
|
||||
query.sshpass = query.sshpass || sshpass; |
|
||||
query.command = |
|
||||
query.path !== undefined |
|
||||
? `$SHELL -c "cd ${query.path};${command === '' ? '$SHELL' : command}"` |
|
||||
: command; |
|
||||
const ssh = match |
|
||||
? `${ |
|
||||
match[0] |
|
||||
.split('/ssh/') |
|
||||
.pop() |
|
||||
.split('?')[0] |
|
||||
}@${sshhost}` |
|
||||
: sshAddress; |
|
||||
const args = command === '' ? ['login', '-h', getRemoteAddress(socket)] : [command]; |
|
||||
|
|
||||
return [ |
|
||||
process.getuid() === 0 && sshhost === 'localhost' |
|
||||
? args |
|
||||
: sshOptions( |
|
||||
sshuser || match ? 'ssh' : path.join(dirname, 'bin/ssh'), |
|
||||
ssh, |
|
||||
sshport, |
|
||||
sshauth, |
|
||||
sshkey, |
|
||||
query, |
|
||||
), |
|
||||
ssh, |
|
||||
]; |
|
||||
} |
|
||||
|
|
||||
export default function start( |
|
||||
port, |
|
||||
sshuser, |
|
||||
sshpass, |
|
||||
sshhost, |
|
||||
sshport, |
|
||||
sshauth, |
|
||||
sshkey, |
|
||||
sslopts, |
|
||||
command, |
|
||||
) { |
|
||||
const events = new EventEmitter(); |
|
||||
const io = server(createServer(port, sslopts), { path: '/wetty/socket.io' }); |
|
||||
io.on('connection', socket => { |
|
||||
console.log(`${new Date()} Connection accepted.`); |
|
||||
const [args, ssh] = getCommand( |
|
||||
socket, |
|
||||
sshuser, |
|
||||
sshpass, |
|
||||
sshhost, |
|
||||
sshport, |
|
||||
sshauth, |
|
||||
sshkey, |
|
||||
command, |
|
||||
); |
|
||||
console.debug({ args, ssh }); |
|
||||
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 remote=${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