Browse Source

use snowpack to import client modules to avoid bundling

pull/270/head
butlerx 4 years ago
parent
commit
8b5a7df712
No known key found for this signature in database GPG Key ID: B37CA765BAA89170
  1. 3
      .eslintignore
  2. 3
      .gitignore
  3. 7
      .prettierrc.json
  4. 0
      assets/favicon.ico
  5. 66
      package.json
  6. 0
      src/client/client/copyToClipboard.ts
  7. 6
      src/client/client/disconnect.ts
  8. 42
      src/client/client/download.spec.ts
  9. 0
      src/client/client/download.ts
  10. 2
      src/client/client/mobile.ts
  11. 2
      src/client/client/options.ts
  12. 2
      src/client/client/socket.ts
  13. 41
      src/client/index.ts
  14. 0
      src/client/shared/elements.ts
  15. 0
      src/client/shared/verify.ts
  16. 69
      src/client/styles.scss
  17. 137
      src/main.ts
  18. 230
      src/server.ts
  19. 16
      src/server/command.ts
  20. 2
      src/server/command/login.ts
  21. 14
      src/server/command/ssh.ts
  22. 8
      src/server/command/ssh/parse.ts
  23. 8
      src/server/default.ts
  24. 12
      src/server/login.ts
  25. 6
      src/server/shared/xterm.ts
  26. 64
      src/server/socketServer.ts
  27. 36
      src/server/socketServer/html.ts
  28. 14
      src/server/spawn.ts
  29. 12
      src/server/ssl.ts
  30. 10
      src/shared/logger.ts
  31. 30
      tsconfig.json
  32. 7
      tsconfig/cjs.json
  33. 14
      tsconfig/esm.json
  34. 2439
      yarn.lock

3
.eslintignore

@ -1,5 +1,6 @@
node_modules/ node_modules/
.esm-cache .esm-cache
dist lib
public/ public/
*hterm* *hterm*
web_modules

3
.gitignore

@ -1,4 +1,5 @@
./lib web_modules
lib
# Created by https://www.toptal.com/developers/gitignore/api/node # Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node # Edit at https://www.toptal.com/developers/gitignore?templates=node

7
.prettierrc.json

