Browse Source

replace esm with typescript

pull/126/head
butlerx 6 years ago
parent
commit
d145ad9d42
No known key found for this signature in database GPG Key ID: B37CA765BAA89170
  1. 9
      .babelrc
  2. 32
      .eslintrc.js
  3. 74
      index.js
  4. 30
      lib/command.mjs
  5. 100
      lib/index.mjs
  6. 46
      lib/server.mjs
  7. 11
      lib/ssl.mjs
  8. 91
      package.json
  9. 20
      public/index.html
  10. 0
      src/client/favicon.ico
  11. 31
      src/client/index.ts
  12. 0
      src/client/wetty.scss
  13. 39
      src/fit.js
  14. 6
      src/server/buffer.ts
  15. 33
      src/server/command.ts
  16. 3
      src/server/emitter.ts
  17. 71
      src/server/index.ts
  18. 16
      src/server/interfaces.ts
  19. 14
      src/server/logger.ts
  20. 84
      src/server/server.ts
  21. 11
      src/server/ssl.ts
  22. 23
      src/server/term.ts
  23. 62
      src/server/wetty.ts
  24. 22
      tsconfig.json
  25. 134
      webpack.config.babel.js
  26. 74
      webpack.config.js
  27. 5845
      yarn.lock

9
.babelrc

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

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

74
index.js

@ -1,12 +1,72 @@
/* 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.SSHPOST, 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',
},
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,
},
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;
}

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

91
package.json

@ -14,15 +14,19 @@
}, },
"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,51 +43,68 @@
}, },
"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>", "Krishna Srinivas <krishna.srinivas@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,
}); });

33
src/server/command.ts

@ -0,0 +1,33 @@
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');
export default (
{ request: { headers }, client: { conn } }: Socket,
{ user, host, port, auth }: SSH
): { args: string[]; user: boolean } => ({
args: localhost(host)
? ['login', '-h', conn.remoteAddress.split(':')[3]]
: [
'ssh',
address(headers.referer, user, host),
'-p',
`${port}`,
'-o',
`PreferredAuthentications=${auth}`,
],
user:
localhost(host) ||
user !== '' ||
user.includes('@') ||
address(headers.referer, user, host).includes('@'),
});
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();

71
src/server/index.ts

@ -0,0 +1,71 @@
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;
sslkey?: string;
sslcert?: string;
base: string;
port: number;
}
interface CLI extends Options {
help: boolean;
}
export default class Server {
public static start({
sshuser,
sshhost,
sshauth,
sshport,
base,
port,
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,
},
base,
port,
{ 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;
}
}
}

16
src/server/interfaces.ts

@ -0,0 +1,16 @@
export interface SSH {
user: string;
host: string;
auth: string;
port: number;
}
export interface SSL {
key?: string;
cert?: string;
}
export interface SSLBuffer {
key?: Buffer;
cert?: Buffer;
}

14
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:
process.env.NODE_ENV === 'development'
? combine(
colorize({ all: true }), colorize({ all: true }),
label({ label: 'Wetty' }), label({ label: 'Wetty' }),
timestamp(), timestamp(),
printf( printf(
info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}` 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 };
}

23
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,15 @@ 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.kill();
term.destroy(); term.destroy();
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,12 +53,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(); term.destroy();
reject(); reject();
}); });

62
lib/emitter.mjs → src/server/wetty.ts

@ -2,42 +2,31 @@
* Create WeTTY server * Create WeTTY server
* @module WeTTy * @module WeTTy
*/ */
import EventEmitter from 'events'; import * as EventEmitter from 'events';
import server from './server.mjs'; import server from './server';
import command from './command.mjs'; import command from './command';
import term from './term.mjs'; import term from './term';
import loadSSL from './ssl.mjs'; import loadSSL from './ssl';
import { SSL, SSH, SSLBuffer } from './interfaces';
class WeTTy extends EventEmitter { export default class WeTTy extends EventEmitter {
/** /**
* Starts WeTTy Server * Starts WeTTy Server
* @name start * @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( public start(
{ user = '', host = 'localhost', auth = 'password', port = 22 }, ssh: SSH = { user: '', host: 'localhost', auth: 'password', port: 22 },
basePath = '/wetty/', basePath: string = '/wetty/',
serverPort = 3000, serverPort: number = 3000,
{ key, cert } ssl?: SSL
) { ): Promise<void> {
return loadSSL(key, cert).then(ssl => { return loadSSL(ssl).then((sslBuffer: SSLBuffer) => {
const io = server(basePath, serverPort, ssl); const io = server(basePath, serverPort, sslBuffer);
/** /**
* Wetty server connected too * Wetty server connected too
* @fires WeTTy#connnection * @fires WeTTy#connnection
*/ */
io.on('connection', socket => { io.on('connection', (socket: SocketIO.Socket) => {
/** /**
* @event wetty#connection * @event wetty#connection
* @name connection * @name connection
@ -49,19 +38,14 @@ class WeTTy extends EventEmitter {
msg: `Connection accepted.`, msg: `Connection accepted.`,
date: new Date(), date: new Date(),
}); });
const { args, user: sshUser } = command(socket, { const { args, user: sshUser } = command(socket, ssh);
user,
host,
auth,
port,
});
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`); this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`);
if (sshUser) { if (sshUser) {
term.spawn(socket, args); term.spawn(socket, args);
} else { } else {
term term
.login(socket) .login(socket)
.then(username => { .then((username: string) => {
this.emit('debug', `username: ${username.trim()}`); this.emit('debug', `username: ${username.trim()}`);
args[1] = `${username.trim()}@${args[1]}`; args[1] = `${username.trim()}@${args[1]}`;
this.emit('debug', `cmd : ${args.join(' ')}`); this.emit('debug', `cmd : ${args.join(' ')}`);
@ -78,7 +62,7 @@ class WeTTy extends EventEmitter {
* *
* @fires module:WeTTy#spawn * @fires module:WeTTy#spawn
*/ */
spawned(pid, address) { public spawned(pid: number, address: string): void {
/** /**
* Terminal process spawned * Terminal process spawned
* @event WeTTy#spawn * @event WeTTy#spawn
@ -100,7 +84,7 @@ class WeTTy extends EventEmitter {
* *
* @fires WeTTy#exit * @fires WeTTy#exit
*/ */
exited(code, pid) { public exited(code: number, pid: number): void {
/** /**
* Terminal process exits * Terminal process exits
* @event WeTTy#exit * @event WeTTy#exit
@ -117,7 +101,7 @@ class WeTTy extends EventEmitter {
* *
* @fires WeTTy#disconnet * @fires WeTTy#disconnet
*/ */
disconnected() { private disconnected(): void {
/** /**
* @event WeTTY#disconnect * @event WeTTY#disconnect
* @name disconnect * @name disconnect
@ -129,7 +113,7 @@ class WeTTy extends EventEmitter {
* Wetty server started * Wetty server started
* @fires WeTTy#server * @fires WeTTy#server
*/ */
server(port, connection) { public server(port: number, connection: string): void {
/** /**
* @event WeTTy#server * @event WeTTy#server
* @type {object} * @type {object}
@ -145,5 +129,3 @@ class WeTTy extends EventEmitter {
}); });
} }
} }
export default new WeTTy();

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

5845
yarn.lock

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