Browse Source

Replace hterm with Xterm.js (#29)

* 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 webpack
pull/126/head v1.0.0
Cian Butler 7 years ago
committed by GitHub
parent
commit
616ba39386
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      .babelrc
  2. 8
      .dockerignore
  3. 3
      .eslintignore
  4. 42
      .eslintrc.js
  5. 2
      .gitignore
  6. 13
      .prettierrc.js
  7. 22
      Dockerfile
  8. 203
      README.md
  9. 3
      bin/index.js
  10. 15
      bin/ssh
  11. 95
      cli.mjs
  12. 3
      docker-compose.yml
  13. 93
      docs/API.md
  14. 24
      docs/README.md
  15. 40
      gulpfile.js
  16. 14
      index.js
  17. 15
      lib/command.mjs
  18. 144
      lib/emitter.mjs
  19. 92
      lib/index.mjs
  20. 15
      lib/logger.mjs
  21. 37
      lib/server.mjs
  22. 11
      lib/ssl.mjs
  23. 65
      lib/term.mjs
  24. 88
      package.json
  25. 20
      public/index.html
  26. 41
      public/wetty/index.html
  27. 1
      public/wetty/wetty.min.js
  28. 37
      src/.eslintrc.js
  29. 39
      src/fit.js
  30. 18392
      src/hterm_all.js
  31. 113
      src/wetty.js
  32. 47
      src/wetty.scss
  33. 74
      webpack.config.js
  34. 82
      wetty.mjs
  35. 5136
      yarn.lock

11
.babelrc

@ -1,11 +0,0 @@
{
"presets": [
[
"es2015",
{
"modules": false
}
]
],
"compact": true,
}

8
.dockerignore

@ -1,2 +1,10 @@
node_modules node_modules
.esm-cache .esm-cache
dist
*.yml
*.md
*.log
*.png
**/*.conf
**/*.service
Dockerfile

3
.eslintignore

@ -1,4 +1,3 @@
node_modules/ node_modules/
public/
.esm-cache .esm-cache
*hterm* dist

42
.eslintrc.js

@ -1,36 +1,16 @@
module.exports = { module.exports = {
env: { env: {
es6 : true, es6: true,
node: true, node: true,
browser: true
}, },
extends: ['airbnb'], root: true,
rules : { extends: ["airbnb-base", "plugin:prettier/recommended"],
'linebreak-style' : ['error', 'unix'], rules: {
'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-shadow' : [ "no-use-before-define": ["error", { functions: false }]
'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' },
},
],
},
}; };

2
.gitignore