@ -1,10 +1,13 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5", "trailingComma": "all",
"proseWrap": "always", "proseWrap": "always",
"overrides": [ "overrides": [
{ {
"files": ["*.js", "*.ts"], "files": [
"*.js",
"*.ts"
],
"options": { "options": {
"printWidth": 80 "printWidth": 80
} }

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

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

66
package.json

@ -3,7 +3,23 @@
"version": "2.0.0", "version": "2.0.0",
"description": "WeTTY = Web + TTY. Terminal access in browser over http/https", "description": "WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/butlerx/wetty", "homepage": "https://github.com/butlerx/wetty",
"license": "MIT",
"type": "module", "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": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/butlerx/wetty.git" "url": "git://github.com/butlerx/wetty.git"
@ -13,24 +29,9 @@
"email": "butlerx@notthe.cloud", "email": "butlerx@notthe.cloud",
"url": "cianbutler.ie" "url": "cianbutler.ie"
}, },
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/butlerx/wetty/issues" "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": { "husky": {
"hooks": { "hooks": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
@ -38,12 +39,10 @@
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts}": [ "*.{js,ts}": [
"eslint --fix", "eslint --fix"
"git add"
], ],
"*.{json,scss,md}": [ "*.{json,scss,md}": [
"prettier --write", "prettier --write"
"git add"
] ]
}, },
"engines": { "engines": {
@ -55,7 +54,21 @@
"*.json" "*.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": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
@ -65,12 +78,12 @@
"file-type": "^12.3.0", "file-type": "^12.3.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"helmet": "^3.20.1", "helmet": "^3.20.1",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"node-sass-middleware": "^0.11.0",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
"toastify-js": "^1.6.1", "toastify-js": "^1.6.1",
"winston": "^3.2.1", "winston": "^3.2.1",
"xterm": "^4.8.1", "xterm": "^4.8.1",
@ -78,12 +91,6 @@
"yargs": "^14.0.0" "yargs": "^14.0.0"
}, },
"devDependencies": { "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/chai": "^4.2.5",
"@types/compression": "^1.0.1", "@types/compression": "^1.0.1",
"@types/express": "^4.17.1", "@types/express": "^4.17.1",
@ -94,6 +101,7 @@
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/morgan": "^1.7.37", "@types/morgan": "^1.7.37",
"@types/node": "^12.7.3", "@types/node": "^12.7.3",
"@types/node-sass-middleware": "^0.0.31",
"@types/serve-favicon": "^2.2.31", "@types/serve-favicon": "^2.2.31",
"@types/sinon": "^7.5.1", "@types/sinon": "^7.5.1",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
@ -111,7 +119,6 @@
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"file-loader": "^4.2.0",
"git-authors-cli": "^1.0.27", "git-authors-cli": "^1.0.27",
"husky": "^4.2.5", "husky": "^4.2.5",
"jsdom": "^15.2.1", "jsdom": "^15.2.1",
@ -120,6 +127,7 @@
"nodemon": "^2.0.4", "nodemon": "^2.0.4",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"snowpack": "^2.7.5",
"typescript": "^3.9.7" "typescript": "^3.9.7"
}, },
"contributors": [ "contributors": [

0
src/client/copyToClipboard.ts → src/client/client/copyToClipboard.ts

6
src/client/disconnect.ts → src/client/client/disconnect.ts

@ -1,6 +1,6 @@
import { isNull, isUndefined } from 'lodash'; import { isNull, isUndefined } from '../../web_modules/lodash.js';
import { verifyPrompt } from '../shared/verify'; import { verifyPrompt } from '../shared/verify.js';
import { overlay } from '../shared/elements'; import { overlay } from '../shared/elements.js';
export function disconnect(reason: string): void { export function disconnect(reason: string): void {
if (isNull(overlay)) return; if (isNull(overlay)) return;

42
src/client/download.spec.ts → src/client/client/download.spec.ts

@ -4,11 +4,8 @@ import { expect } from 'chai';
import 'mocha'; import 'mocha';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { JSDOM } from 'jsdom';
import { FileDownloader } from './download'; import { FileDownloader } from './download';
const { window } = new JSDOM(`...`);
describe('FileDownloader', () => { describe('FileDownloader', () => {
const FILE_BEGIN = 'BEGIN'; const FILE_BEGIN = 'BEGIN';
const FILE_END = 'END'; const FILE_END = 'END';
@ -25,7 +22,7 @@ describe('FileDownloader', () => {
it('should return data before file markers', () => { it('should return data before file markers', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect( 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'); ).to.equal('DATA AT THE LEFT');
expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
@ -34,7 +31,7 @@ describe('FileDownloader', () => {
it('should return data after file markers', () => { it('should return data after file markers', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect( 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'); ).to.equal('DATA AT THE RIGHT');
expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
@ -44,17 +41,17 @@ describe('FileDownloader', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect( expect(
fileDownloader.buffer( 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'); ).to.equal('DATA AT THE LEFTDATA AT THE RIGHT');
expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
}); });
it('should return data before a beginning marker found', () => { 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( 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'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect(fileDownloader.buffer(`${FILE_BEGIN}FI`)).to.equal(''); expect(fileDownloader.buffer(`${FILE_BEGIN}FI`)).to.equal('');
expect(fileDownloader.buffer(`LE${FILE_END}DATA AT THE RIGHT`)).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.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
@ -96,13 +93,13 @@ describe('FileDownloader', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( 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('E')).to.equal('');
expect(fileDownloader.buffer('G')).to.equal(''); expect(fileDownloader.buffer('G')).to.equal('');
expect(fileDownloader.buffer('I')).to.equal(''); expect(fileDownloader.buffer('I')).to.equal('');
expect(fileDownloader.buffer('NFILE' + 'ENDDATA AT THE RIGHT')).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.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
@ -113,14 +110,14 @@ describe('FileDownloader', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( 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('E')).to.equal('');
expect(fileDownloader.buffer('G')).to.equal(''); expect(fileDownloader.buffer('G')).to.equal('');
// This isn't part of the file_begin marker and should trigger the partial // This isn't part of the file_begin marker and should trigger the partial
// file begin marker to be returned with the normal data // file begin marker to be returned with the normal data
expect(fileDownloader.buffer('ZDATA AT THE RIGHT')).to.equal( expect(fileDownloader.buffer('ZDATA AT THE RIGHT')).to.equal(
'BEGZDATA AT THE RIGHT' 'BEGZDATA AT THE RIGHT',
); );
expect(onCompleteFileStub.called).to.be.false; expect(onCompleteFileStub.called).to.be.false;
}); });
@ -143,14 +140,14 @@ describe('FileDownloader', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'BE')).to.equal( 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('G')).to.equal('');
expect(fileDownloader.buffer('I')).to.equal(''); expect(fileDownloader.buffer('I')).to.equal('');
expect(fileDownloader.buffer('NFILEE')).to.equal(''); expect(fileDownloader.buffer('NFILEE')).to.equal('');
expect(fileDownloader.buffer('N')).to.equal(''); expect(fileDownloader.buffer('N')).to.equal('');
expect(fileDownloader.buffer('DDATA AT THE RIGHT')).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.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE');
@ -162,8 +159,13 @@ describe('FileDownloader', () => {
expect( expect(
fileDownloader.buffer( 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'); ).to.equal('DATA AT THE LEFT' + 'SECOND DATA');
expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1');
@ -180,11 +182,11 @@ describe('FileDownloader', () => {
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile');
expect( 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'); ).to.equal('DATA AT THE LEFT');
expect(onCompleteFileStub.calledOnce).to.be.false; expect(onCompleteFileStub.calledOnce).to.be.false;
expect( expect(
fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN') fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN'),
).to.equal('SECOND DATA'); ).to.equal('SECOND DATA');
expect(onCompleteFileStub.calledOnce).to.be.true; expect(onCompleteFileStub.calledOnce).to.be.true;
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1');

0
src/client/download.ts → src/client/client/download.ts

2
src/client/mobile.ts → src/client/client/mobile.ts

@ -1,4 +1,4 @@
import { isNull } from 'lodash'; import { isNull } from '../../web_modules/lodash.js';
export function mobileKeyboard(): void { export function mobileKeyboard(): void {
const [screen] = document.getElementsByClassName('xterm-screen'); const [screen] = document.getElementsByClassName('xterm-screen');

2
src/client/options.ts → src/client/client/options.ts

@ -1,4 +1,4 @@
import { isUndefined } from 'lodash'; import { isUndefined } from '../../web_modules/lodash.js';
export function loadOptions(): object { export function loadOptions(): object {
const defaultOptions = { fontSize: 14 }; const defaultOptions = { fontSize: 14 };

2
src/client/socket.ts → 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/[^/]+$'); const userRegex = new RegExp('ssh/[^/]+$');
export const trim = (str: string): string => str.replace(/\/*$/, ''); export const trim = (str: string): string => str.replace(/\/*$/, '');

41
src/client.ts → src/client/index.ts

@ -1,21 +1,22 @@
import { Terminal } from 'xterm'; import { Terminal } from '../web_modules/xterm.js';
import { isNull } from 'lodash'; import { isNull } from '../web_modules/lodash.js';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from '../web_modules/xterm-addon-fit.js';
import { dom, library } from '@fortawesome/fontawesome-svg-core'; import {
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs'; dom,
import Toastify from 'toastify-js'; library,
import * as fileType from 'file-type'; } 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 { FileDownloader } from './client/download.js';
import { copySelected, copyShortcut } from './client/copyToClipboard'; import { copySelected, copyShortcut } from './client/copyToClipboard.js';
import { disconnect } from './client/disconnect'; import { disconnect } from './client/disconnect.js';
import { loadOptions } from './client/options'; import { loadOptions } from './client/options.js';
import { mobileKeyboard } from './client/mobile'; import { mobileKeyboard } from './client/mobile.js';
import { overlay, terminal } from './shared/elements'; import { overlay, terminal } from './shared/elements.js';
import { socket } from './client/socket'; import { socket } from './client/socket.js';
import { verifyPrompt } from './shared/verify'; import { verifyPrompt } from './shared/verify.js';
import './client/wetty.scss';
import './client/favicon.ico';
// Setup for fontawesome // Setup for fontawesome
library.add(faCogs); library.add(faCogs);
@ -76,7 +77,7 @@ socket.on('connect', () => {
() => { () => {
if (term.hasSelection()) copySelected(term.getSelection()); if (term.hasSelection()) copySelected(term.getSelection());
}, },
false false,
); );
window.onresize = resize; window.onresize = resize;
@ -135,10 +136,10 @@ socket.on('connect', () => {
}).showToast(); }).showToast();
}); });
term.onData(data => { term.onData((data: string) => {
socket.emit('input', data); socket.emit('input', data);
}); });
term.onResize(size => { term.onResize((size: string) => {
socket.emit('resize', size); socket.emit('resize', size);
}); });
socket socket

0
src/shared/elements.ts → src/client/shared/elements.ts

0
src/shared/verify.ts → src/client/shared/verify.ts

69
src/client/wetty.scss → src/client/styles.scss

@ -1,10 +1,11 @@
@import '~xterm/css/xterm'; @import '~xterm/dist/xterm';
@import '~toastify-js/src/toastify.css'; @import '~toastify-js/src/toastify.css';
$black: #000; $black: #000;
$grey: rgba(0, 0, 0, 0.75); $grey: rgba(0, 0, 0, 0.75);
$white: #fff; $white: #fff;
$lgrey: #ccc; $lgrey: #ccc;
$red: red;
html, html,
body { body {
@ -48,51 +49,55 @@ body {
} }
#options { #options {
height: 16px;
position: absolute; position: absolute;
top: 1em;
right: 1em; right: 1em;
z-index: 20; top: 1em;
height: 16px;
width: 16px; width: 16px;
z-index: 20;
a.toggler { a {
display: inline-block; .toggler {
position: absolute; color: $lgrey;
right: 1em; display: inline-block;
top: 0em; font-size: 16px;
font-size: 16px; position: absolute;
color: $lgrey; right: 1em;
z-index: 20; top: 0;
z-index: 20;
:hover { :hover {
color: $white; color: $white;
}
} }
} }
.editor { .editor {
background-color: rgba(0, 0, 0, 0.85); background-color: rgba(0, 0, 0, 0.85);
padding: 0.5em;
border-radius: 0.3em;
border-color: rgba(255, 255, 255, 0.25); border-color: rgba(255, 255, 255, 0.25);
border-radius: 0.3em;
color: #eee;
display: none; display: none;
position: relative; font-size: 24px;
height: 100%; height: 100%;
width: 100%; padding: 0.5em;
top: 1em; position: relative;
right: 2em; right: 2em;
color: #eee; top: 1em;
font-size: 24px; width: 100%;
} .error {
.editor.error { color: $red;
color: red; }
} }
} }
#options.opened { #options {
height: 50%; .opened {
width: 50%; height: 50%;
.editor { width: 50%;
display: flex; .editor {
display: flex;
}
} }
} }
@ -102,6 +107,8 @@ body {
} }
} }
.xterm .xterm-viewport { .xterm {
overflow-y: hidden; .xterm-viewport {
overflow-y: hidden;
}
} }

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

230
src/server.ts

@ -2,145 +2,19 @@
* Create WeTTY server * Create WeTTY server
* @module WeTTy * @module WeTTy
*/ */
import * as yargs from 'yargs'; import type { SSH, SSL, SSLBuffer, Server } from './shared/interfaces';
import { isUndefined } from 'lodash'; import { getCommand } from './server/command.js';
import { SSH, SSL, SSLBuffer, Server } from './shared/interfaces'; import { loadSSL } from './server/ssl.js';
import { getCommand } from './server/command'; import { logger } from './shared/logger.js';
import { loadSSL } from './server/ssl'; import { login } from './server/login.js';
import { logger } from './shared/logger'; import { server } from './server/socketServer.js';
import { login } from './server/login'; import { spawn } from './server/spawn.js';
import { server } from './server/socketServer'; import {
import { spawn } from './server/spawn'; sshDefault,
import { sshDefault, serverDefault, forceSSHDefault, defaultCommand} from './server/default'; serverDefault,
forceSSHDefault,
defaultCommand,
/** } from './server/default.js';
* 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;
}
}
/** /**
* Starts WeTTy Server * Starts WeTTy Server
@ -149,57 +23,51 @@ if (require.main === module) {
export async function startServer( export async function startServer(
ssh: SSH = sshDefault, ssh: SSH = sshDefault,
serverConf: Server = serverDefault, serverConf: Server = serverDefault,
command:string = defaultCommand, command: string = defaultCommand,
forcessh:boolean = forceSSHDefault, forcessh: boolean = forceSSHDefault,
ssl?: SSL ssl?: SSL,
): Promise<SocketIO.Server> { ): Promise<SocketIO.Server> {
if (ssh.key) { if (ssh.key) {
logger.warn(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! logger.warn(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! Password-less auth enabled using private key from ${ssh.key}. ! Password-less auth enabled using private key from ${ssh.key}.
! This is dangerous, anything that reaches the wetty server ! This is dangerous, anything that reaches the wetty server
! will be able to run remote operations without authentication. ! will be able to run remote operations without authentication.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`); !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
} }
const sslBuffer: SSLBuffer = await loadSSL(ssl); const sslBuffer: SSLBuffer = await loadSSL(ssl);
const io = server(serverConf, sslBuffer); const io = server(serverConf, sslBuffer);
/**
* Wetty server connected too
* @fires WeTTy#connnection
*/
io.on('connection', async (socket: SocketIO.Socket) => {
/** /**
* Wetty server connected too * @event wetty#connection
* @fires WeTTy#connnection * @name connection
*/ */
io.on('connection', async (socket: SocketIO.Socket) => { logger.info('Connection accepted.');
/** const { args, user: sshUser } = getCommand(socket, ssh, command, forcessh);
* @event wetty#connection logger.debug('Command Generated', {
* @name connection 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); spawn(socket, args);
} else { } catch (error) {
try { logger.info('Disconnect signal sent');
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');
}
} }
}); }
return io });
return io;
} }

16
src/server/command.ts

@ -1,9 +1,9 @@
import * as url from 'url'; import url from 'url';
import { Socket } from 'socket.io'; import { Socket } from 'socket.io';
import { SSH } from '../shared/interfaces'; import { SSH } from '../shared/interfaces.js';
import { address } from './command/address'; import { address } from './command/address.js';
import { loginOptions } from './command/login'; import { loginOptions } from './command/login.js';
import { sshOptions } from './command/ssh'; import { sshOptions } from './command/ssh.js';
const localhost = (host: string): boolean => const localhost = (host: string): boolean =>
process.getuid() === 0 && process.getuid() === 0 &&
@ -11,7 +11,7 @@ const localhost = (host: string): boolean =>
const urlArgs = ( const urlArgs = (
referer: string, referer: string,
def: { [s: string]: string } def: { [s: string]: string },
): { [s: string]: string } => ): { [s: string]: string } =>
Object.assign(def, url.parse(referer, true).query); Object.assign(def, url.parse(referer, true).query);
@ -26,7 +26,7 @@ export const getCommand = (
}: Socket, }: Socket,
{ user, host, port, auth, pass, key, knownHosts }: SSH, { user, host, port, auth, pass, key, knownHosts }: SSH,
command: string, command: string,
forcessh: boolean forcessh: boolean,
): { args: string[]; user: boolean } => ({ ): { args: string[]; user: boolean } => ({
args: args:
!forcessh && localhost(host) !forcessh && localhost(host)
@ -42,7 +42,7 @@ export const getCommand = (
}), }),
host: address(referer, user, host), host: address(referer, user, host),
}, },
key key,
), ),
user: user:
(!forcessh && localhost(host)) || (!forcessh && localhost(host)) ||

2
src/server/command/login.ts

@ -1,4 +1,4 @@
import { isUndefined } from 'lodash'; import isUndefined from 'lodash/isUndefined.js';
const getRemoteAddress = (remoteAddress: string): string => const getRemoteAddress = (remoteAddress: string): string =>
isUndefined(remoteAddress.split(':')[3]) isUndefined(remoteAddress.split(':')[3])

14
src/server/command/ssh.ts

@ -1,6 +1,5 @@
import { isUndefined } from 'lodash'; import isUndefined from 'lodash/isUndefined.js';
import { parseCommand } from './ssh/parse'; import { logger } from '../../shared/logger.js';
import { logger } from '../../shared/logger';
export function sshOptions( export function sshOptions(
{ {
@ -12,7 +11,7 @@ export function sshOptions(
auth, auth,
knownhosts, knownhosts,
}: { [s: string]: string }, }: { [s: string]: string },
key?: string key?: string,
): string[] { ): string[] {
const cmd = parseCommand(command, path); const cmd = parseCommand(command, path);
const hostChecking = knownhosts !== '/dev/null' ? 'yes' : 'no'; const hostChecking = knownhosts !== '/dev/null' ? 'yes' : 'no';
@ -42,3 +41,10 @@ export function sshOptions(
return cmd === '' ? sshRemoteOptsBase : sshRemoteOptsBase.concat([cmd]); 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;
}

8
src/server/command/ssh/parse.ts

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

8
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 || '', user: process.env.SSHUSER || '',
host: process.env.SSHHOST || 'localhost', host: process.env.SSHHOST || 'localhost',
auth: process.env.SSHAUTH || 'password', auth: process.env.SSHAUTH || 'password',
@ -8,7 +8,7 @@ export const sshDefault:SSH = {
key: process.env.SSHKEY || undefined, key: process.env.SSHKEY || undefined,
port: parseInt(process.env.SSHPORT || '22', 10), port: parseInt(process.env.SSHPORT || '22', 10),
knownHosts: process.env.KNOWNHOSTS || '/dev/null', knownHosts: process.env.KNOWNHOSTS || '/dev/null',
} };
export const serverDefault: Server = { export const serverDefault: Server = {
base: process.env.BASE || '/wetty/', base: process.env.BASE || '/wetty/',
@ -19,4 +19,4 @@ export const serverDefault: Server = {
}; };
export const forceSSHDefault = process.env.FORCESSH === 'true' || false; export const forceSSHDefault = process.env.FORCESSH === 'true' || false;
export const defaultCommand = process.env.COMMAND || 'login' export const defaultCommand = process.env.COMMAND || 'login';

12
src/server/login.ts

@ -1,5 +1,5 @@
import { spawn } from 'node-pty'; import pty from 'node-pty';
import { xterm } from './shared/xterm'; import { xterm } from './shared/xterm.js';
export function login(socket: SocketIO.Socket): Promise<string> { export function login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username // Check request-header for username
@ -12,13 +12,17 @@ export function login(socket: SocketIO.Socket): Promise<string> {
// Request carries no username information // Request carries no username information
// Create terminal and ask user for username // 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 = ''; let buf = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
term.on('exit', () => { term.on('exit', () => {
resolve(buf); resolve(buf);
}); });
term.on('data', data => { term.on('data', (data: string) => {
socket.emit('data', data); socket.emit('data', data);
}); });
socket socket

6
src/server/shared/xterm.ts

@ -1,5 +1,5 @@
import { IPtyForkOptions } from 'node-pty'; import isUndefined from 'lodash/isUndefined.js';
import { isUndefined } from 'lodash'; import type { IPtyForkOptions } from 'node-pty';
export const xterm: IPtyForkOptions = { export const xterm: IPtyForkOptions = {
name: 'xterm-256color', name: 'xterm-256color',
@ -10,6 +10,6 @@ export const xterm: IPtyForkOptions = {
{}, {},
...Object.keys(process.env) ...Object.keys(process.env)
.filter((key: string) => !isUndefined(process.env[key])) .filter((key: string) => !isUndefined(process.env[key]))
.map((key: string) => ({ [key]: process.env[key] })) .map((key: string) => ({ [key]: process.env[key] })),
), ),
}; };

64
src/server/socketServer.ts

@ -1,24 +1,24 @@
import { isUndefined } from 'lodash'; import compression from 'compression';
import * as compression from 'compression'; import express from 'express';
import * as express from 'express'; import favicon from 'serve-favicon';
import * as favicon from 'serve-favicon'; import helmet from 'helmet';
import * as helmet from 'helmet'; import http from 'http';
import * as http from 'http'; import https from 'https';
import * as https from 'https'; import isUndefined from 'lodash/isUndefined.js';
import * as path from 'path'; import sassMiddleware from 'node-sass-middleware';
import * as socket from 'socket.io'; import socket from 'socket.io';
import * as expressWinston from 'express-winston'; import winston from 'express-winston';
import { SSLBuffer, Server } from '../shared/interfaces'; import { join, resolve } from 'path';
import html from './socketServer/html';
import { logger } from '../shared/logger';
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(/\/*$/, ''); const trim = (str: string): string => str.replace(/\/*$/, '');
export function server( export function server(
{ base, port, host, title, bypassHelmet }: Server, { base, port, host, title, bypassHelmet }: Server,
{ key, cert }: SSLBuffer { key, cert }: SSLBuffer,
): SocketIO.Server { ): SocketIO.Server {
const basePath = trim(base); const basePath = trim(base);
@ -32,18 +32,36 @@ export function server(
const app = express(); const app = express();
app 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(compression())
.use(favicon(path.join(distDir, 'favicon.ico'))) .use(favicon(join('assets', 'favicon.ico')));
.use(`${basePath}/public`, express.static(distDir)) /* .use((req, res, next) => {
.use((req, res, next) => {
if (req.path.substr(-1) === '/' && req.path.length > 1) if (req.path.substr(-1) === '/' && req.path.length > 1)
res.redirect( res.redirect(
301, 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(); else next();
}); }); */
// Allow helmet to be bypassed. // Allow helmet to be bypassed.
// Unfortunately, order matters with middleware // Unfortunately, order matters with middleware
@ -52,7 +70,7 @@ export function server(
app.use(helmet()); app.use(helmet());
} }
const client = html(base, title); const client = html(basePath, title);
app.get(basePath, client).get(`${basePath}/ssh/:user`, client); app.get(basePath, client).get(`${basePath}/ssh/:user`, client);
return socket( return socket(
@ -73,6 +91,6 @@ export function server(
path: `${basePath}/socket.io`, path: `${basePath}/socket.io`,
pingInterval: 3000, pingInterval: 3000,
pingTimeout: 7000, pingTimeout: 7000,
} },
); );
} }

36
src/server/socketServer/html.ts

@ -1,21 +1,17 @@
import * as express from 'express'; import express from 'express';
export default (base: string, title: string) => ( const render = (
req: express.Request, title: string,
res: express.Response css: string,
): void => { js: string,
const resourcePath = /^\/ssh\//.test(req.url.replace(base, '/')) ): string => `<!doctype html>
? '../'
: base;
res.send(`<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="utf8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>${title}</title> <title>${title}</title>
<link rel="stylesheet" href="${resourcePath}public/index.css" /> <link rel="stylesheet" href="${css}" />
</head> </head>
<body> <body>
<div id="overlay"> <div id="overlay">
@ -27,11 +23,19 @@ export default (base: string, title: string) => (
<div id="options"> <div id="options">
<a class="toggler" <a class="toggler"
href="#" href="#"
alt="Toggle options"><i class="fas fa-cogs"></i></a> alt="Toggle options"
><i class="fas fa-cogs" /></a>
<textarea class="editor"></textarea> <textarea class="editor"></textarea>
</div> </div>
<div id="terminal"></div> <div id="terminal"></div>
<script src="${resourcePath}public/index.js"></script> <script type="module" src="${js}" />
</body> </body>
</html>`); </html>`;
};
export const html = (base: string, title: string) => (
_req: express.Request,
res: express.Response,
) =>
res.send(
render(title, `${base}/assets/styles.css`, `${base}/client/index.js`),
);

14
src/server/spawn.ts

@ -1,10 +1,10 @@
import { isUndefined } from 'lodash'; import isUndefined from 'lodash/isUndefined.js';
import { spawn as spawnTerm } from 'node-pty'; import pty from 'node-pty';
import { logger } from '../shared/logger'; import { logger } from '../shared/logger.js';
import { xterm } from './shared/xterm'; import { xterm } from './shared/xterm.js';
export function spawn(socket: SocketIO.Socket, args: string[]): void { export function spawn(socket: SocketIO.Socket, args: string[]): void {
const term = spawnTerm('/usr/bin/env', args, xterm); const term = pty.spawn('/usr/bin/env', args, xterm);
const { pid } = term; const { pid } = term;
const address = args[0] === 'ssh' ? args[1] : 'localhost'; const address = args[0] === 'ssh' ? args[1] : 'localhost';
logger.info('Process Started on behalf of user', { logger.info('Process Started on behalf of user', {
@ -12,7 +12,7 @@ export function spawn(socket: SocketIO.Socket, args: string[]): void {
address, address,
}); });
socket.emit('login'); socket.emit('login');
term.on('exit', code => { term.on('exit', (code: number) => {
logger.info('Process exited', { code, pid }); logger.info('Process exited', { code, pid });
socket.emit('logout'); socket.emit('logout');
socket socket
@ -20,7 +20,7 @@ export function spawn(socket: SocketIO.Socket, args: string[]): void {
.removeAllListeners('resize') .removeAllListeners('resize')
.removeAllListeners('input'); .removeAllListeners('input');
}); });
term.on('data', data => { term.on('data', (data: string) => {
socket.emit('data', data); socket.emit('data', data);
}); });
socket socket

12
src/server/ssl.ts

@ -1,14 +1,14 @@
import { readFile } from 'fs-extra'; import fs from 'fs-extra';
import { resolve } from 'path'; import path from 'path';
import { isUndefined } from 'lodash'; import isUndefined from 'lodash/isUndefined.js';
import { SSL, SSLBuffer } from '../shared/interfaces'; import type { SSL, SSLBuffer } from '../shared/interfaces';
export async function loadSSL(ssl?: SSL): Promise<SSLBuffer> { export async function loadSSL(ssl?: SSL): Promise<SSLBuffer> {
if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert)) if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert))
return {}; return {};
const [key, cert]: Buffer[] = await Promise.all([ const [key, cert]: Buffer[] = await Promise.all([
readFile(resolve(ssl.key)), fs.readFile(path.resolve(ssl.key)),
readFile(resolve(ssl.cert)), fs.readFile(path.resolve(ssl.cert)),
]); ]);
return { key, cert }; return { key, cert };
} }

10
src/shared/logger.ts

@ -1,20 +1,20 @@
import { createLogger, format, transports } from 'winston'; import winston from 'winston';
const { combine, timestamp, label, simple, json, colorize } = format; const { combine, timestamp, label, simple, json, colorize } = winston.format;
const dev = combine( const dev = combine(
colorize(), colorize(),
label({ label: 'Wetty' }), label({ label: 'Wetty' }),
timestamp(), timestamp(),
simple() simple(),
); );
const prod = combine(label({ label: 'Wetty' }), timestamp(), json()); const prod = combine(label({ label: 'Wetty' }), timestamp(), json());
export const logger = createLogger({ export const logger = winston.createLogger({
format: process.env.NODE_ENV === 'development' ? dev : prod, format: process.env.NODE_ENV === 'development' ? dev : prod,
transports: [ transports: [
new transports.Console({ new winston.transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
handleExceptions: true, handleExceptions: true,
}), }),

30
tsconfig.json

@ -0,0 +1,30 @@
{
"compilerOptions": {
"module": "esnext",
"target": "es2019",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"downlevelIteration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./lib",
"removeComments": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
},
"include": [
"src"
],
"exclude": [
"node_modules/*"
]
}

7
tsconfig/cjs.json

@ -1,7 +0,0 @@
{
"extends": "./esm.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "../lib/cjs"
}
}

14
tsconfig/esm.json

@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2020",
"strict": true,
"sourceMap": true,
"esModuleInterop": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"outDir": "../lib/esm",
"typeRoots": ["node_module/@types"]
},
"include": ["../src/**/*.ts"]
}

2439
yarn.lock

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