From 8b5a7df7129a3c9a11d0ac8fda6794c7af00b6d6 Mon Sep 17 00:00:00 2001 From: butlerx Date: Wed, 5 Aug 2020 10:23:45 +0100 Subject: [PATCH] use snowpack to import client modules to avoid bundling --- .eslintignore | 3 +- .gitignore | 3 +- .prettierrc.json | 7 +- {src/client => assets}/favicon.ico | Bin package.json | 66 +- src/client/{ => client}/copyToClipboard.ts | 0 src/client/{ => client}/disconnect.ts | 6 +- src/client/{ => client}/download.spec.ts | 42 +- src/client/{ => client}/download.ts | 0 src/client/{ => client}/mobile.ts | 2 +- src/client/{ => client}/options.ts | 2 +- src/client/{ => client}/socket.ts | 2 +- src/{client.ts => client/index.ts} | 41 +- src/{ => client}/shared/elements.ts | 0 src/{ => client}/shared/verify.ts | 0 src/client/{wetty.scss => styles.scss} | 69 +- src/main.ts | 137 ++ src/server.ts | 230 +- src/server/command.ts | 16 +- src/server/command/login.ts | 2 +- src/server/command/ssh.ts | 14 +- src/server/command/ssh/parse.ts | 8 - src/server/default.ts | 8 +- src/server/login.ts | 12 +- src/server/shared/xterm.ts | 6 +- src/server/socketServer.ts | 64 +- src/server/socketServer/html.ts | 36 +- src/server/spawn.ts | 14 +- src/server/ssl.ts | 12 +- src/shared/logger.ts | 10 +- tsconfig.json | 30 + tsconfig/cjs.json | 7 - tsconfig/esm.json | 14 - yarn.lock | 2439 +++++++++++--------- 34 files changed, 1756 insertions(+), 1546 deletions(-) rename {src/client => assets}/favicon.ico (100%) rename src/client/{ => client}/copyToClipboard.ts (100%) rename src/client/{ => client}/disconnect.ts (64%) rename src/client/{ => client}/download.spec.ts (93%) rename src/client/{ => client}/download.ts (100%) rename src/client/{ => client}/mobile.ts (89%) rename src/client/{ => client}/options.ts (80%) rename src/client/{ => client}/socket.ts (83%) rename src/{client.ts => client/index.ts} (81%) rename src/{ => client}/shared/elements.ts (100%) rename src/{ => client}/shared/verify.ts (100%) rename src/client/{wetty.scss => styles.scss} (72%) create mode 100644 src/main.ts delete mode 100644 src/server/command/ssh/parse.ts create mode 100644 tsconfig.json delete mode 100644 tsconfig/cjs.json delete mode 100644 tsconfig/esm.json diff --git a/.eslintignore b/.eslintignore index 4a01086..e6bd6c7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ node_modules/ .esm-cache -dist +lib public/ *hterm* +web_modules diff --git a/.gitignore b/.gitignore index 34f0776..0719ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -./lib +web_modules +lib # Created by https://www.toptal.com/developers/gitignore/api/node # Edit at https://www.toptal.com/developers/gitignore?templates=node diff --git a/.prettierrc.json b/.prettierrc.json index 3c5ef98..b9f1619 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,10 +1,13 @@ { "singleQuote": true, - "trailingComma": "es5", + "trailingComma": "all", "proseWrap": "always", "overrides": [ { - "files": ["*.js", "*.ts"], + "files": [ + "*.js", + "*.ts" + ], "options": { "printWidth": 80 } diff --git a/src/client/favicon.ico b/assets/favicon.ico similarity index 100% rename from src/client/favicon.ico rename to assets/favicon.ico diff --git a/package.json b/package.json index aae1c98..b42caae 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,23 @@ "version": "2.0.0", "description": "WeTTY = Web + TTY. Terminal access in browser over http/https", "homepage": "https://github.com/butlerx/wetty", + "license": "MIT", "type": "module", + "main": "./lib/main.js", + "module": "./lib/server.js", + "files": [ + "lib/" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "contributor": "all-contributors", + "dev": "NODE_ENV=development concurrently --kill-others --success first \"tsc -p tsconfig.json \" \"nodemon .\"", + "lint": "eslint --ext .ts,.js .", + "prepublishOnly": "NODE_ENV=production yarn build", + "start": "NODE_ENV=production node .", + "test": "mocha -r babel-register-ts src/**/*.spec.ts", + "postinstall": "snowpack install" + }, "repository": { "type": "git", "url": "git://github.com/butlerx/wetty.git" @@ -13,24 +29,9 @@ "email": "butlerx@notthe.cloud", "url": "cianbutler.ie" }, - "license": "MIT", "bugs": { "url": "https://github.com/butlerx/wetty/issues" }, - "main": "./lib/cjs/server.js", - "module": "./lib/esm/server.js", - "files": [ - "lib/" - ], - "scripts": { - "build": "tsc -p tsconfig/esm.json && tsc -p tsconfig/cjs.json", - "contributor": "all-contributors", - "dev": "NODE_ENV=development concurrently --kill-others --success first \"babel-node node_modules/.bin/webpack --watch\" \"nodemon .\"", - "lint": "eslint --ext .ts,.js .", - "prepublishOnly": "NODE_ENV=production yarn build", - "start": "NODE_ENV=production node .", - "test": "mocha -r babel-register-ts src/**/*.spec.ts" - }, "husky": { "hooks": { "pre-commit": "lint-staged" @@ -38,12 +39,10 @@ }, "lint-staged": { "*.{js,ts}": [ - "eslint --fix", - "git add" + "eslint --fix" ], "*.{json,scss,md}": [ - "prettier --write", - "git add" + "prettier --write" ] }, "engines": { @@ -55,7 +54,21 @@ "*.json" ] }, - "preferGlobal": true, + "snowpack": { + "install": [ + "@fortawesome/fontawesome-svg-core", + "@fortawesome/free-solid-svg-icons", + "file-type", + "lodash", + "socket.io-client", + "toastify-js", + "xterm", + "xterm-addon-fit" + ], + "exclude": [ + "**" + ] + }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/free-solid-svg-icons": "^5.11.2", @@ -65,12 +78,12 @@ "file-type": "^12.3.0", "fs-extra": "^8.1.0", "helmet": "^3.20.1", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "node-pty": "^0.9.0", + "node-sass-middleware": "^0.11.0", "serve-favicon": "^2.5.0", "socket.io": "^2.2.0", "socket.io-client": "^2.2.0", - "source-map-loader": "^0.2.4", "toastify-js": "^1.6.1", "winston": "^3.2.1", "xterm": "^4.8.1", @@ -78,12 +91,6 @@ "yargs": "^14.0.0" }, "devDependencies": { - "@babel/core": "^7.5.5", - "@babel/node": "^7.5.5", - "@babel/plugin-proposal-class-properties": "^7.7.4", - "@babel/preset-env": "^7.5.5", - "@babel/preset-typescript": "^7.3.3", - "@babel/register": "^7.5.5", "@types/chai": "^4.2.5", "@types/compression": "^1.0.1", "@types/express": "^4.17.1", @@ -94,6 +101,7 @@ "@types/mocha": "^5.2.7", "@types/morgan": "^1.7.37", "@types/node": "^12.7.3", + "@types/node-sass-middleware": "^0.0.31", "@types/serve-favicon": "^2.2.31", "@types/sinon": "^7.5.1", "@types/socket.io": "^2.1.2", @@ -111,7 +119,6 @@ "eslint-config-prettier": "^6.11.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.4", - "file-loader": "^4.2.0", "git-authors-cli": "^1.0.27", "husky": "^4.2.5", "jsdom": "^15.2.1", @@ -120,6 +127,7 @@ "nodemon": "^2.0.4", "prettier": "^2.0.5", "sinon": "^7.5.0", + "snowpack": "^2.7.5", "typescript": "^3.9.7" }, "contributors": [ diff --git a/src/client/copyToClipboard.ts b/src/client/client/copyToClipboard.ts similarity index 100% rename from src/client/copyToClipboard.ts rename to src/client/client/copyToClipboard.ts diff --git a/src/client/disconnect.ts b/src/client/client/disconnect.ts similarity index 64% rename from src/client/disconnect.ts rename to src/client/client/disconnect.ts index ae2e72a..8ce3113 100644 --- a/src/client/disconnect.ts +++ b/src/client/client/disconnect.ts @@ -1,6 +1,6 @@ -import { isNull, isUndefined } from 'lodash'; -import { verifyPrompt } from '../shared/verify'; -import { overlay } from '../shared/elements'; +import { isNull, isUndefined } from '../../web_modules/lodash.js'; +import { verifyPrompt } from '../shared/verify.js'; +import { overlay } from '../shared/elements.js'; export function disconnect(reason: string): void { if (isNull(overlay)) return; diff --git a/src/client/download.spec.ts b/src/client/client/download.spec.ts similarity index 93% rename from src/client/download.spec.ts rename to src/client/client/download.spec.ts index 6da22e3..42f66b3 100644 --- a/src/client/download.spec.ts +++ b/src/client/client/download.spec.ts @@ -4,11 +4,8 @@ import { expect } from 'chai'; import 'mocha'; import * as sinon from 'sinon'; -import { JSDOM } from 'jsdom'; import { FileDownloader } from './download'; -const { window } = new JSDOM(`...`); - describe('FileDownloader', () => { const FILE_BEGIN = 'BEGIN'; const FILE_END = 'END'; @@ -25,7 +22,7 @@ describe('FileDownloader', () => { it('should return data before file markers', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect( - fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}`) + fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}`), ).to.equal('DATA AT THE LEFT'); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); @@ -34,7 +31,7 @@ describe('FileDownloader', () => { it('should return data after file markers', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect( - fileDownloader.buffer(`${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`) + fileDownloader.buffer(`${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`), ).to.equal('DATA AT THE RIGHT'); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); @@ -44,17 +41,17 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect( fileDownloader.buffer( - `DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT` - ) + `DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`, + ), ).to.equal('DATA AT THE LEFTDATA AT THE RIGHT'); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); }); it('should return data before a beginning marker found', () => { - const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); + sinon.stub(fileDownloader, 'onCompleteFile'); expect(fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE`)).to.equal( - 'DATA AT THE LEFT' + 'DATA AT THE LEFT', ); }); @@ -62,7 +59,7 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect(fileDownloader.buffer(`${FILE_BEGIN}FI`)).to.equal(''); expect(fileDownloader.buffer(`LE${FILE_END}DATA AT THE RIGHT`)).to.equal( - 'DATA AT THE RIGHT' + 'DATA AT THE RIGHT', ); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); @@ -96,13 +93,13 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( - 'DATA AT THE LEFT' + 'DATA AT THE LEFT', ); expect(fileDownloader.buffer('E')).to.equal(''); expect(fileDownloader.buffer('G')).to.equal(''); expect(fileDownloader.buffer('I')).to.equal(''); expect(fileDownloader.buffer('NFILE' + 'ENDDATA AT THE RIGHT')).to.equal( - 'DATA AT THE RIGHT' + 'DATA AT THE RIGHT', ); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); @@ -113,14 +110,14 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( - 'DATA AT THE LEFT' + 'DATA AT THE LEFT', ); expect(fileDownloader.buffer('E')).to.equal(''); expect(fileDownloader.buffer('G')).to.equal(''); // This isn't part of the file_begin marker and should trigger the partial // file begin marker to be returned with the normal data expect(fileDownloader.buffer('ZDATA AT THE RIGHT')).to.equal( - 'BEGZDATA AT THE RIGHT' + 'BEGZDATA AT THE RIGHT', ); expect(onCompleteFileStub.called).to.be.false; }); @@ -143,14 +140,14 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect(fileDownloader.buffer('DATA AT THE LEFT' + 'BE')).to.equal( - 'DATA AT THE LEFT' + 'DATA AT THE LEFT', ); expect(fileDownloader.buffer('G')).to.equal(''); expect(fileDownloader.buffer('I')).to.equal(''); expect(fileDownloader.buffer('NFILEE')).to.equal(''); expect(fileDownloader.buffer('N')).to.equal(''); expect(fileDownloader.buffer('DDATA AT THE RIGHT')).to.equal( - 'DATA AT THE RIGHT' + 'DATA AT THE RIGHT', ); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); @@ -162,8 +159,13 @@ describe('FileDownloader', () => { expect( fileDownloader.buffer( - 'DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'END' + 'SECOND DATA' + 'BEGIN' - ) + 'DATA AT THE LEFT' + + 'BEGIN' + + 'FILE1' + + 'END' + + 'SECOND DATA' + + 'BEGIN', + ), ).to.equal('DATA AT THE LEFT' + 'SECOND DATA'); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); @@ -180,11 +182,11 @@ describe('FileDownloader', () => { const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); expect( - fileDownloader.buffer('DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'EN') + fileDownloader.buffer('DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'EN'), ).to.equal('DATA AT THE LEFT'); expect(onCompleteFileStub.calledOnce).to.be.false; expect( - fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN') + fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN'), ).to.equal('SECOND DATA'); expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); diff --git a/src/client/download.ts b/src/client/client/download.ts similarity index 100% rename from src/client/download.ts rename to src/client/client/download.ts diff --git a/src/client/mobile.ts b/src/client/client/mobile.ts similarity index 89% rename from src/client/mobile.ts rename to src/client/client/mobile.ts index c9b5528..671092c 100644 --- a/src/client/mobile.ts +++ b/src/client/client/mobile.ts @@ -1,4 +1,4 @@ -import { isNull } from 'lodash'; +import { isNull } from '../../web_modules/lodash.js'; export function mobileKeyboard(): void { const [screen] = document.getElementsByClassName('xterm-screen'); diff --git a/src/client/options.ts b/src/client/client/options.ts similarity index 80% rename from src/client/options.ts rename to src/client/client/options.ts index 276477e..8a6d46f 100644 --- a/src/client/options.ts +++ b/src/client/client/options.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import { isUndefined } from '../../web_modules/lodash.js'; export function loadOptions(): object { const defaultOptions = { fontSize: 14 }; diff --git a/src/client/socket.ts b/src/client/client/socket.ts similarity index 83% rename from src/client/socket.ts rename to src/client/client/socket.ts index 4195c8d..5341c35 100644 --- a/src/client/socket.ts +++ b/src/client/client/socket.ts @@ -1,4 +1,4 @@ -import io from 'socket.io-client'; +import io from '../../web_modules/socket.io-client.js'; const userRegex = new RegExp('ssh/[^/]+$'); export const trim = (str: string): string => str.replace(/\/*$/, ''); diff --git a/src/client.ts b/src/client/index.ts similarity index 81% rename from src/client.ts rename to src/client/index.ts index 919151a..a1e7b41 100644 --- a/src/client.ts +++ b/src/client/index.ts @@ -1,21 +1,22 @@ -import { Terminal } from 'xterm'; -import { isNull } from 'lodash'; -import { FitAddon } from 'xterm-addon-fit'; -import { dom, library } from '@fortawesome/fontawesome-svg-core'; -import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs'; -import Toastify from 'toastify-js'; -import * as fileType from 'file-type'; +import { Terminal } from '../web_modules/xterm.js'; +import { isNull } from '../web_modules/lodash.js'; +import { FitAddon } from '../web_modules/xterm-addon-fit.js'; +import { + dom, + library, +} from '../web_modules/@fortawesome/fontawesome-svg-core.js'; +import { faCogs } from '../web_modules/@fortawesome/free-solid-svg-icons.js'; +import Toastify from '../web_modules/toastify-js.js'; +import fileType from '../web_modules/file-type.js'; -import { FileDownloader } from './client/download'; -import { copySelected, copyShortcut } from './client/copyToClipboard'; -import { disconnect } from './client/disconnect'; -import { loadOptions } from './client/options'; -import { mobileKeyboard } from './client/mobile'; -import { overlay, terminal } from './shared/elements'; -import { socket } from './client/socket'; -import { verifyPrompt } from './shared/verify'; -import './client/wetty.scss'; -import './client/favicon.ico'; +import { FileDownloader } from './client/download.js'; +import { copySelected, copyShortcut } from './client/copyToClipboard.js'; +import { disconnect } from './client/disconnect.js'; +import { loadOptions } from './client/options.js'; +import { mobileKeyboard } from './client/mobile.js'; +import { overlay, terminal } from './shared/elements.js'; +import { socket } from './client/socket.js'; +import { verifyPrompt } from './shared/verify.js'; // Setup for fontawesome library.add(faCogs); @@ -76,7 +77,7 @@ socket.on('connect', () => { () => { if (term.hasSelection()) copySelected(term.getSelection()); }, - false + false, ); window.onresize = resize; @@ -135,10 +136,10 @@ socket.on('connect', () => { }).showToast(); }); - term.onData(data => { + term.onData((data: string) => { socket.emit('input', data); }); - term.onResize(size => { + term.onResize((size: string) => { socket.emit('resize', size); }); socket diff --git a/src/shared/elements.ts b/src/client/shared/elements.ts similarity index 100% rename from src/shared/elements.ts rename to src/client/shared/elements.ts diff --git a/src/shared/verify.ts b/src/client/shared/verify.ts similarity index 100% rename from src/shared/verify.ts rename to src/client/shared/verify.ts diff --git a/src/client/wetty.scss b/src/client/styles.scss similarity index 72% rename from src/client/wetty.scss rename to src/client/styles.scss index 7ef9d68..955488b 100644 --- a/src/client/wetty.scss +++ b/src/client/styles.scss @@ -1,10 +1,11 @@ -@import '~xterm/css/xterm'; +@import '~xterm/dist/xterm'; @import '~toastify-js/src/toastify.css'; $black: #000; $grey: rgba(0, 0, 0, 0.75); $white: #fff; $lgrey: #ccc; +$red: red; html, body { @@ -48,51 +49,55 @@ body { } #options { + height: 16px; position: absolute; - top: 1em; right: 1em; - z-index: 20; - height: 16px; + top: 1em; width: 16px; + z-index: 20; - a.toggler { - display: inline-block; - position: absolute; - right: 1em; - top: 0em; - font-size: 16px; - color: $lgrey; - z-index: 20; + a { + .toggler { + color: $lgrey; + display: inline-block; + font-size: 16px; + position: absolute; + right: 1em; + top: 0; + z-index: 20; - :hover { - color: $white; + :hover { + color: $white; + } } } .editor { background-color: rgba(0, 0, 0, 0.85); - padding: 0.5em; - border-radius: 0.3em; border-color: rgba(255, 255, 255, 0.25); + border-radius: 0.3em; + color: #eee; display: none; - position: relative; + font-size: 24px; height: 100%; - width: 100%; - top: 1em; + padding: 0.5em; + position: relative; right: 2em; - color: #eee; - font-size: 24px; - } - .editor.error { - color: red; + top: 1em; + width: 100%; + .error { + color: $red; + } } } - #options.opened { - height: 50%; - width: 50%; - .editor { - display: flex; + #options { + .opened { + height: 50%; + width: 50%; + .editor { + display: flex; + } } } @@ -102,6 +107,8 @@ body { } } -.xterm .xterm-viewport { - overflow-y: hidden; +.xterm { + .xterm-viewport { + overflow-y: hidden; + } } diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e1b190a --- /dev/null +++ b/src/main.ts @@ -0,0 +1,137 @@ +/** + * Create WeTTY server + * @module WeTTy + */ +import yargs from 'yargs'; +import isUndefined from 'lodash/isUndefined.js'; +import { logger } from './shared/logger.js'; +import { + sshDefault, + serverDefault, + forceSSHDefault, + defaultCommand, +} from './server/default.js'; +import { startServer } from './server.js'; + +const opts = yargs + .option('ssl-key', { + type: 'string', + description: 'path to SSL key', + }) + .option('ssl-cert', { + type: 'string', + description: 'path to SSL certificate', + }) + .option('ssh-host', { + description: 'ssh server host', + type: 'string', + default: sshDefault.host, + }) + .option('ssh-port', { + description: 'ssh server port', + type: 'number', + default: sshDefault.port, + }) + .option('ssh-user', { + description: 'ssh user', + type: 'string', + default: sshDefault.user, + }) + .option('title', { + description: 'window title', + type: 'string', + default: serverDefault.title, + }) + .option('ssh-auth', { + description: + 'defaults to "password", you can use "publickey,password" instead', + type: 'string', + default: sshDefault.auth, + }) + .option('ssh-pass', { + description: 'ssh password', + type: 'string', + default: sshDefault.pass, + }) + .option('ssh-key', { + demand: false, + description: + 'path to an optional client private key (connection will be password-less and insecure!)', + type: 'string', + default: sshDefault.key, + }) + .option('force-ssh', { + description: 'Connecting through ssh even if running as root', + type: 'boolean', + default: forceSSHDefault, + }) + .option('known-hosts', { + description: 'path to known hosts file', + type: 'string', + default: sshDefault.knownHosts, + }) + .option('base', { + alias: 'b', + description: 'base path to wetty', + type: 'string', + default: serverDefault.base, + }) + .option('port', { + alias: 'p', + description: 'wetty listen port', + type: 'number', + default: serverDefault.port, + }) + .option('host', { + description: 'wetty listen host', + default: serverDefault.host, + type: 'string', + }) + .option('command', { + alias: 'c', + description: 'command to run in shell', + type: 'string', + default: defaultCommand, + }) + .option('bypass-helmet', { + description: 'disable helmet from placing security restrictions', + type: 'boolean', + default: serverDefault.bypassHelmet, + }) + .option('help', { + alias: 'h', + type: 'boolean', + description: 'Print help message', + }) + .boolean('allow_discovery').argv; +if (!opts.help) { + startServer( + { + user: opts['ssh-user'], + host: opts['ssh-host'], + auth: opts['ssh-auth'], + port: opts['ssh-port'], + pass: opts['ssh-pass'], + key: opts['ssh-key'], + knownHosts: opts['known-hosts'], + }, + { + base: opts.base, + host: opts.host, + port: opts.port, + title: opts.title, + bypassHelmet: opts['bypass-helmet'], + }, + opts.command, + opts['force-ssh'], + isUndefined(opts['ssl-key']) || isUndefined(opts['ssl-cert']) + ? undefined + : { key: opts['ssl-key'], cert: opts['ssl-cert'] }, + ).catch((err: Error) => { + logger.error(err); + process.exitCode = 1; + }); +} else { + yargs.showHelp(); + process.exitCode = 0; +} diff --git a/src/server.ts b/src/server.ts index c1415f1..2f11e6a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,145 +2,19 @@ * Create WeTTY server * @module WeTTy */ -import * as yargs from 'yargs'; -import { isUndefined } from 'lodash'; -import { SSH, SSL, SSLBuffer, Server } from './shared/interfaces'; -import { getCommand } from './server/command'; -import { loadSSL } from './server/ssl'; -import { logger } from './shared/logger'; -import { login } from './server/login'; -import { server } from './server/socketServer'; -import { spawn } from './server/spawn'; -import { sshDefault, serverDefault, forceSSHDefault, defaultCommand} from './server/default'; - - -/** - * Check if being run by cli or require - */ -if (require.main === module) { - const opts = yargs - .option('ssl-key', { - type: 'string', - description: 'path to SSL key', - }) - .option('ssl-cert', { - type: 'string', - description: 'path to SSL certificate', - }) - .option('ssh-host', { - description: 'ssh server host', - type: 'string', - default: sshDefault.host, - }) - .option('ssh-port', { - description: 'ssh server port', - type: 'number', - default: sshDefault.port, - }) - .option('ssh-user', { - description: 'ssh user', - type: 'string', - default: sshDefault.user, - }) - .option('title', { - description: 'window title', - type: 'string', - default: serverDefault.title, - }) - .option('ssh-auth', { - description: - 'defaults to "password", you can use "publickey,password" instead', - type: 'string', - default: sshDefault.auth, - }) - .option('ssh-pass', { - description: 'ssh password', - type: 'string', - default: sshDefault.pass, - }) - .option('ssh-key', { - demand: false, - description: - 'path to an optional client private key (connection will be password-less and insecure!)', - type: 'string', - default: sshDefault.key, - }) - .option('force-ssh', { - description: 'Connecting through ssh even if running as root', - type: 'boolean', - default: forceSSHDefault - }) - .option('known-hosts', { - description: 'path to known hosts file', - type: 'string', - default: sshDefault.knownHosts, - }) - .option('base', { - alias: 'b', - description: 'base path to wetty', - type: 'string', - default: serverDefault.base, - }) - .option('port', { - alias: 'p', - description: 'wetty listen port', - type: 'number', - default: serverDefault.port, - }) - .option('host', { - description: 'wetty listen host', - default: serverDefault.host, - type: 'string', - }) - .option('command', { - alias: 'c', - description: 'command to run in shell', - type: 'string', - default: defaultCommand, - }) - .option('bypass-helmet', { - description: 'disable helmet from placing security restrictions', - type: 'boolean', - default: serverDefault.bypassHelmet, - }) - .option('help', { - alias: 'h', - type: 'boolean', - description: 'Print help message', - }) - .boolean('allow_discovery').argv; - if (!opts.help) { - startServer( - { - user: opts['ssh-user'], - host: opts['ssh-host'], - auth: opts['ssh-auth'], - port: opts['ssh-port'], - pass: opts['ssh-pass'], - key: opts['ssh-key'], - knownHosts: opts['known-hosts'], - }, - { - base: opts.base, - host: opts.host, - port: opts.port, - title: opts.title, - bypassHelmet: opts['bypass-helmet'], - }, - opts.command, - opts['force-ssh'], - isUndefined(opts['ssl-key']) || isUndefined(opts['ssl-cert']) - ? undefined - : { key: opts['ssl-key'], cert: opts['ssl-cert'] } - ).catch(err => { - logger.error(err); - process.exitCode = 1; - }); - } else { - yargs.showHelp(); - process.exitCode = 0; - } -} +import type { SSH, SSL, SSLBuffer, Server } from './shared/interfaces'; +import { getCommand } from './server/command.js'; +import { loadSSL } from './server/ssl.js'; +import { logger } from './shared/logger.js'; +import { login } from './server/login.js'; +import { server } from './server/socketServer.js'; +import { spawn } from './server/spawn.js'; +import { + sshDefault, + serverDefault, + forceSSHDefault, + defaultCommand, +} from './server/default.js'; /** * Starts WeTTy Server @@ -149,57 +23,51 @@ if (require.main === module) { export async function startServer( ssh: SSH = sshDefault, serverConf: Server = serverDefault, - command:string = defaultCommand, - forcessh:boolean = forceSSHDefault, - ssl?: SSL + command: string = defaultCommand, + forcessh: boolean = forceSSHDefault, + ssl?: SSL, ): Promise { - if (ssh.key) { - logger.warn(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + if (ssh.key) { + logger.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 sslBuffer: SSLBuffer = await loadSSL(ssl); - const io = server(serverConf, sslBuffer); + const sslBuffer: SSLBuffer = await loadSSL(ssl); + const io = server(serverConf, sslBuffer); + /** + * Wetty server connected too + * @fires WeTTy#connnection + */ + io.on('connection', async (socket: SocketIO.Socket) => { /** - * Wetty server connected too - * @fires WeTTy#connnection + * @event wetty#connection + * @name connection */ - io.on('connection', async (socket: SocketIO.Socket) => { - /** - * @event wetty#connection - * @name connection - */ - logger.info('Connection accepted.'); - const { args, user: sshUser } = getCommand( - socket, - ssh, - command, - forcessh - ); - logger.debug('Command Generated', { - user: sshUser, - cmd: args.join(' '), - }); + logger.info('Connection accepted.'); + const { args, user: sshUser } = getCommand(socket, ssh, command, forcessh); + logger.debug('Command Generated', { + user: sshUser, + cmd: args.join(' '), + }); - if (sshUser) { + if (sshUser) { + spawn(socket, args); + } else { + try { + const username = await login(socket); + args[1] = `${username.trim()}@${args[1]}`; + logger.debug('Spawning term', { + username: username.trim(), + cmd: args.join(' ').trim(), + }); spawn(socket, args); - } else { - try { - const username = await login(socket) - args[1] = `${username.trim()}@${args[1]}`; - logger.debug('Spawning term', { - username: username.trim(), - cmd: args.join(' ').trim(), - }); - spawn(socket, args); - }catch (error) { - logger.info('Disconnect signal sent'); - } + } catch (error) { + logger.info('Disconnect signal sent'); } - }); - return io + } + }); + return io; } - diff --git a/src/server/command.ts b/src/server/command.ts index cba6a50..e44b815 100644 --- a/src/server/command.ts +++ b/src/server/command.ts @@ -1,9 +1,9 @@ -import * as url from 'url'; +import url from 'url'; import { Socket } from 'socket.io'; -import { SSH } from '../shared/interfaces'; -import { address } from './command/address'; -import { loginOptions } from './command/login'; -import { sshOptions } from './command/ssh'; +import { SSH } from '../shared/interfaces.js'; +import { address } from './command/address.js'; +import { loginOptions } from './command/login.js'; +import { sshOptions } from './command/ssh.js'; const localhost = (host: string): boolean => process.getuid() === 0 && @@ -11,7 +11,7 @@ const localhost = (host: string): boolean => const urlArgs = ( referer: string, - def: { [s: string]: string } + def: { [s: string]: string }, ): { [s: string]: string } => Object.assign(def, url.parse(referer, true).query); @@ -26,7 +26,7 @@ export const getCommand = ( }: Socket, { user, host, port, auth, pass, key, knownHosts }: SSH, command: string, - forcessh: boolean + forcessh: boolean, ): { args: string[]; user: boolean } => ({ args: !forcessh && localhost(host) @@ -42,7 +42,7 @@ export const getCommand = ( }), host: address(referer, user, host), }, - key + key, ), user: (!forcessh && localhost(host)) || diff --git a/src/server/command/login.ts b/src/server/command/login.ts index 52c52fe..805bf36 100644 --- a/src/server/command/login.ts +++ b/src/server/command/login.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined.js'; const getRemoteAddress = (remoteAddress: string): string => isUndefined(remoteAddress.split(':')[3]) diff --git a/src/server/command/ssh.ts b/src/server/command/ssh.ts index 78efa9d..6e3a2a6 100644 --- a/src/server/command/ssh.ts +++ b/src/server/command/ssh.ts @@ -1,6 +1,5 @@ -import { isUndefined } from 'lodash'; -import { parseCommand } from './ssh/parse'; -import { logger } from '../../shared/logger'; +import isUndefined from 'lodash/isUndefined.js'; +import { logger } from '../../shared/logger.js'; export function sshOptions( { @@ -12,7 +11,7 @@ export function sshOptions( auth, knownhosts, }: { [s: string]: string }, - key?: string + key?: string, ): string[] { const cmd = parseCommand(command, path); const hostChecking = knownhosts !== '/dev/null' ? 'yes' : 'no'; @@ -42,3 +41,10 @@ export function sshOptions( return cmd === '' ? sshRemoteOptsBase : sshRemoteOptsBase.concat([cmd]); } + +function parseCommand(command: string, path?: string): string { + if (command === 'login' && isUndefined(path)) return ''; + return !isUndefined(path) + ? `$SHELL -c "cd ${path};${command === 'login' ? '$SHELL' : command}"` + : command; +} diff --git a/src/server/command/ssh/parse.ts b/src/server/command/ssh/parse.ts deleted file mode 100644 index 422165d..0000000 --- a/src/server/command/ssh/parse.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isUndefined } from 'lodash'; - -export function parseCommand(command: string, path?: string): string { - if (command === 'login' && isUndefined(path)) return ''; - return !isUndefined(path) - ? `$SHELL -c "cd ${path};${command === 'login' ? '$SHELL' : command}"` - : command; -} diff --git a/src/server/default.ts b/src/server/default.ts index 68685d7..4646b79 100644 --- a/src/server/default.ts +++ b/src/server/default.ts @@ -1,6 +1,6 @@ -import { SSH, Server } from '../shared/interfaces'; +import type { SSH, Server } from '../shared/interfaces'; -export const sshDefault:SSH = { +export const sshDefault: SSH = { user: process.env.SSHUSER || '', host: process.env.SSHHOST || 'localhost', auth: process.env.SSHAUTH || 'password', @@ -8,7 +8,7 @@ export const sshDefault:SSH = { key: process.env.SSHKEY || undefined, port: parseInt(process.env.SSHPORT || '22', 10), knownHosts: process.env.KNOWNHOSTS || '/dev/null', -} +}; export const serverDefault: Server = { base: process.env.BASE || '/wetty/', @@ -19,4 +19,4 @@ export const serverDefault: Server = { }; export const forceSSHDefault = process.env.FORCESSH === 'true' || false; -export const defaultCommand = process.env.COMMAND || 'login' +export const defaultCommand = process.env.COMMAND || 'login'; diff --git a/src/server/login.ts b/src/server/login.ts index 9ab0eac..6cc035b 100644 --- a/src/server/login.ts +++ b/src/server/login.ts @@ -1,5 +1,5 @@ -import { spawn } from 'node-pty'; -import { xterm } from './shared/xterm'; +import pty from 'node-pty'; +import { xterm } from './shared/xterm.js'; export function login(socket: SocketIO.Socket): Promise { // Check request-header for username @@ -12,13 +12,17 @@ export function login(socket: SocketIO.Socket): Promise { // Request carries no username information // Create terminal and ask user for username - const term = spawn('/usr/bin/env', ['node', `${__dirname}/buffer.js`], xterm); + const term = pty.spawn( + '/usr/bin/env', + ['node', `${__dirname}/buffer.js`], + xterm, + ); let buf = ''; return new Promise((resolve, reject) => { term.on('exit', () => { resolve(buf); }); - term.on('data', data => { + term.on('data', (data: string) => { socket.emit('data', data); }); socket diff --git a/src/server/shared/xterm.ts b/src/server/shared/xterm.ts index 99c26c4..b4d4480 100644 --- a/src/server/shared/xterm.ts +++ b/src/server/shared/xterm.ts @@ -1,5 +1,5 @@ -import { IPtyForkOptions } from 'node-pty'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined.js'; +import type { IPtyForkOptions } from 'node-pty'; export const xterm: IPtyForkOptions = { name: 'xterm-256color', @@ -10,6 +10,6 @@ export const xterm: IPtyForkOptions = { {}, ...Object.keys(process.env) .filter((key: string) => !isUndefined(process.env[key])) - .map((key: string) => ({ [key]: process.env[key] })) + .map((key: string) => ({ [key]: process.env[key] })), ), }; diff --git a/src/server/socketServer.ts b/src/server/socketServer.ts index 342b8bc..c232dca 100644 --- a/src/server/socketServer.ts +++ b/src/server/socketServer.ts @@ -1,24 +1,24 @@ -import { isUndefined } from 'lodash'; -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 * as expressWinston from 'express-winston'; -import { SSLBuffer, Server } from '../shared/interfaces'; -import html from './socketServer/html'; -import { logger } from '../shared/logger'; +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 isUndefined from 'lodash/isUndefined.js'; +import sassMiddleware from 'node-sass-middleware'; +import socket from 'socket.io'; +import winston from 'express-winston'; +import { join, resolve } from 'path'; -const distDir = path.join(__dirname, 'client'); +import type { SSLBuffer, Server } from '../shared/interfaces'; +import { html } from './socketServer/html.js'; +import { logger } from '../shared/logger.js'; const trim = (str: string): string => str.replace(/\/*$/, ''); export function server( { base, port, host, title, bypassHelmet }: Server, - { key, cert }: SSLBuffer + { key, cert }: SSLBuffer, ): SocketIO.Server { const basePath = trim(base); @@ -32,18 +32,36 @@ export function server( const app = express(); app - .use(expressWinston.logger(logger)) + .use( + `${basePath}/web_modules`, + express.static(resolve(process.cwd(), 'web_modules')), + ) + .use( + sassMiddleware({ + src: resolve(process.cwd(), 'lib', 'client'), + dest: resolve(process.cwd(), 'assets'), + outputStyle: 'compressed', + log(severity: string, key: string, value: string) { + logger.log(severity, 'node-sass-middleware %s : %s', key, value); + }, + }), + ) + .use(`${basePath}/assets`, express.static(resolve(process.cwd(), 'assets'))) + .use( + `${basePath}/client`, + express.static(resolve(process.cwd(), 'lib', 'client')), + ) + .use(winston.logger(logger)) .use(compression()) - .use(favicon(path.join(distDir, 'favicon.ico'))) - .use(`${basePath}/public`, express.static(distDir)) - .use((req, res, next) => { + .use(favicon(join('assets', 'favicon.ico'))); + /* .use((req, res, next) => { if (req.path.substr(-1) === '/' && req.path.length > 1) res.redirect( 301, - req.path.slice(0, -1) + req.url.slice(req.path.length) + req.path.slice(0, -1) + req.url.slice(req.path.length), ); else next(); - }); + }); */ // Allow helmet to be bypassed. // Unfortunately, order matters with middleware @@ -52,7 +70,7 @@ export function server( app.use(helmet()); } - const client = html(base, title); + const client = html(basePath, title); app.get(basePath, client).get(`${basePath}/ssh/:user`, client); return socket( @@ -73,6 +91,6 @@ export function server( path: `${basePath}/socket.io`, pingInterval: 3000, pingTimeout: 7000, - } + }, ); } diff --git a/src/server/socketServer/html.ts b/src/server/socketServer/html.ts index b6f612c..219f677 100644 --- a/src/server/socketServer/html.ts +++ b/src/server/socketServer/html.ts @@ -1,21 +1,17 @@ -import * as express from 'express'; +import express from 'express'; -export default (base: string, title: string) => ( - req: express.Request, - res: express.Response -): void => { - const resourcePath = /^\/ssh\//.test(req.url.replace(base, '/')) - ? '../' - : base; - - res.send(` +const render = ( + title: string, + css: string, + js: string, +): string => ` - + ${title} - +
@@ -27,11 +23,19 @@ export default (base: string, title: string) => (
+ alt="Toggle options" + >
- +