Browse Source

Merge pull request #48 from butlerx/feature/types

replace esm with typescript
pull/126/head
Cian Butler 6 years ago
committed by GitHub
parent
commit
08cc4da170
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .babelrc
  2. 2
      .eslintignore
  3. 32
      .eslintrc.js
  4. 3
      .gitignore
  5. 2
      .prettierrc.js
  6. 15
      Dockerfile
  7. 3
      Dockerfile-ssh
  8. 104
      README.md
  9. 20
      docker-compose.yml
  10. 95
      index.js
  11. 30
      lib/command.mjs
  12. 149
      lib/emitter.mjs
  13. 100
      lib/index.mjs
  14. 46
      lib/server.mjs
  15. 11
      lib/ssl.mjs
  16. 124
      package.json
  17. 20
      public/index.html
  18. 0
      src/client/favicon.ico
  19. 31
      src/client/index.ts
  20. 0
      src/client/wetty.scss
  21. 39
      src/fit.js
  22. 6
      src/server/buffer.ts
  23. 94
      src/server/command.ts
  24. 3
      src/server/emitter.ts
  25. 80
      src/server/index.ts
  26. 18
      src/server/interfaces.ts
  27. 24
      src/server/logger.ts
  28. 84
      src/server/server.ts
  29. 11
      src/server/ssl.ts
  30. 25
      src/server/term.ts
  31. 131
      src/server/wetty.ts
  32. 22
      tsconfig.json
  33. 134
      webpack.config.babel.js
  34. 74
      webpack.config.js
  35. 5936
      yarn.lock

10
.babelrc

@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-typescript",
["@babel/env"]
],
"compact": true,
"plugins": [
"lodash"
]
}

2
.eslintignore

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

32
.eslintrc.js

@ -1,16 +1,32 @@
module.exports = { module.exports = {
parser: 'eslint-plugin-typescript/parser',
plugins: ['typescript', 'prettier'],
env: { env: {
es6: true, es6: true,
node: true, node: true,
browser: true browser: true,
}, },
root: true, root: true,
extends: ["airbnb-base", "plugin:prettier/recommended"], extends: [
'airbnb-base',
'plugin:typescript/recommended',
'plugin:prettier/recommended',
],
rules: { rules: {
"linebreak-style": ["error", "unix"], 'typescript/indent': 'off',
"arrow-parens": ["error", "as-needed"], 'linebreak-style': ['error', 'unix'],
"no-param-reassign": ["error", { props: false }], 'arrow-parens': ['error', 'as-needed'],
"func-style": ["error", "declaration", { allowArrowFunctions: true }], 'no-param-reassign': ['error', { props: false }],
"no-use-before-define": ["error", { functions: false }] 'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
} 'no-use-before-define': ['error', { functions: false }],
'typescript/no-use-before-define': ['error', { functions: false }],
},
settings: {
'import/resolver': {
'typescript-eslint-parser': ['.ts', '.tsx'],
node: {
extensions: ['.ts', '.js'],
},
},
},
}; };

3
.gitignore