@ -15,4 +15,4 @@ results
npm-debug.log npm-debug.log
node_modules/* node_modules/*
.esm-cache .esm-cache
libapps dist

13
.prettierrc.js

@ -0,0 +1,13 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
proseWrap: 'always',
overrides: [
{
files: ['*.js', '*.mjs'],
options: {
printWidth: 80,
},
},
],
};

22
Dockerfile

@ -1,9 +1,19 @@
FROM node:8-alpine FROM node:alpine as builder
MAINTAINER butlerx@notthe.cloud 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 WORKDIR /app
RUN adduser -D -h /home/term -s /bin/sh term && \ ENV NODE_ENV=production
echo "term:term" | chpasswd RUN apk add -U openssh && \
adduser -D -h /home/term -s /bin/sh term && \
echo "term:term" | chpasswd
EXPOSE 3000 EXPOSE 3000
COPY . /app COPY --from=builder /usr/src/app /app
RUN apk add --update build-base python openssh && yarn
CMD yarn start CMD yarn start

203
README.md

@ -1,145 +1,155 @@
Wetty = Web + tty ## WeTTy = Web + TTy
-----------------
Terminal over HTTP and HTTPS. Wetty is an alternative to Terminal over HTTP and https. WeTTy is an alternative to ajaxterm and anyterm
ajaxterm/anyterm but much better than them because wetty uses ChromeOS' but much better than them because WeTTy uses xterm.js which is a full fledged
terminal emulator (hterm) which is a full fledged implementation of implementation of terminal emulation written entirely in JavaScript. WeTTy uses
terminal emulation written entirely in Javascript. Also it uses websockets rather then Ajax and hence better response time.
websockets instead of Ajax and hence better response time.
[hterm source](https://chromium.googlesource.com/apps/libapps/+/master/hterm/) ![WeTTy](/terminal.png?raw=true)
![Wetty](/terminal.png?raw=true) This fork was originally bug fixes and updates, but has since evolved in to a
full rewrite to use xterm.js to have better support and make it more
This fork has a few of the open PR's from the original merged in as well as scripts to make running maintainable.
in docker better.
## Install ## Install
- `git clone https://github.com/butlerx/wetty` WeTTy can be installed from source or from npm. To install from source run:
- `cd wetty`
- `yarn` ```bash
$ git clone https://github.com/butlerx/wetty
$ cd wetty
$ yarn
```
or or install it globally with yarn, `yarn -g add wetty.js`, or npm,
`npm i -g wetty.js`
`yarn add wetty.js` ## Running WeTTy
## Run on HTTP Wettu can either be run as a standalone service or from another node script. To
see how to use WeTTy from node see the [API Doc](./docs)
``` bash ```bash
node bin/index.js -p 3000 $ node index.js
``` ```
If you run it as root it will launch `/bin/login` (where you can specify Open your browser on `http://yourserver:3000/` and you will prompted to login.
the user name), else it will launch `ssh` and connect by default to Or go too `http://yourserver:3000/ssh/<username>` to specify the user before
`localhost`. hand.
If instead you wish to connect to a remote host you can specify the ### Flags
`--sshhost` option, the SSH port using the `--sshport` option and the
SSH user using the `--sshuser` option.
You can also specify the SSH user name in the address bar like this: WeTTy can be run with the `--help` flag to get a full list of flags.
`http://yourserver:3000/wetty/ssh/<username>` WeTTy runs on port `3000` by default. You can change the default port by tunning
with the `--port` or `-p` flag.
or If WeTTy is run as root while the host is set as the local machine it will use
the `login` binary rather than ssh. If no host is specified it will use
`localhost` as the ssh host.
`http://yourserver:3000/ssh/<username>` If instead you wish to connect to a remote host you can specify the host with
the `--sshhost` flag and pass the IP or DNS address of the host you want to
connect to.
## Run on HTTPS You can specify the default user used to ssh to a host using the `--sshuser`.
This user can overwritten by going to `http://yourserver:3000/ssh/<username>`.
If this is left blank a user will be prompted to enter their username when they
connect.
Always use HTTPS. If you don't have SSL certificates from a CA you can By default WeTTy will try to ssh to port `22`, if your host uses an alternative
create a self signed certificate using this command: ssh port this can be specified with the flag `--sshport`.
``` ### https
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```
And then run: Always use https especially with a terminal to your server. You can add https by
either using WeTTy behind a proxy or directly.
``` If you don't have SSL certificates from a CA you can create a self signed
node bin/index.js --sslkey key.pem --sslcert cert.pem -p 3000 certificate using this command:
```
Again, if you run it as root it will launch `/bin/login`, else it will ```bash
launch SSH to `localhost` or a specified host as explained above. $ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```
## Run wetty behind nginx or apache To run WeTTy directly with ssl use both the `--sslkey` and `--sslcert` flags and
pass them the path too your cert and key as follows:
Put the following configuration in nginx's conf: ```bash
node index.js --sslkey key.pem --sslcert cert.pem -p 3000
```
location /wetty { ### Behind a Proxy
proxy_pass http://127.0.0.1:3000/wetty;
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; As said earlier you can use a proxy to add https to WeTTy.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
Put the following configuration in apache's conf: **Note** that if your proxy is configured for https you should run WeTTy without
SSL
RewriteCond %{REQUEST_URI} ^/wetty/socket.io [NC] #### Nginx
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /wetty/socket.io/(.*) ws://localhost:9123/wetty/socket.io/$1 [P,L]
<LocationMatch ^/wetty/(.*)> Put the following configuration in nginx's conf:
DirectorySlash On
Require all granted
ProxyPassMatch http://127.0.0.1:9123
ProxyPassReverse /wetty/
</LocationMatch>
If you are running `bin/index.js` as `root` and have an Nginx proxy you have to use: ```nginx
location /wetty {
proxy_pass http://127.0.0.1:3000/wetty;
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;
http://yourserver.com/wetty proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
``` ```
Else if you are running `bin/index.js` as a regular user you can use: #### Apache
``` Put the following configuration in apache's conf:
http://yourserver.com/wetty/ssh/<username>
```
or ```apache
RewriteCond %{REQUEST_URI} ^/wetty/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /wetty/socket.io/(.*) ws://localhost:3000/wetty/socket.io/$1 [P,L]
<LocationMatch ^/wetty/(.*)>
DirectorySlash On
Require all granted
ProxyPassMatch http://127.0.0.1:3000
ProxyPassReverse /wetty/
</LocationMatch>
``` ```
http://yourserver.com/wetty
```
**Note that if your Nginx is configured for HTTPS you should run wetty without SSL.**
## Dockerized Version ### Dockerized Version
This repo includes a Dockerfile you can use to run a Dockerized version of wetty. You can run WeTTy can be run from a container to ssh to a remote host or the host system.
whatever you want! This is handy for quick deployments. Just modify `docker-compose.yml` for your
host and run:
Just modify docker-compose and run: ```sh
$ docker-compose up -d
```
docker-compose up -d
``` ```
Visit the appropriate URL in your browser (`[localhost|$(boot2docker ip)]:PORT`). Visit the appropriate URL in your browser
(`[localhost|$(boot2docker ip)]:PORT`).
The default username is `term` and the password is `term`, if you did not modify `SSHHOST` The default username is `term` and the password is `term`, if you did not modify
`SSHHOST`
If you dont want to build the image yourself just remove the line `build; .` In the docker version all flags can be accessed as environment variables such as
`SSHHOST` or `SSHPORT`.
## Run wetty as a service daemon ## Run WeTTy as a service daemon
Install wetty globally with global option: Install WeTTy globally with global option:
### init.d ### init.d
```bash ```bash
$ sudo yarn global add wetty.js $ sudo yarn global add wetty.js
$ sudo cp /usr/local/lib/node_modules/wetty.js/bin/wetty.conf /etc/init $ sudo cp ~/.config/yarn/global/node_modules/wetty.js/bin/wetty.conf /etc/init
$ sudo start wetty $ sudo start wetty
``` ```
@ -152,18 +162,17 @@ $ systemctl --user enable wetty
$ systemctl --user start wetty $ systemctl --user start wetty
``` ```
This will start wetty on port 3000. If you want to change the port or redirect This will start WeTTy on port 3000. If you want to change the port or redirect
stdout/stderr you should change the last line in `wetty.conf` file, something stdout/stderr you should change the last line in `wetty.conf` file, something
like this: like this:
```
```systemd
exec sudo -u root wetty -p 80 >> /var/log/wetty.log 2>&1 exec sudo -u root wetty -p 80 >> /var/log/wetty.log 2>&1
``` ```
## FAQ ## FAQ
### What browsers are supported? ### What browsers are supported?
Wetty supports all browsers that Google's hterm supports. Wetty has been [reported](https://github.com/krishnasrinivas/wetty/issues/45#issuecomment-181448586) to work on Google Chrome, Firefox and IE 11. WeTTy supports all browsers that
[xterm.js supports](https://github.com/xtermjs/xterm.js#browser-support).
### Why isn't Wetty working with IE?
[This fix](https://stackoverflow.com/questions/13102116/access-denied-for-localstorage-in-ie10#20848924) has been known to help some users.

3
bin/index.js

@ -1,3 +0,0 @@
#! /usr/bin/env node
require = require('@std/esm')(module); // eslint-disable-line no-global-assign
require('../cli.mjs');

15
bin/ssh

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

95
cli.mjs

@ -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);
});
}

3
docker-compose.yml

@ -1,8 +1,6 @@
version: "3" version: "3"
services: services:
wetty: wetty:
build: .
image: butlerx/wetty image: butlerx/wetty
container_name: wetty container_name: wetty
tty: true tty: true
@ -10,6 +8,5 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
PORT: 3000
SSHHOST: 'localhost' SSHHOST: 'localhost'
SSHPORT: 22 SSHPORT: 22

93
docs/API.md

@ -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>&quot;&#x27;&#x27;&quot;</code> | default user for ssh |
| [ssh.host] | <code>string</code> | <code>&quot;localhost&quot;</code> | machine to ssh too |
| [ssh.auth] | <code>string</code> | <code>&quot;password&quot;</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 |

24
docs/README.md

@ -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)

40
gulpfile.js

@ -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']));

14
index.js

@ -1,2 +1,12 @@
require = require('@std/esm')(module); // eslint-disable-line no-global-assign /* eslint-disable */
module.exports = require('./wetty.mjs').default; 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();