@ -13,6 +13,7 @@ logs
results results
npm-debug.log npm-debug.log
node_modules/* node_modules
.esm-cache .esm-cache
dist dist
.idea

2
.prettierrc.js

@ -4,7 +4,7 @@ module.exports = {
proseWrap: 'always', proseWrap: 'always',
overrides: [ overrides: [
{ {
files: ['*.js', '*.mjs'], files: ['*.js', '*.ts'],
options: { options: {
printWidth: 80, printWidth: 80,
}, },

15
Dockerfile

@ -8,12 +8,15 @@ RUN yarn && \
FROM node:boron-alpine FROM node:boron-alpine
LABEL maintainer="butlerx@notthe.cloud" LABEL maintainer="butlerx@notthe.cloud"
WORKDIR /app WORKDIR /usr/src/app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apk add -U openssh && \ RUN apk add -U openssh-client sshpass
adduser -D -h /home/term -s /bin/sh term && \
echo "term:term" | chpasswd
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 ssh-keyscan -H wetty-ssh >> ~/.ssh/known_hosts
ENTRYPOINT [ "/usr/local/bin/yarn", "start" ] ENTRYPOINT [ "node", "." ]

3
Dockerfile-ssh

@ -0,0 +1,3 @@
FROM sickp/alpine-sshd:latest
RUN adduser -D -h /home/term -s /bin/sh term && \
( echo "term:term" | chpasswd )

104
README.md

@ -9,23 +9,29 @@ websockets rather then Ajax and hence better response time.
![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
maintainable.
## Install ## Install
WeTTy can be installed from source or from npm. To install from source run: WeTTy can be installed from source or from npm.
To install from source run:
```bash ```bash
$ git clone https://github.com/butlerx/wetty $ git clone https://github.com/krishnasrinivas/wetty.git
$ cd wetty $ cd wetty
$ yarn $ yarn
$ yarn build $ yarn build
``` ```
or install it globally with yarn, `yarn -g add wetty.js`, or npm, To install it globally from npm use yarn or npm:
`npm i -g wetty.js`
- yarn, `yarn -g add wetty.js`
- npm, `npm i -g wetty.js`
For auto-login feature you'll need sshpass installed(NOT required for rest of
the program".
- `apt-get install sshpass` (debian eg. Ubuntu)
- `yum install sshpass` (red hat flavours eg. CentOs)
## Running WeTTy ## Running WeTTy
@ -36,16 +42,27 @@ see how to use WeTTy from node see the [API Doc](./docs)
$ node index.js $ node index.js
``` ```
Open your browser on `http://yourserver:3000/` and you will prompted to login. Open your browser on `http://yourserver:3000/wetty` and you will prompted to
Or go to `http://yourserver:3000/ssh/<username>` to specify the user before login. Or go to `http://yourserver:3000/wetty/ssh/<username>` to specify the
hand. user before hand.
If you run it as root it will launch `/bin/login` (where you can specify the
user name), else it will launch `ssh` and connect by default to `localhost`.
If instead you wish to connect to a remote host you can specify the `--sshhost`
option, the SSH port using the `--sshport` option and the SSH user using the
`--sshuser` option.
### Flags ### Flags
WeTTy can be run with the `--help` flag to get a full list of flags. WeTTy can be run with the `--help` flag to get a full list of flags.
WeTTy runs on port `3000` by default. You can change the default port by tunning #### Server Port
with the `--port` or `-p` flag.
WeTTy runs on port `3000` by default. You can change the default port by
starting with the `--port` or `-p` flag.
#### SSH Host
If WeTTy is run as root while the host is set as the local machine it will use 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 the `login` binary rather than ssh. If no host is specified it will use
@ -55,55 +72,75 @@ 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 the `--sshhost` flag and pass the IP or DNS address of the host you want to
connect to. connect to.
#### Default User
You can specify the default user used to ssh to a host using the `--sshuser`. 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>`. 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 If this is left blank a user will be prompted to enter their username when they
connect. connect.
#### SSH Port
By default WeTTy will try to ssh to port `22`, if your host uses an alternative By default WeTTy will try to ssh to port `22`, if your host uses an alternative
ssh port this can be specified with the flag `--sshport`. ssh port this can be specified with the flag `--sshport`.
#### WeTTy URL
If you'd prefer an HTTP base prefix other than `/wetty`, you can specify that If you'd prefer an HTTP base prefix other than `/wetty`, you can specify that
with `--base`. Do not set this to `/ssh/${something}`, as this will break with `--base`.
username matching code.
### https **Do not set this to `/ssh/${something}`, as this will break username matching
code.**
Always use https especially with a terminal to your server. You can add https by #### HTTPS
Always use HTTPS especially with a terminal to your server. You can add HTTPS by
either using WeTTy behind a proxy or directly. either using WeTTy behind a proxy or directly.
If you don't have SSL certificates from a CA you can create a self signed To run WeTTy directly with ssl use both the `--sslkey` and `--sslcert` flags and
certificate using this command: pass them the path too your cert and key as follows:
```bash ```bash
$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes node index.js --sslkey key.pem --sslcert cert.pem
``` ```
To run WeTTy directly with ssl use both the `--sslkey` and `--sslcert` flags and If you don't have SSL certificates from a CA you can create a self signed
pass them the path too your cert and key as follows: certificate using this command:
```bash
node index.js --sslkey key.pem --sslcert cert.pem -p 3000
``` ```
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```
### Auto Login:
You can also pass the ssh password as an optional query parameter to auto-login
the user like this (Only while running wetty as a non root account):
`http://yourserver:3000/wetty/ssh/<username>?sshpass=<password>`
This is not a required feature and the security implications for passing the
password in the url will have to be considered by the user
### Behind a Proxy ## Run wetty behind nginx or apache
As said earlier you can use a proxy to add https to WeTTy. As said earlier you can use a proxy to add https to WeTTy.
**Note** that if your proxy is configured for https you should run WeTTy without **Note** that if your proxy is configured for https you should run WeTTy without
SSL SSL
If your proxy uses a base path other than `/wetty`, If your proxy uses a base path other than `/wetty`, specify the path with the
specify the path with the `--base` flag, `--base` flag, or the `BASE` environment variable.
or the `BASE` environment variable.
#### Nginx #### Nginx
For a more detailed look see the [nginx.conf](./bin/nginx.template) used for
testing
Put the following configuration in nginx's conf: Put the following configuration in nginx's conf:
```nginx ```nginx
location ^~ /wetty { location /wetty {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000/wetty;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
@ -116,9 +153,6 @@ location ^~ /wetty {
} }
``` ```
For a more detailed look see the [nginx.conf](./bin/nginx.template) used for
testing
#### Apache #### Apache
Put the following configuration in apache's conf: Put the following configuration in apache's conf:
@ -155,9 +189,9 @@ The default username is `term` and the password is `term`, if you did not modify
In the docker version all flags can be accessed as environment variables such as In the docker version all flags can be accessed as environment variables such as
`SSHHOST` or `SSHPORT`. `SSHHOST` or `SSHPORT`.
## Run WeTTy as a service daemon If you dont want to build the image yourself just remove the line `build; .`
Install WeTTy globally with global option: ## Run WeTTy as a service daemon
### init.d ### init.d

20
docker-compose.yml

@ -1,20 +1,20 @@
version: "3" ---
version: "3.5"
services: services:
wetty: wetty:
build: .
image: butlerx/wetty image: butlerx/wetty
container_name: wetty container_name: wetty
tty: true tty: true
restart: always
working_dir: /app working_dir: /app
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
SSHHOST: 'localhost' SSHHOST: 'wetty-ssh'
SSHPORT: 22 SSHPORT: 22
NODE_ENV: 'development' NODE_ENV: 'development'
command: yarn start --sshhost redbrick.dcu.ie command: yarn start --sshhost redbrick.dcu.ie
volumes:
- ./lib:/app/lib
web: web:
image: nginx image: nginx
volumes: volumes:
@ -27,3 +27,13 @@ services:
- WETTY_HOST=wetty - WETTY_HOST=wetty
- WETTY_PORT=3000 - WETTY_PORT=3000
command: /bin/bash -c "envsubst '$${NGINX_DOMAIN},$${NGINX_PORT},$${WETTY_HOST},$${WETTY_PORT}' < /etc/nginx/conf.d/wetty.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" command: /bin/bash -c "envsubst '$${NGINX_DOMAIN},$${NGINX_PORT},$${WETTY_HOST},$${WETTY_PORT}' < /etc/nginx/conf.d/wetty.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
wetty-ssh:
build:
context: .
dockerfile: Dockerfile-ssh
container_name: 'wetty-ssh'
networks:
default:
name: wetty

95
index.js

@ -1,12 +1,93 @@
/* eslint-disable */ /* eslint-disable typescript/no-var-requires */
require = require('@std/esm')(module, {
cjs: 'true', const yargs = require('yargs');
esm: 'js', const wetty = require('./dist').default;
});
const wetty = require('./lib/index.mjs').default;
module.exports = wetty.wetty; module.exports = wetty.wetty;
/** /**
* Check if being run by cli or require * Check if being run by cli or require
*/ */
if (require.main === module) wetty.init(); if (require.main === module) {
wetty.init(
yargs
.options({
sslkey: {
demand: false,
type: 'string',
description: 'path to SSL key',
},
sslcert: {
demand: false,
type: 'string',
description: 'path to SSL certificate',
},
sshhost: {
demand: false,
description: 'ssh server host',
type: 'string',
default: process.env.SSHHOST || 'localhost',
},
sshport: {
demand: false,
description: 'ssh server port',
type: 'number',
default: parseInt(process.env.SSHPORT, 10) || 22,
},
sshuser: {
demand: false,
description: 'ssh user',
type: 'string',
default: process.env.SSHUSER || '',
},
sshauth: {
demand: false,
description:
'defaults to "password", you can use "publickey,password" instead',
type: 'string',
default: process.env.SSHAUTH || 'password',
},
sshpass: {
demand: false,
description: 'ssh password',
type: 'string',
default: process.env.SSHPASS || undefined,
},
sshkey: {
demand: false,
description:
'path to an optional client private key (connection will be password-less and insecure!)',
type: 'string',
default: process.env.SSHKEY || undefined,
},
base: {
demand: false,
alias: 'b',
description: 'base path to wetty',
type: 'string',
default: process.env.BASE || '/wetty/',
},
port: {
demand: false,
alias: 'p',
description: 'wetty listen port',
type: 'number',
default: parseInt(process.env.PORT, 10) || 3000,
},
command: {
demand: false,
alias: 'c',
description: 'command to run in shell',
type: 'string',
default: process.env.COMMAND || 'login',
},
help: {
demand: false,
alias: 'h',
type: 'boolean',
description: 'Print help message',
},
})
.boolean('allow_discovery').argv
);
}