15
lib/command.mjs

@ -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;
}

144
lib/emitter.mjs

@ -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();

92
lib/index.mjs

@ -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;
}
}
}

15
lib/logger.mjs

@ -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;

37
lib/server.mjs

@ -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' },
);
}

11
lib/ssl.mjs

@ -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 }));

65
lib/term.mjs

@ -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();
});
});
}
}

88
package.json

@ -1,7 +1,8 @@
{ {
"name": "wetty.js", "name": "wetty.js",
"version": "0.5.1", "version": "1.0.0",
"description": "Wetty = Web + tty. Terminal access in browser over http/https ", "description":
"WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/butlerx/wetty", "homepage": "https://github.com/butlerx/wetty",
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,58 +16,61 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "build": "webpack",
"build": "gulp", "start": "node .",
"start": "node bin", "dev":
"commit": "git add public", "NODE_ENV=development concurrently --kill-others --success first \"webpack --watch\" \"nodemon .\"",
"fix": "eslint . --fix" "prepublishOnly": "yarn build",
"precommit": "lint-staged"
},
"lint-staged": {
"*.{js,mjs}": ["eslint --fix", "git add"],
"*.{json,scss,md}": ["prettier --write", "git add"]
}, },
"bin": { "bin": {
"wetty": "./bin/index.js" "wetty": "./index.js"
}, },
"pre-commit": [ "nodemonConfig": {
"fix", "ignore": ["dist/*", "src/*", "*.json"]
"commit"
],
"preferGlobal": "true",
"@std/esm": {
"cjs": "true"
}, },
"preferGlobal": "true",
"dependencies": { "dependencies": {
"@std/esm": "^0.12.1", "@std/esm": "^0.12.1",
"compression": "^1.7.1",
"express": "^4.15.3", "express": "^4.15.3",
"fs-extra": "^4.0.1", "fs-extra": "^4.0.1",
"helmet": "^3.9.0",
"jsdoc-to-markdown": "^4.0.1",
"lodash": "^4.17.4",
"node-pty": "^0.7.4",
"optimist": "^0.6", "optimist": "^0.6",
"pre-commit": "^1.2.2",
"pty.js": "^0.3.1",
"serve-favicon": "^2.4.3", "serve-favicon": "^2.4.3",
"socket.io": "^1.3.7" "socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
"winston": "^3.0.0-rc1",
"xterm": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "6.24.1", "babel-core": "^6.26.0",
"babel-core": "6.24.1", "babel-loader": "^7.1.2",
"babel-eslint": "7.2.3", "babel-plugin-lodash": "^3.3.2",
"babel-plugin-add-module-exports": "0.2.1", "babel-preset-env": "^1.6.1",
"babel-plugin-es6-promise": "1.1.1", "concurrently": "^3.5.1",
"babel-plugin-syntax-async-functions": "6.13.0", "css-loader": "^0.28.8",
"babel-plugin-transform-async-to-generator": "6.24.1", "eslint": "^4.18.0",
"babel-plugin-transform-object-assign": "6.22.0", "eslint-config-airbnb-base": "^12.1.0",
"babel-preset-es2015": "6.24.1", "eslint-config-prettier": "^2.9.0",
"del": "^3.0.0",
"es6-promise": "^4.1.1",
"eslint": "3.19.0",
"eslint-config-airbnb": "^15.1.0",
"eslint-config-standard": "10.2.1",
"eslint-plugin-import": "^2.7.0", "eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^5.1.1", "eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-node": "^5.1.1", "extract-text-webpack-plugin": "^3.0.2",
"eslint-plugin-promise": "^3.5.0", "husky": "^0.14.3",
"eslint-plugin-react": "^7.1.0", "lint-staged": "^6.1.1",
"eslint-plugin-standard": "^3.0.1", "node-sass": "^4.7.2",
"gulp": "^3.9.1", "nodemon": "^1.14.10",
"gulp-babel": "^6.1.2", "prettier": "^1.10.2",
"gulp-concat": "^2.6.1", "sass-loader": "^6.0.6",
"gulp-minify": "^1.0.0", "style-loader": "^0.19.1",
"gulp-shell": "^0.6.3" "uglifyjs-webpack-plugin": "^1.1.6",
"webpack": "^3.10.0"
} }
} }