30
lib/command.mjs

@ -1,30 +0,0 @@
const localhost = host =>
process.getuid() === 0 &&
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1');
export default (
{ request: { headers }, client: { conn } },
{ user, host, port, auth }
) => ({
args: localhost(host)
? ['login', '-h', conn.remoteAddress.split(':')[3]]
: [
'ssh',
address(headers, user, host),
'-p',
port,
'-o',
`PreferredAuthentications=${auth}`,
],
user:
localhost(host) ||
user !== '' ||
user.includes('@') ||
address(headers, user, host).includes('@'),
});
function address(headers, user, host) {
const match = headers.referer.match('.+/ssh/([^/]+)$');
const fallback = user ? `${user}@${host}` : host;
return match ? `${match[1]}@${host}` : fallback;
}

149
lib/emitter.mjs

@ -1,149 +0,0 @@
/**
* Create WeTTY server
* @module WeTTy
*/
import EventEmitter from 'events';
import server from './server.mjs';
import command from './command.mjs';
import term from './term.mjs';
import loadSSL from './ssl.mjs';
class WeTTy extends EventEmitter {
/**
* Starts WeTTy Server
* @name start
* @async
* @param {Object} [ssh] SSH settings
* @param {string} [ssh.user=''] default user for ssh
* @param {string} [ssh.host=localhost] machine to ssh too
* @param {string} [ssh.auth=password] authtype to use
* @param {number} [ssh.port=22] port to connect to over ssh
* @param {number} [basePath=/wetty/] base part of URL
* @param {number} [serverPort=3000] Port to run server on
* @param {Object} [ssl] SSL settings
* @param {?string} [ssl.key] Path to ssl key
* @param {?string} [ssl.cert] Path to ssl cert
* @return {Promise} Promise resolves once server is running
*/
start(
{ user = '', host = 'localhost', auth = 'password', port = 22 },
basePath = '/wetty/',
serverPort = 3000,
{ key, cert }
) {
return loadSSL(key, cert).then(ssl => {
const io = server(basePath, serverPort, ssl);
/**
* Wetty server connected too
* @fires WeTTy#connnection
*/
io.on('connection', socket => {
/**
* @event wetty#connection
* @name connection
* @type {object}
* @property {string} msg Message for logs
* @property {Date} date date and time of connection
*/
this.emit('connection', {
msg: `Connection accepted.`,
date: new Date(),
});
const { args, user: sshUser } = command(socket, {
user,
host,
auth,
port,
});
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`);
if (sshUser) {
term.spawn(socket, args);
} else {
term
.login(socket)
.then(username => {
this.emit('debug', `username: ${username.trim()}`);
args[1] = `${username.trim()}@${args[1]}`;
this.emit('debug', `cmd : ${args.join(' ')}`);
return term.spawn(socket, args);
})
.catch(() => this.disconnected());
}
});
});
}
/**
* terminal spawned
*
* @fires module:WeTTy#spawn
*/
spawned(pid, address) {
/**
* Terminal process spawned
* @event WeTTy#spawn
* @name spawn
* @type {object}
* @property {string} msg Message containing pid info and status
* @property {number} pid Pid of the terminal
* @property {string} address address of connecting user
*/
this.emit('spawn', {
msg: `PID=${pid} STARTED on behalf of ${address}`,
pid,
address,
});
}
/**
* terminal exited
*
* @fires WeTTy#exit
*/
exited(code, pid) {
/**
* Terminal process exits
* @event WeTTy#exit
* @name exit
* @type {object}
* @property {number} code the exit code
* @property {string} msg Message containing pid info and status
*/
this.emit('exit', { code, msg: `PID=${pid} ENDED` });
}
/**
* Disconnect from WeTTY
*
* @fires WeTTy#disconnet
*/
disconnected() {
/**
* @event WeTTY#disconnect
* @name disconnect
*/
this.emit('disconnect');
}
/**
* Wetty server started
* @fires WeTTy#server
*/
server(port, connection) {
/**
* @event WeTTy#server
* @type {object}
* @name server
* @property {string} msg Message for logging
* @property {number} port port sever is on
* @property {string} connection connection type for web traffic
*/
this.emit('server', {
msg: `${connection} on port ${port}`,
port,
connection,
});
}
}
export default new WeTTy();

100
lib/index.mjs

@ -1,100 +0,0 @@
import optimist from 'optimist';
import logger from './logger.mjs';
import wetty from './emitter.mjs';
const opts = optimist
.options({
sslkey: {
demand: false,
description: 'path to SSL key',
},
sslcert: {
demand: false,
description: 'path to SSL certificate',
},
sshhost: {
demand: false,
description: 'ssh server host',
},
sshport: {
demand: false,
description: 'ssh server port',
},
sshuser: {
demand: false,
description: 'ssh user',
},
sshauth: {
demand: false,
description:
'defaults to "password", you can use "publickey,password" instead',
},
base: {
demand: false,
alias: 'b',
description: 'base path to wetty',
},
port: {
demand: false,
alias: 'p',
description: 'wetty listen port',
},
help: {
demand: false,
alias: 'h',
description: 'Print help message',
},
})
.boolean('allow_discovery').argv;
export default class {
static start({
sshuser = process.env.SSHUSER || '',
sshhost = process.env.SSHHOST || 'localhost',
sshauth = process.env.SSHAUTH || 'password',
sshport = process.env.SSHPOST || 22,
base = process.env.BASE || '/wetty/',
port = process.env.PORT || 3000,
sslkey,
sslcert,
}) {
wetty
.on('exit', ({ code, msg }) => {
logger.info(`Exit with code: ${code} ${msg}`);
})
.on('disconnect', () => {
logger.info('disconnect');
})
.on('spawn', ({ msg }) => logger.info(msg))
.on('connection', ({ msg, date }) => logger.info(`${date} ${msg}`))
.on('server', ({ msg }) => logger.info(msg))
.on('debug', msg => logger.debug(msg));
return wetty.start(
{
user: sshuser,
host: sshhost,
auth: sshauth,
port: sshport,
},
base,
port,
{ key: sslkey, cert: sslcert }
);
}
static get wetty() {
return wetty;
}
static init() {
if (!opts.help) {
this.start(opts).catch(err => {
logger.error(err);
process.exitCode = 1;
});
} else {
optimist.showHelp();
process.exitCode = 0;
}
}
}

46
lib/server.mjs

@ -1,46 +0,0 @@
import compression from 'compression';
import express from 'express';
import favicon from 'serve-favicon';
import helmet from 'helmet';
import http from 'http';
import https from 'https';
import path from 'path';
import socket from 'socket.io';
import { isUndefined } from 'lodash';
import morgan from 'morgan';
import logger from './logger.mjs';
import events from './emitter.mjs';
const pubDir = path.join(__dirname, '..', 'public');
const distDir = path.join(__dirname, '..', 'dist');
export default function createServer(base, port, { key, cert }) {
base = base.replace(/\/*$/, "");
events.emit('debug', `key: ${key}, cert: ${cert}, port: ${port}, base: ${base}`);
const app = express();
const html = (req, res) => res.sendFile(path.join(pubDir, 'index.html'));
const css = (req, res) => res.sendFile(path.join(distDir, 'main.css'));
const js = (req, res) => res.sendFile(path.join(distDir, 'main.js'));
app
.use(morgan('combined', { stream: logger.stream }))
.use(helmet())
.use(compression())
.use(favicon(path.join(pubDir, 'favicon.ico')))
.get(`${base}/`, html)
.get(`${base}/main.css`, css)
.get(`${base}/main.js`, js)
.get(`${base}/ssh/main.css`, css)
.get(`${base}/ssh/main.js`, js)
.get(`${base}/ssh/:user`, html)
return socket(
!isUndefined(key) && !isUndefined(cert)
? https.createServer({ key, cert }, app).listen(port, () => {
events.server(port, 'https');
})
: http.createServer(app).listen(port, () => {
events.server(port, 'http');
}),
{ path: `${base}/socket.io` }
);
}

11
lib/ssl.mjs

@ -1,11 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import { isUndefined } from 'lodash';
export default (sslkey, sslcert) =>
isUndefined(sslkey) || isUndefined(sslcert)
? Promise.resolve({})
: Promise.all([
fs.readFile(path.resolve(sslkey)),
fs.readFile(path.resolve(sslcert)),
]).then(([key, cert]) => ({ key, cert }));

124
package.json

@ -2,27 +2,31 @@
"name": "wetty.js", "name": "wetty.js",
"version": "1.0.3", "version": "1.0.3",
"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/krishnasrinivas/wetty",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/butlerx/wetty.git" "url": "git://github.com/krishnasrinivas/wetty.git"
}, },
"author": "Cian Butler <butlerx@notthe.cloud> (https://github.com/butlerx)", "author": "Krishna Srinivas <krishna.srinivas@gmail.com> (https://github.com/krishnasrinivas)",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/butlerx/wetty/issues" "url": "https://github.com/krishnasrinivas/wetty/issues"
}, },
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"lint": "eslint --ext .js,.mjs .", "lint": "eslint --ext .js,.ts .",
"build": "webpack", "build": "babel-node node_modules/.bin/webpack",
"start": "node .", "start": "node .",
"dev": "NODE_ENV=development concurrently --kill-others --success first \"webpack --watch\" \"nodemon .\"", "dev": "NODE_ENV=development concurrently --kill-others --success first \"babel-node node_modules/.bin/webpack --watch\" \"nodemon .\"",
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build"
"precommit": "lint-staged" },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}, },
"lint-staged": { "lint-staged": {
"*.{js,mjs}": [ "*.{js,ts}": [
"eslint --fix", "eslint --fix",
"git add" "git add"
], ],
@ -39,66 +43,88 @@
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [
"dist/*",
"src/*", "src/*",
"*.json" "*.json"
] ]
}, },
"preferGlobal": "true", "preferGlobal": "true",
"dependencies": { "dependencies": {
"@std/esm": "^0.12.1",
"compression": "^1.7.1", "compression": "^1.7.1",
"express": "^4.15.3", "express": "^4.16.4",
"fs-extra": "^4.0.1", "fs-extra": "^4.0.1",
"helmet": "^3.9.0", "helmet": "^3.9.0",
"jsdoc-to-markdown": "^4.0.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"morgan": "^1.9.0", "morgan": "^1.9.1",
"node-pty": "^0.7.4", "node-pty": "^0.7.4",
"optimist": "^0.6", "serve-favicon": "^2.5.0",
"serve-favicon": "^2.4.3", "socket.io": "^2.2.0",
"socket.io": "^2.0.4", "socket.io-client": "^2.2.0",
"socket.io-client": "^2.0.4", "source-map-loader": "^0.2.4",
"winston": "^3.0.0-rc1", "winston": "^3.1.0",
"xterm": "^3.0.1" "xterm": "^3.10.0",
"yargs": "^12.0.5"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "@babel/core": "^7.2.2",
"babel-loader": "^7.1.2", "@babel/node": "^7.2.2",
"babel-plugin-lodash": "^3.3.2", "@babel/preset-env": "^7.2.3",
"babel-preset-env": "^1.6.1", "@babel/preset-typescript": "^7.1.0",
"@babel/register": "^7.0.0",
"@types/compression": "^0.0.36",
"@types/express": "^4.16.0",
"@types/fs-extra": "^5.0.4",
"@types/helmet": "^0.0.42",
"@types/lodash": "^4.14.119",
"@types/morgan": "^1.7.35",
"@types/node": "^10.12.18",
"@types/serve-favicon": "^2.2.30",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/webpack-env": "^1.13.6",
"@types/yargs": "^12.0.5",
"babel-loader": "^8.0.5",
"babel-plugin-lodash": "^3.3.4",
"concurrently": "^3.5.1", "concurrently": "^3.5.1",
"css-loader": "^0.28.8", "css-loader": "^2.1.0",
"eslint": "^4.18.0", "eslint": "^5.12.0",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^3.3.0",
"eslint-plugin-import": "^2.7.0", "eslint-plugin-import": "^2.14.0",
"eslint-plugin-prettier": "^2.6.0", "eslint-plugin-prettier": "^3.0.1",
"extract-text-webpack-plugin": "^3.0.2", "eslint-plugin-typescript": "^1.0.0-rc.1",
"husky": "^0.14.3", "file-loader": "^3.0.1",
"husky": "^1.3.1",
"lint-staged": "^6.1.1", "lint-staged": "^6.1.1",
"node-sass": "^4.7.2", "mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0",
"nodemon": "^1.14.10", "nodemon": "^1.14.10",
"prettier": "^1.10.2", "prettier": "^1.15.3",
"sass-loader": "^6.0.6", "sass-loader": "^7.1.0",
"style-loader": "^0.19.1", "style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^1.1.6", "typescript": "~3.1.1",
"webpack": "^3.10.0" "webpack": "^4.28.3",
"webpack-cli": "^3.2.0",
"webpack-node-externals": "^1.7.2"
}, },
"contributors": [ "contributors": [
"Krishna Srinivas <krishna.srinivas@gmail.com>",
"Boyan Rabchev <boyan@rabchev.com>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Strubbl <github@linux4tw.de>",
"Jarrett Gilliam <jarrettgilliam@gmail.com>",
"Nathan LeClaire <nathan.leclaire@docker.com>",
"mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>",
"Andreas Kloeckner <inform@tiker.net>", "Andreas Kloeckner <inform@tiker.net>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Boyan Rabchev <TELERIK\\rabchev@rabchevlnx.telerik.com>",
"Boyan Rabchev <boyan@rabchev.com>",
"Cian Butler <butlerx@notthe.cloud>",
"Farhan Khan <khanzf@gmail.com>",
"Imuli <i@imu.li>", "Imuli <i@imu.li>",
"James Turnbull <james@lovedthanlost.net>", "James Turnbull <james@lovedthanlost.net>",
"Jarrett Gilliam <jarrettgilliam@gmail.com>",
"Kasper Holbek Jensen <kholbekj@gmail.com>",
"Krishna Srinivas <krishna@minio.io>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"Nathan LeClaire <nathan.leclaire@docker.com>",
"Neale Pickett <neale@woozle.org>", "Neale Pickett <neale@woozle.org>",
"Robert <robert@n5qm.com>" "Robert <robert@n5qm.com>",
"Strubbl <github@linux4tw.de>",
"koushikmln <mln02koushik@gmail.com>",
"mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>"
] ]
} }

20
public/index.html

@ -1,20 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>WeTTy - The Web Terminal Emulator</title>
<link rel="stylesheet" href="main.css" />
</head>
<body>
<div id="overlay">
<div class="error">
<div id="msg"></div>
<input type="button" onclick="location.reload();" value="reconnect" />
</div>
</div>
<div id="terminal"></div>
<script src="main.js"></script>
</body>
</html>

0
public/favicon.ico → src/client/favicon.ico

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

31
src/index.js → src/client/index.ts

@ -1,17 +1,20 @@
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import io from 'socket.io-client'; import * as io from 'socket.io-client';
import * as fit from './fit'; import { fit } from 'xterm/lib/addons/fit/fit';
import './wetty.scss'; import './wetty.scss';
import './favicon.ico';
Terminal.applyAddon(fit); const userRegex = new RegExp('ssh/[^/]+$');
var userRegex = new RegExp("ssh/\[^/]+$"); const trim = (str: string): string => str.replace(/\/*$/, '');
var socketPath = window.location.pathname.replace(userRegex, ""); const socketBase = trim(window.location.pathname).replace(userRegex, '');
var socket = io(window.location.origin, { path: socketPath + "socket.io" }); const socket = io(window.location.origin, {
path: `${trim(socketBase)}/socket.io`,
});
socket.on('connect', () => { socket.on('connect', () => {
const term = new Terminal(); const term = new Terminal();
term.open(document.getElementById('terminal'), { focus: true }); term.open(document.getElementById('terminal'));
term.setOption('fontSize', 14); 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);
@ -29,15 +32,15 @@ socket.on('connect', () => {
return true; return true;
}); });
function resize() { function resize(): void {
term.fit(); fit(term);
socket.emit('resize', { cols: term.cols, rows: term.rows }); socket.emit('resize', { cols: term.cols, rows: term.rows });
} }
window.onresize = resize; window.onresize = resize;
resize(); resize();
term.focus(); term.focus();
function kill(data) { function kill(data: string): void {
disconnect(data); disconnect(data);
} }
@ -48,7 +51,7 @@ socket.on('connect', () => {
socket.emit('resize', size); socket.emit('resize', size);
}); });
socket socket
.on('data', data => { .on('data', (data: string) => {
term.write(data); term.write(data);
}) })
.on('login', () => { .on('login', () => {
@ -57,18 +60,18 @@ socket.on('connect', () => {
}) })
.on('logout', kill) .on('logout', kill)
.on('disconnect', kill) .on('disconnect', kill)
.on('error', err => { .on('error', (err: string | null) => {
if (err) disconnect(err); if (err) disconnect(err);
}); });
}); });
function disconnect(reason) { function disconnect(reason: string): void {
document.getElementById('overlay').style.display = 'block'; document.getElementById('overlay').style.display = 'block';
if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason; if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason;
window.removeEventListener('beforeunload', handler, false); window.removeEventListener('beforeunload', handler, false);
} }
function handler(e) { function handler(e: { returnValue: string }): string {
e.returnValue = 'Are you sure?'; e.returnValue = 'Are you sure?';
return e.returnValue; return e.returnValue;
} }

0
src/wetty.scss → src/client/wetty.scss

39
src/fit.js

@ -1,39 +0,0 @@
import { isUndefined } from 'lodash';
export function proposeGeometry({ element, renderer }) {
if (!element.parentElement) return null;
const parentElementStyle = window.getComputedStyle(element.parentElement);
return {
cols: Math.floor(
Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10)) /
renderer.dimensions.actualCellWidth,
),
rows: Math.floor(
parseInt(parentElementStyle.getPropertyValue('height'), 10) /
renderer.dimensions.actualCellHeight,
),
};
}
export function fit(term) {
const { rows, cols } = proposeGeometry(term);
if (!isUndefined(rows) && !isUndefined(cols)) {
// Force a full render
if (term.rows !== rows || term.cols !== cols) {
term.renderer.clear();
term.resize(cols, rows);
}
}
}
export function apply({ prototype }) {
prototype.proposeGeometry = function proProposeGeometry() {
return proposeGeometry(this);
};
prototype.fit = function proFit() {
return fit(this);
};
}

6
lib/buffer.mjs → src/server/buffer.ts

@ -1,9 +1,9 @@
import rl from 'readline'; import { createInterface } from 'readline';
ask('Enter your username'); ask('Enter your username');
export default function ask(question) { export default function ask(question: string): Promise<string> {
const r = rl.createInterface({ const r = createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}); });

94
src/server/command.ts

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

3
src/server/emitter.ts

@ -0,0 +1,3 @@
import WeTTy from './wetty';
export default new WeTTy();

80
src/server/index.ts

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

18
src/server/interfaces.ts

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

24
lib/logger.mjs → src/server/logger.ts

@ -3,14 +3,18 @@ import { createLogger, format, transports } from 'winston';
const { combine, timestamp, label, printf, colorize } = format; const { combine, timestamp, label, printf, colorize } = format;
const logger = createLogger({ const logger = createLogger({
format: combine( format:
colorize({ all: true }), process.env.NODE_ENV === 'development'
label({ label: 'Wetty' }), ? combine(
timestamp(), colorize({ all: true }),
printf( label({ label: 'Wetty' }),
info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}` timestamp(),
) printf(
), info =>
`${info.timestamp} [${info.label}] ${info.level}: ${info.message}`
)
)
: format.json(),
transports: [ transports: [
new transports.Console({ new transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
@ -20,8 +24,8 @@ const logger = createLogger({
}); });
logger.stream = { logger.stream = {
write(message) { write(message: string): void {
logger.verbose(message); logger.info(message);
}, },
}; };

84
src/server/server.ts

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

11
src/server/ssl.ts

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

25
lib/term.mjs → src/server/term.ts

@ -1,6 +1,6 @@
import { spawn } from 'node-pty'; import { spawn } from 'node-pty';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import events from './emitter.mjs'; import events from './emitter';
const xterm = { const xterm = {
name: 'xterm-256color', name: 'xterm-256color',
@ -11,15 +11,15 @@ const xterm = {
}; };
export default class Term { export default class Term {
static spawn(socket, args) { public static spawn(socket: SocketIO.Socket, args: string[]): void {
const term = spawn('/usr/bin/env', args, xterm); const term = spawn('/usr/bin/env', args, xterm);
const address = args[0] === 'ssh' ? args[1] : 'localhost'; const address = args[0] === 'ssh' ? args[1] : 'localhost';
events.spawned(term.pid, address); events.spawned(term.pid, address);
socket.emit('login'); socket.emit('login');
term.on('exit', code => { term.on('exit', code => {
events.exited(code, term.pid); events.exited(code, term.pid);
socket.emit('logout');
socket socket
.emit('logout')
.removeAllListeners('disconnect') .removeAllListeners('disconnect')
.removeAllListeners('resize') .removeAllListeners('resize')
.removeAllListeners('input'); .removeAllListeners('input');
@ -35,18 +35,14 @@ export default class Term {
if (!isUndefined(term)) term.write(input); if (!isUndefined(term)) term.write(input);
}) })
.on('disconnect', () => { .on('disconnect', () => {
term.end(); const { pid } = term;
term.destroy(); term.kill();
events.exited(); events.exited(0, pid);
}); });
} }
static login(socket) { public static login(socket: SocketIO.Socket): Promise<string> {
const term = spawn( const term = spawn('/usr/bin/env', ['node', './dist/buffer.js'], xterm);
'/usr/bin/env',
['node', '-r', '@std/esm', './lib/buffer.mjs'],
xterm
);
let buf = ''; let buf = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
term.on('exit', () => { term.on('exit', () => {
@ -56,13 +52,12 @@ export default class Term {
socket.emit('data', data); socket.emit('data', data);
}); });
socket socket
.on('input', input => { .on('input', (input: string) => {
term.write(input); term.write(input);
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input; buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input;
}) })
.on('disconnect', () => { .on('disconnect', () => {
term.end(); term.kill();
term.destroy();
reject(); reject();
}); });
}); });

131
src/server/wetty.ts

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

22
tsconfig.json

@ -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"
]
}

134
webpack.config.babel.js

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

74
webpack.config.js

@ -1,74 +0,0 @@
const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractSass = new ExtractTextPlugin({
filename: '[name].css',
disable: process.env.NODE_ENV === 'development',
});
const loader = new webpack.ProvidePlugin({
fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch',
});
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
plugins: ['lodash'],
presets: [
[
'env',
{
targets: {
browsers: ['last 2 versions', 'safari >= 7'],
},
},
],
],
},
},
{
test: /\.scss$/,
use: extractSass.extract({
use: [
{
loader: 'css-loader',
options: { minimize: true },
},
{
loader: 'sass-loader',
},
],
fallback: 'style-loader',
}),
},
],
},
plugins:
process.env.NODE_ENV !== 'development'
? [
loader,
extractSass,
new UglifyJSPlugin({
parallel: true,
uglifyOptions: {
ecma: 8,
},
}),
]
: [loader, extractSass],
stats: {
colors: true,
},
};

5936
yarn.lock

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