20
public/index.html

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

41
public/wetty/index.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>

1
public/wetty/wetty.min.js

File diff suppressed because one or more lines are too long

37
src/.eslintrc.js

@ -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' },
},
],
},
};

39
src/fit.js

@ -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);
};
}

18392
src/hterm_all.js

File diff suppressed because it is too large

113
src/wetty.js

@ -1,82 +1,69 @@
const socket = io(location.origin, { path: '/wetty/socket.io' }); import { Terminal } from 'xterm';
let term; import { isUndefined } from 'lodash';
let buf = ''; import io from 'socket.io-client';
import * as fit from './fit';
import './wetty.scss';
class Wetty { Terminal.applyAddon(fit);
constructor(argv) { const socket = io(window.location.origin, { path: '/wetty/socket.io' });
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', () => { socket.on('connect', () => {
const term = new Terminal();
term.open(document.getElementById('terminal'), { focus: true });
term.setOption('fontSize', 14);
document.getElementById('overlay').style.display = 'none'; document.getElementById('overlay').style.display = 'none';
window.addEventListener('beforeunload', handler, false); 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.scrollPort_.screen_.setAttribute('contenteditable', 'false');
*/
term.runCommandClass(Wetty, document.location.hash.substr(1)); term.attachCustomKeyEventHandler(e => {
socket.emit('resize', { // Ctrl + Shift + C
col: term.screenSize.width, if (e.ctrlKey && e.shiftKey && e.keyCode === 67) {
row: term.screenSize.height, e.preventDefault();
}); document.execCommand('copy');
return false;
if (buf && buf !== '') {
term.io.writeUTF8(buf);
buf = '';
} }
return true;
}); });
});
socket.on('output', data => { function resize() {
if (!term) { term.fit();
buf += data; socket.emit('resize', { cols: term.cols, rows: term.rows });
return;
} }
term.io.writeUTF8(data); window.onresize = resize;
}); resize();
socket.on('logout', () => { function kill(data) {
document.getElementById('overlay').style.display = 'block'; disconnect(data);
window.removeEventListener('beforeunload', handler, false); }
term.on('data', data => {
socket.emit('input', data);
});
term.on('resize', size => {
socket.emit('resize', size);
});
socket
.on('data', data => {
term.write(data);
})
.on('login', () => {
term.writeln('');
resize();
})
.on('logout', kill)
.on('disconnect', kill)
.on('error', err => {
if (err) disconnect(err);
});
}); });
socket.on('disconnect', () => { function disconnect(reason) {
document.getElementById('overlay').style.display = 'block'; document.getElementById('overlay').style.display = 'block';
if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason;
window.removeEventListener('beforeunload', handler, false); window.removeEventListener('beforeunload', handler, false);
}); }
function handler(e) { function handler(e) {
e.returnValue = 'Are you sure?'; e.returnValue = 'Are you sure?';

47
src/wetty.scss

@ -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%;
}
}

74
webpack.config.js

@ -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,
},
};

82
wetty.mjs

@ -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;
}

5136
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save