committed by
GitHub
86 changed files with 3078 additions and 6241 deletions
@ -1,5 +0,0 @@ |
|||||
{ |
|
||||
"presets": ["@babel/preset-typescript", ["@babel/env"]], |
|
||||
"compact": true, |
|
||||
"plugins": ["lodash", "@babel/plugin-proposal-class-properties"] |
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
node_modules/ |
|
||||
.esm-cache |
|
||||
dist |
|
||||
public/ |
|
||||
*hterm* |
|
@ -1,45 +0,0 @@ |
|||||
module.exports = { |
|
||||
parser: '@typescript-eslint/parser', |
|
||||
plugins: ['@typescript-eslint', 'prettier'], |
|
||||
env: { |
|
||||
es6: true, |
|
||||
node: true, |
|
||||
browser: true, |
|
||||
}, |
|
||||
root: true, |
|
||||
extends: [ |
|
||||
'airbnb-base', |
|
||||
'plugin:@typescript-eslint/recommended', |
|
||||
'prettier', |
|
||||
'prettier/@typescript-eslint', |
|
||||
], |
|
||||
rules: { |
|
||||
'linebreak-style': ['error', 'unix'], |
|
||||
'arrow-parens': ['error', 'as-needed'], |
|
||||
'no-param-reassign': ['error', { props: false }], |
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }], |
|
||||
'no-use-before-define': ['error', { functions: false }], |
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }], |
|
||||
'import/prefer-default-export': 'off', |
|
||||
'lines-between-class-members': [ |
|
||||
'error', |
|
||||
'always', |
|
||||
{ exceptAfterSingleLine: true }, |
|
||||
], |
|
||||
'import/extensions': [ |
|
||||
'error', |
|
||||
'always', |
|
||||
{ |
|
||||
js: 'ignorePackages', |
|
||||
ts: 'never', |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
settings: { |
|
||||
'import/resolver': { |
|
||||
node: { |
|
||||
extensions: ['.ts', '.js'], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}; |
|
@ -0,0 +1,71 @@ |
|||||
|
{ |
||||
|
"parser": "@typescript-eslint/parser", |
||||
|
"plugins": ["@typescript-eslint", "prettier", "mocha"], |
||||
|
"env": { |
||||
|
"es6": true, |
||||
|
"node": true, |
||||
|
"browser": true |
||||
|
}, |
||||
|
"root": true, |
||||
|
"extends": [ |
||||
|
"airbnb-base", |
||||
|
"plugin:@typescript-eslint/recommended", |
||||
|
"prettier", |
||||
|
"prettier/@typescript-eslint" |
||||
|
], |
||||
|
"rules": { |
||||
|
"linebreak-style": ["error", "unix"], |
||||
|
"arrow-parens": ["error", "as-needed"], |
||||
|
"no-param-reassign": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"props": false |
||||
|
} |
||||
|
], |
||||
|
"func-style": [ |
||||
|
"error", |
||||
|
"declaration", |
||||
|
{ |
||||
|
"allowArrowFunctions": true |
||||
|
} |
||||
|
], |
||||
|
"no-use-before-define": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"functions": false |
||||
|
} |
||||
|
], |
||||
|
"@typescript-eslint/no-use-before-define": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"functions": false |
||||
|
} |
||||
|
], |
||||
|
"import/prefer-default-export": "off", |
||||
|
"lines-between-class-members": [ |
||||
|
"error", |
||||
|
"always", |
||||
|
{ |
||||
|
"exceptAfterSingleLine": true |
||||
|
} |
||||
|
], |
||||
|
"import/extensions": [ |
||||
|
"error", |
||||
|
"always", |
||||
|
{ |
||||
|
"js": "ignorePackages", |
||||
|
"ts": "never" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"settings": { |
||||
|
"import/resolver": { |
||||
|
"typescript": { |
||||
|
"project": ["./tsconfig.browser.json", "./tsconfig.node.json"] |
||||
|
}, |
||||
|
"node": { |
||||
|
"extensions": [".ts", ".js"] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,19 +1,118 @@ |
|||||
lib-cov |
web_modules |
||||
*.seed |
lib |
||||
|
assets/css |
||||
|
build |
||||
|
|
||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node |
||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node |
||||
|
|
||||
|
### Node ### |
||||
|
# Logs |
||||
|
logs |
||||
*.log |
*.log |
||||
*.csv |
npm-debug.log* |
||||
*.dat |
yarn-debug.log* |
||||
*.out |
yarn-error.log* |
||||
*.pid |
lerna-debug.log* |
||||
*.gz |
|
||||
|
# Diagnostic reports (https://nodejs.org/api/report.html) |
||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
||||
|
|
||||
tmp |
# Runtime data |
||||
pids |
pids |
||||
logs |
*.pid |
||||
results |
*.seed |
||||
|
*.pid.lock |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
*.lcov |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
npm-debug.log |
# TypeScript v1 declaration files |
||||
node_modules |
typings/ |
||||
.esm-cache |
|
||||
|
# TypeScript cache |
||||
|
*.tsbuildinfo |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Microbundle cache |
||||
|
.rpt2_cache/ |
||||
|
.rts2_cache_cjs/ |
||||
|
.rts2_cache_es/ |
||||
|
.rts2_cache_umd/ |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variables file |
||||
|
.env |
||||
|
.env.test |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
|
||||
|
# Next.js build output |
||||
|
.next |
||||
|
|
||||
|
# Nuxt.js build / generate output |
||||
|
.nuxt |
||||
dist |
dist |
||||
.idea |
|
||||
|
# Gatsby files |
||||
|
.cache/ |
||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js |
||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support |
||||
|
# public |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless/ |
||||
|
|
||||
|
# FuseBox cache |
||||
|
.fusebox/ |
||||
|
|
||||
|
# DynamoDB Local files |
||||
|
.dynamodb/ |
||||
|
|
||||
|
# TernJS port file |
||||
|
.tern-port |
||||
|
|
||||
|
# Stores VSCode versions used for testing VSCode extensions |
||||
|
.vscode-test |
||||
|
|
||||
|
# End of https://www.toptal.com/developers/gitignore/api/node |
||||
|
@ -1,13 +0,0 @@ |
|||||
module.exports = { |
|
||||
singleQuote: true, |
|
||||
trailingComma: 'es5', |
|
||||
proseWrap: 'always', |
|
||||
overrides: [ |
|
||||
{ |
|
||||
files: ['*.js', '*.ts'], |
|
||||
options: { |
|
||||
printWidth: 80, |
|
||||
}, |
|
||||
}, |
|
||||
], |
|
||||
}; |
|
@ -0,0 +1,16 @@ |
|||||
|
{ |
||||
|
"singleQuote": true, |
||||
|
"trailingComma": "all", |
||||
|
"proseWrap": "always", |
||||
|
"overrides": [ |
||||
|
{ |
||||
|
"files": [ |
||||
|
"*.js", |
||||
|
"*.ts" |
||||
|
], |
||||
|
"options": { |
||||
|
"printWidth": 80 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
{ |
||||
|
ssh: { |
||||
|
// user: 'username', // default user to use when ssh-ing |
||||
|
host: 'localhost', // Server to ssh to |
||||
|
auth: 'password', // shh authentication, method. Defaults to "password", you can use "publickey,password" instead' |
||||
|
// pass: "password", // Password to use when sshing |
||||
|
// key: "", // path to an optional client private key, connection will be password-less and insecure! |
||||
|
port: 22, // Port to ssh to |
||||
|
knownHosts: '/dev/null', // ssh knownHosts file to use |
||||
|
}, |
||||
|
server: { |
||||
|
base: '/wetty/', // URL base to serve resources from |
||||
|
port: 3000, // Port to listen on |
||||
|
host: '0.0.0.0', // address to listen on |
||||
|
title: 'WeTTy - The Web Terminal Emulator', // Page title |
||||
|
bypassHelmet: false, // Disable Helmet security checks |
||||
|
}, |
||||
|
|
||||
|
forceSSH: false, // Force sshing to local machine over login if running as root |
||||
|
command: 'login', // Command to run on server. Login will use ssh if connecting to different server |
||||
|
/* |
||||
|
ssl:{ |
||||
|
key: 'ssl.key', |
||||
|
cert: 'ssl.cert', |
||||
|
} |
||||
|
*/ |
||||
|
} |
@ -1,124 +0,0 @@ |
|||||
#! /usr/bin/env node
|
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, import/no-unresolved */ |
|
||||
|
|
||||
const yargs = require('yargs'); |
|
||||
const wetty = require('./dist').default; |
|
||||
|
|
||||
module.exports = wetty.wetty; |
|
||||
|
|
||||
/** |
|
||||
* Check if being run by cli or require |
|
||||
*/ |
|
||||
if (require.main === module) { |
|
||||
wetty.init( |
|
||||
yargs |
|
||||
.options({ |
|
||||
sslkey: { |
|
||||
demand: false, |
|
||||
type: 'string', |
|
||||
description: 'path to SSL key', |
|
||||
}, |
|
||||
sslcert: { |
|
||||
demand: false, |
|
||||
type: 'string', |
|
||||
description: 'path to SSL certificate', |
|
||||
}, |
|
||||
sshhost: { |
|
||||
demand: false, |
|
||||
description: 'ssh server host', |
|
||||
type: 'string', |
|
||||
default: process.env.SSHHOST || 'localhost', |
|
||||
}, |
|
||||
sshport: { |
|
||||
demand: false, |
|
||||
description: 'ssh server port', |
|
||||
type: 'number', |
|
||||
default: parseInt(process.env.SSHPORT, 10) || 22, |
|
||||
}, |
|
||||
sshuser: { |
|
||||
demand: false, |
|
||||
description: 'ssh user', |
|
||||
type: 'string', |
|
||||
default: process.env.SSHUSER || '', |
|
||||
}, |
|
||||
title: { |
|
||||
demand: false, |
|
||||
description: 'window title', |
|
||||
type: 'string', |
|
||||
default: process.env.TITLE || 'WeTTy - The Web Terminal Emulator', |
|
||||
}, |
|
||||
sshauth: { |
|
||||
demand: false, |
|
||||
description: |
|
||||
'defaults to "password", you can use "publickey,password" instead', |
|
||||
type: 'string', |
|
||||
default: process.env.SSHAUTH || 'password', |
|
||||
}, |
|
||||
sshpass: { |
|
||||
demand: false, |
|
||||
description: 'ssh password', |
|
||||
type: 'string', |
|
||||
default: process.env.SSHPASS || undefined, |
|
||||
}, |
|
||||
sshkey: { |
|
||||
demand: false, |
|
||||
description: |
|
||||
'path to an optional client private key (connection will be password-less and insecure!)', |
|
||||
type: 'string', |
|
||||
default: process.env.SSHKEY || undefined, |
|
||||
}, |
|
||||
forcessh: { |
|
||||
demand: false, |
|
||||
description: 'Connecting through ssh even if running as root', |
|
||||
type: 'boolean', |
|
||||
default: process.env.FORCESSH || false |
|
||||
}, |
|
||||
knownhosts: { |
|
||||
demand: false, |
|
||||
description: 'path to known hosts file', |
|
||||
type: 'string', |
|
||||
default: process.env.KNOWNHOSTS || '/dev/null', |
|
||||
}, |
|
||||
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, |
|
||||
}, |
|
||||
host: { |
|
||||
demand: false, |
|
||||
description: 'wetty listen host', |
|
||||
default: '0.0.0.0', |
|
||||
type: 'string', |
|
||||
}, |
|
||||
command: { |
|
||||
demand: false, |
|
||||
alias: 'c', |
|
||||
description: 'command to run in shell', |
|
||||
type: 'string', |
|
||||
default: process.env.COMMAND || 'login', |
|
||||
}, |
|
||||
bypasshelmet: { |
|
||||
demand: false, |
|
||||
description: 'disable helmet from placing security restrictions', |
|
||||
type: 'boolean', |
|
||||
default: false, |
|
||||
}, |
|
||||
help: { |
|
||||
demand: false, |
|
||||
alias: 'h', |
|
||||
type: 'boolean', |
|
||||
description: 'Print help message', |
|
||||
}, |
|
||||
}) |
|
||||
.boolean('allow_discovery').argv |
|
||||
); |
|
||||
} |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -0,0 +1,52 @@ |
|||||
|
@use './variables'; |
||||
|
|
||||
|
#options { |
||||
|
height: 16px; |
||||
|
position: absolute; |
||||
|
right: 1em; |
||||
|
top: 1em; |
||||
|
width: 16px; |
||||
|
z-index: 20; |
||||
|
|
||||
|
.toggler { |
||||
|
color: variables.$lgrey; |
||||
|
display: inline-block; |
||||
|
font-size: 16px; |
||||
|
position: absolute; |
||||
|
right: 1em; |
||||
|
top: 0; |
||||
|
z-index: 20; |
||||
|
|
||||
|
:hover { |
||||
|
color: variables.$white; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.editor { |
||||
|
background-color: rgba(0, 0, 0, 0.85); |
||||
|
border-color: rgba(255, 255, 255, 0.25); |
||||
|
border-radius: 0.3em; |
||||
|
color: #eee; |
||||
|
display: none; |
||||
|
font-size: 24px; |
||||
|
height: 100%; |
||||
|
padding: 0.5em; |
||||
|
position: relative; |
||||
|
right: 2em; |
||||
|
top: 1em; |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#options.opened { |
||||
|
height: 50%; |
||||
|
width: 50%; |
||||
|
|
||||
|
.editor { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
.error { |
||||
|
color: red; |
||||
|
} |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
@use './variables'; |
||||
|
|
||||
|
#overlay { |
||||
|
background-color: variables.$grey; |
||||
|
display: none; |
||||
|
height: 100%; |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
z-index: 100; |
||||
|
|
||||
|
.error { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
|
||||
|
#msg { |
||||
|
align-self: center; |
||||
|
color: variables.$white; |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
align-self: center; |
||||
|
margin: 16px; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
@use 'xterm/css/xterm.css'; |
||||
|
@use 'toastify-js/src/toastify.css'; |
||||
|
@use './variables'; |
||||
|
@use './overlay'; |
||||
|
@use './options'; |
||||
|
@use './terminal'; |
||||
|
|
||||
|
html, |
||||
|
body { |
||||
|
background-color: variables.$black; |
||||
|
height: 100%; |
||||
|
margin: 0; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.toastify { |
||||
|
border-radius: 0; |
||||
|
color: variables.$black; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.xterm { |
||||
|
.xterm-viewport { |
||||
|
overflow-y: hidden; |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
#terminal { |
||||
|
display: flex; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
$black: #000; |
||||
|
$grey: rgba(0, 0, 0, 0.75); |
||||
|
$white: #fff; |
||||
|
$lgrey: #ccc; |
@ -0,0 +1,5 @@ |
|||||
|
caches.keys().then(cacheNames => { |
||||
|
cacheNames.forEach(cacheName => { |
||||
|
caches.delete(cacheName); |
||||
|
}); |
||||
|
}); |
@ -1,11 +0,0 @@ |
|||||
import { isNull, isUndefined } from 'lodash'; |
|
||||
import verifyPrompt from './verify'; |
|
||||
import { overlay } from './elements'; |
|
||||
|
|
||||
export default function disconnect(reason: string): void { |
|
||||
if (isNull(overlay)) return; |
|
||||
overlay.style.display = 'block'; |
|
||||
const msg = document.getElementById('msg'); |
|
||||
if (!isUndefined(reason) && !isNull(msg)) msg.innerHTML = reason; |
|
||||
window.removeEventListener('beforeunload', verifyPrompt, false); |
|
||||
} |
|
@ -1,160 +0,0 @@ |
|||||
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 { socket } from './socket'; |
|
||||
import { overlay, terminal } from './elements'; |
|
||||
import { FileDownloader } from './download'; |
|
||||
import verifyPrompt from './verify'; |
|
||||
import disconnect from './disconnect'; |
|
||||
import mobileKeyboard from './mobile'; |
|
||||
import loadOptions from './options'; |
|
||||
import { copySelected, copyShortcut } from './copyToClipboard'; |
|
||||
import './wetty.scss'; |
|
||||
import './favicon.ico'; |
|
||||
|
|
||||
// Setup for fontawesome
|
|
||||
library.add(faCogs); |
|
||||
dom.watch(); |
|
||||
|
|
||||
socket.on('connect', () => { |
|
||||
const term = new Terminal(); |
|
||||
if (isNull(terminal)) return; |
|
||||
const fitAddon = new FitAddon(); |
|
||||
term.loadAddon(fitAddon); |
|
||||
term.open(terminal); |
|
||||
const resize = (): void => { |
|
||||
fitAddon.fit(); |
|
||||
socket.emit('resize', { cols: term.cols, rows: term.rows }); |
|
||||
}; |
|
||||
|
|
||||
const options = loadOptions(); |
|
||||
Object.entries(options).forEach(([key, value]) => { |
|
||||
term.setOption(key, value); |
|
||||
}); |
|
||||
const code = JSON.stringify(options, null, 2); |
|
||||
const editor = document.querySelector('#options .editor'); |
|
||||
if (!isNull(editor)) { |
|
||||
editor.value = code; |
|
||||
editor.addEventListener('keyup', () => { |
|
||||
try { |
|
||||
const updated = JSON.parse(editor.value); |
|
||||
const updatedCode = JSON.stringify(updated, null, 2); |
|
||||
editor.value = updatedCode; |
|
||||
editor.classList.remove('error'); |
|
||||
localStorage.options = updatedCode; |
|
||||
Object.keys(updated).forEach(key => { |
|
||||
const value = updated[key]; |
|
||||
term.setOption(key, value); |
|
||||
}); |
|
||||
resize(); |
|
||||
} catch { |
|
||||
// skip
|
|
||||
editor.classList.add('error'); |
|
||||
} |
|
||||
}); |
|
||||
const toggle = document.querySelector('#options .toggler'); |
|
||||
const optionsElem = document.getElementById('options'); |
|
||||
if (!isNull(toggle) && !isNull(optionsElem)) { |
|
||||
toggle.addEventListener('click', e => { |
|
||||
optionsElem.classList.toggle('opened'); |
|
||||
e.preventDefault(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
if (!isNull(overlay)) overlay.style.display = 'none'; |
|
||||
window.addEventListener('beforeunload', verifyPrompt, false); |
|
||||
|
|
||||
term.attachCustomKeyEventHandler(copyShortcut); |
|
||||
|
|
||||
document.addEventListener( |
|
||||
'mouseup', |
|
||||
() => { |
|
||||
if (term.hasSelection()) copySelected(term.getSelection()); |
|
||||
}, |
|
||||
false |
|
||||
); |
|
||||
|
|
||||
window.onresize = resize; |
|
||||
resize(); |
|
||||
term.focus(); |
|
||||
mobileKeyboard(); |
|
||||
|
|
||||
const fileDownloader = new FileDownloader((bufferCharacters: string) => { |
|
||||
let fileCharacters = bufferCharacters; |
|
||||
// Try to decode it as base64, if it fails we assume it's not base64
|
|
||||
try { |
|
||||
fileCharacters = window.atob(fileCharacters); |
|
||||
} catch (err) { |
|
||||
// Assuming it's not base64...
|
|
||||
} |
|
||||
|
|
||||
const bytes = new Uint8Array(fileCharacters.length); |
|
||||
for (let i = 0; i < fileCharacters.length; i += 1) { |
|
||||
bytes[i] = fileCharacters.charCodeAt(i); |
|
||||
} |
|
||||
|
|
||||
let mimeType = 'application/octet-stream'; |
|
||||
let fileExt = ''; |
|
||||
const typeData = fileType(bytes); |
|
||||
if (typeData) { |
|
||||
mimeType = typeData.mime; |
|
||||
fileExt = typeData.ext; |
|
||||
} |
|
||||
// Check if the buffer is ASCII
|
|
||||
// Ref: https://stackoverflow.com/a/14313213
|
|
||||
// eslint-disable-next-line no-control-regex
|
|
||||
else if (/^[\x00-\xFF]*$/.test(fileCharacters)) { |
|
||||
mimeType = 'text/plain'; |
|
||||
fileExt = 'txt'; |
|
||||
} |
|
||||
const fileName = `file-${new Date() |
|
||||
.toISOString() |
|
||||
.split('.')[0] |
|
||||
.replace(/-/g, '') |
|
||||
.replace('T', '') |
|
||||
.replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
|
|
||||
|
|
||||
const blob = new Blob([new Uint8Array(bytes.buffer)], { |
|
||||
type: mimeType, |
|
||||
}); |
|
||||
const blobUrl = URL.createObjectURL(blob); |
|
||||
|
|
||||
Toastify({ |
|
||||
text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`, |
|
||||
duration: 10000, |
|
||||
newWindow: true, |
|
||||
gravity: 'bottom', |
|
||||
position: 'right', |
|
||||
backgroundColor: '#fff', |
|
||||
stopOnFocus: true, |
|
||||
}).showToast(); |
|
||||
}); |
|
||||
|
|
||||
term.onData(data => { |
|
||||
socket.emit('input', data); |
|
||||
}); |
|
||||
term.onResize(size => { |
|
||||
socket.emit('resize', size); |
|
||||
}); |
|
||||
socket |
|
||||
.on('data', (data: string) => { |
|
||||
const remainingData = fileDownloader.buffer(data); |
|
||||
if (remainingData) { |
|
||||
term.write(remainingData); |
|
||||
} |
|
||||
}) |
|
||||
.on('login', () => { |
|
||||
term.writeln(''); |
|
||||
resize(); |
|
||||
}) |
|
||||
.on('logout', disconnect) |
|
||||
.on('disconnect', disconnect) |
|
||||
.on('error', (err: string | null) => { |
|
||||
if (err) disconnect(err); |
|
||||
}); |
|
||||
}); |
|
@ -1,2 +1,5 @@ |
|||||
export const overlay = document.getElementById('overlay'); |
export const overlay = document.getElementById('overlay'); |
||||
export const terminal = document.getElementById('terminal'); |
export const terminal = document.getElementById('terminal'); |
||||
|
export const editor = document.querySelector( |
||||
|
'#options .editor', |
||||
|
) as HTMLInputElement; |
@ -0,0 +1,4 @@ |
|||||
|
export function verifyPrompt(e: { returnValue: string }): string { |
||||
|
e.returnValue = 'Are you sure?'; |
||||
|
return e.returnValue; |
||||
|
} |
@ -1,195 +0,0 @@ |
|||||
/* eslint-disable */ |
|
||||
|
|
||||
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'; |
|
||||
let fileDownloader: any; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
fileDownloader = new FileDownloader(() => { }, FILE_BEGIN, FILE_END); |
|
||||
}); |
|
||||
|
|
||||
afterEach(() => { |
|
||||
sinon.restore(); |
|
||||
}); |
|
||||
|
|
||||
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}`) |
|
||||
).to.equal('DATA AT THE LEFT'); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
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`) |
|
||||
).to.equal('DATA AT THE RIGHT'); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should return data before and after file markers', () => { |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
expect( |
|
||||
fileDownloader.buffer( |
|
||||
`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'); |
|
||||
expect(fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE`)).to.equal( |
|
||||
'DATA AT THE LEFT' |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
it('should return data after an ending marker found', () => { |
|
||||
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' |
|
||||
); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file begin marker sequence on two calls', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('BEG')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('INFILEEND')).to.equal(''); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file begin marker sequence on n calls', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('B')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('E')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('G')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('I')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('NFILE' + 'END')).to.equal(''); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file begin marker sequence with data on the left and right on multiple calls', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
|
||||
'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' |
|
||||
); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file begin marker sequence then handle false positive', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
|
||||
'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' |
|
||||
); |
|
||||
expect(onCompleteFileStub.called).to.be.false; |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file end marker sequence on two calls', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const mockFilePart1 = 'DATA AT THE LEFTBEGINFILEE'; |
|
||||
const mockFilePart2 = 'NDDATA AT THE RIGHT'; |
|
||||
|
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
expect(fileDownloader.buffer(mockFilePart1)).to.equal('DATA AT THE LEFT'); |
|
||||
expect(fileDownloader.buffer(mockFilePart2)).to.equal('DATA AT THE RIGHT'); |
|
||||
|
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should buffer across incomplete file end and file begin marker sequence with data on the left and right on multiple calls', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'BE')).to.equal( |
|
||||
'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' |
|
||||
); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
|
||||
}); |
|
||||
|
|
||||
it('should be able to handle multiple files', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect( |
|
||||
fileDownloader.buffer( |
|
||||
'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'); |
|
||||
|
|
||||
expect(fileDownloader.buffer('FILE2')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('E')).to.equal(''); |
|
||||
expect(fileDownloader.buffer('NDRIGHT')).to.equal('RIGHT'); |
|
||||
expect(onCompleteFileStub.calledTwice).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(1).args[0]).to.equal('FILE2'); |
|
||||
}); |
|
||||
|
|
||||
it('should be able to handle multiple files with an ending marker', () => { |
|
||||
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
|
||||
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
|
||||
|
|
||||
expect( |
|
||||
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') |
|
||||
).to.equal('SECOND DATA'); |
|
||||
expect(onCompleteFileStub.calledOnce).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); |
|
||||
expect(fileDownloader.buffer('D')).to.equal(''); |
|
||||
expect(onCompleteFileStub.calledTwice).to.be.true; |
|
||||
expect(onCompleteFileStub.getCall(1).args[0]).to.equal('FILE2'); |
|
||||
}); |
|
||||
}); |
|
@ -1,4 +0,0 @@ |
|||||
export default function verifyPrompt(e: { returnValue: string }): string { |
|
||||
e.returnValue = 'Are you sure?'; |
|
||||
return e.returnValue; |
|
||||
} |
|
@ -1,107 +0,0 @@ |
|||||
@import '~xterm/css/xterm'; |
|
||||
@import '~toastify-js/src/toastify.css'; |
|
||||
|
|
||||
$black: #000; |
|
||||
$grey: rgba(0, 0, 0, 0.75); |
|
||||
$white: #fff; |
|
||||
$lgrey: #ccc; |
|
||||
|
|
||||
html, |
|
||||
body { |
|
||||
background-color: $black; |
|
||||
height: 100%; |
|
||||
margin: 0; |
|
||||
overflow: hidden; |
|
||||
|
|
||||
#overlay { |
|
||||
background-color: $grey; |
|
||||
display: none; |
|
||||
height: 100%; |
|
||||
position: absolute; |
|
||||
width: 100%; |
|
||||
z-index: 100; |
|
||||
|
|
||||
.error { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
height: 100%; |
|
||||
justify-content: center; |
|
||||
width: 100%; |
|
||||
|
|
||||
#msg { |
|
||||
align-self: center; |
|
||||
color: $white; |
|
||||
} |
|
||||
|
|
||||
input { |
|
||||
align-self: center; |
|
||||
margin: 16px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#terminal { |
|
||||
display: flex; |
|
||||
height: 100%; |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
#options { |
|
||||
position: absolute; |
|
||||
top: 1em; |
|
||||
right: 1em; |
|
||||
z-index: 20; |
|
||||
height: 16px; |
|
||||
width: 16px; |
|
||||
|
|
||||
a.toggler { |
|
||||
display: inline-block; |
|
||||
position: absolute; |
|
||||
right: 1em; |
|
||||
top: 0em; |
|
||||
font-size: 16px; |
|
||||
color: $lgrey; |
|
||||
z-index: 20; |
|
||||
|
|
||||
: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); |
|
||||
display: none; |
|
||||
position: relative; |
|
||||
height: 100%; |
|
||||
width: 100%; |
|
||||
top: 1em; |
|
||||
right: 2em; |
|
||||
color: #eee; |
|
||||
font-size: 24px; |
|
||||
} |
|
||||
.editor.error { |
|
||||
color: red; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
#options.opened { |
|
||||
height: 50%; |
|
||||
width: 50%; |
|
||||
.editor { |
|
||||
display: flex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.toastify { |
|
||||
border-radius: 0; |
|
||||
color: $black; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.xterm .xterm-viewport { |
|
||||
overflow-y: hidden; |
|
||||
} |
|
@ -0,0 +1,51 @@ |
|||||
|
import _ from 'lodash'; |
||||
|
import { dom, library } from '@fortawesome/fontawesome-svg-core'; |
||||
|
import { faCogs } from '@fortawesome/free-solid-svg-icons'; |
||||
|
|
||||
|
import { FileDownloader } from './wetty/download.js'; |
||||
|
import { disconnect } from './wetty/disconnect.js'; |
||||
|
import { mobileKeyboard } from './wetty/mobile.js'; |
||||
|
import { overlay } from './shared/elements.js'; |
||||
|
import { socket } from './wetty/socket.js'; |
||||
|
import { verifyPrompt } from './shared/verify.js'; |
||||
|
import { terminal } from './wetty/term.js'; |
||||
|
|
||||
|
// Setup for fontawesome
|
||||
|
library.add(faCogs); |
||||
|
dom.watch(); |
||||
|
|
||||
|
socket.on('connect', () => { |
||||
|
const term = terminal(socket); |
||||
|
if (_.isUndefined(term)) return; |
||||
|
|
||||
|
if (!_.isNull(overlay)) overlay.style.display = 'none'; |
||||
|
window.addEventListener('beforeunload', verifyPrompt, false); |
||||
|
|
||||
|
term.resizeTerm(); |
||||
|
term.focus(); |
||||
|
mobileKeyboard(); |
||||
|
const fileDownloader = new FileDownloader(); |
||||
|
|
||||
|
term.onData((data: string) => { |
||||
|
socket.emit('input', data); |
||||
|
}); |
||||
|
term.onResize((size: { cols: number; rows: number }) => { |
||||
|
socket.emit('resize', size); |
||||
|
}); |
||||
|
socket |
||||
|
.on('data', (data: string) => { |
||||
|
const remainingData = fileDownloader.buffer(data); |
||||
|
if (remainingData) { |
||||
|
term.write(remainingData); |
||||
|
} |
||||
|
}) |
||||
|
.on('login', () => { |
||||
|
term.writeln(''); |
||||
|
term.resizeTerm(); |
||||
|
}) |
||||
|
.on('logout', disconnect) |
||||
|
.on('disconnect', disconnect) |
||||
|
.on('error', (err: string | null) => { |
||||
|
if (err) disconnect(err); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,11 @@ |
|||||
|
import _ from 'lodash'; |
||||
|
import { verifyPrompt } from '../shared/verify.js'; |
||||
|
import { overlay } from '../shared/elements.js'; |
||||
|
|
||||
|
export function disconnect(reason: string): void { |
||||
|
if (_.isNull(overlay)) return; |
||||
|
overlay.style.display = 'block'; |
||||
|
const msg = document.getElementById('msg'); |
||||
|
if (!_.isUndefined(reason) && !_.isNull(msg)) msg.innerHTML = reason; |
||||
|
window.removeEventListener('beforeunload', verifyPrompt, false); |
||||
|
} |
@ -0,0 +1,236 @@ |
|||||
|
/* eslint-disable */ |
||||
|
|
||||
|
import { expect } from 'chai'; |
||||
|
import 'mocha'; |
||||
|
import * as sinon from 'sinon'; |
||||
|
|
||||
|
import { JSDOM } from 'jsdom'; |
||||
|
import { FileDownloader } from './download'; |
||||
|
|
||||
|
describe('FileDownloader', () => { |
||||
|
const FILE_BEGIN = 'BEGIN'; |
||||
|
const FILE_END = 'END'; |
||||
|
let fileDownloader: any; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
const { window } = new JSDOM(`...`); |
||||
|
global.document = window.document; |
||||
|
fileDownloader = new FileDownloader(() => {}, FILE_BEGIN, FILE_END); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
sinon.restore(); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before file markers', () => { |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
expect( |
||||
|
fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}`), |
||||
|
).to.equal('DATA AT THE LEFT'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data after file markers', () => { |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
expect( |
||||
|
fileDownloader.buffer(`${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`), |
||||
|
).to.equal('DATA AT THE RIGHT'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before and after file markers', () => { |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
expect( |
||||
|
fileDownloader.buffer( |
||||
|
`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`, |
||||
|
), |
||||
|
).to.equal('DATA AT THE LEFTDATA AT THE RIGHT'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before a beginning marker found', () => { |
||||
|
sinon.stub(fileDownloader, 'onCompleteFileCallback'); |
||||
|
expect(fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE`)).to.equal( |
||||
|
'DATA AT THE LEFT', |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should return data after an ending marker found', () => { |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
expect(fileDownloader.buffer(`${FILE_BEGIN}FI`)).to.equal(''); |
||||
|
expect(fileDownloader.buffer(`LE${FILE_END}DATA AT THE RIGHT`)).to.equal( |
||||
|
'DATA AT THE RIGHT', |
||||
|
); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence on two calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect(fileDownloader.buffer('BEG')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('INFILEEND')).to.equal(''); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence on n calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect(fileDownloader.buffer('B')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('G')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('I')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NFILE' + 'END')).to.equal(''); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence with data on the left and right on multiple calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
||||
|
'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', |
||||
|
); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence then handle false positive', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
||||
|
'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', |
||||
|
); |
||||
|
expect(onCompleteFileCallbackStub.called).to.be.false; |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file end marker sequence on two calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const mockFilePart1 = 'DATA AT THE LEFTBEGINFILEE'; |
||||
|
const mockFilePart2 = 'NDDATA AT THE RIGHT'; |
||||
|
|
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
expect(fileDownloader.buffer(mockFilePart1)).to.equal('DATA AT THE LEFT'); |
||||
|
expect(fileDownloader.buffer(mockFilePart2)).to.equal('DATA AT THE RIGHT'); |
||||
|
|
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file end and file begin marker sequence with data on the left and right on multiple calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'BE')).to.equal( |
||||
|
'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', |
||||
|
); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to handle multiple files', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect( |
||||
|
fileDownloader.buffer( |
||||
|
'DATA AT THE LEFT' + |
||||
|
'BEGIN' + |
||||
|
'FILE1' + |
||||
|
'END' + |
||||
|
'SECOND DATA' + |
||||
|
'BEGIN', |
||||
|
), |
||||
|
).to.equal('DATA AT THE LEFT' + 'SECOND DATA'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE1'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('FILE2')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NDRIGHT')).to.equal('RIGHT'); |
||||
|
expect(onCompleteFileCallbackStub.calledTwice).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(1).args[0]).to.equal('FILE2'); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to handle multiple files with an ending marker', () => { |
||||
|
fileDownloader = new FileDownloader(() => {}, 'BEGIN', 'END'); |
||||
|
const onCompleteFileCallbackStub = sinon.stub( |
||||
|
fileDownloader, |
||||
|
'onCompleteFileCallback', |
||||
|
); |
||||
|
|
||||
|
expect( |
||||
|
fileDownloader.buffer('DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'EN'), |
||||
|
).to.equal('DATA AT THE LEFT'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.false; |
||||
|
expect( |
||||
|
fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN'), |
||||
|
).to.equal('SECOND DATA'); |
||||
|
expect(onCompleteFileCallbackStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(0).args[0]).to.equal('FILE1'); |
||||
|
expect(fileDownloader.buffer('D')).to.equal(''); |
||||
|
expect(onCompleteFileCallbackStub.calledTwice).to.be.true; |
||||
|
expect(onCompleteFileCallbackStub.getCall(1).args[0]).to.equal('FILE2'); |
||||
|
}); |
||||
|
}); |
@ -1,8 +1,8 @@ |
|||||
import { isNull } from 'lodash'; |
import _ from 'lodash'; |
||||
|
|
||||
export default function mobileKeyboard(): void { |
export function mobileKeyboard(): void { |
||||
const [screen] = document.getElementsByClassName('xterm-screen'); |
const [screen] = document.getElementsByClassName('xterm-screen'); |
||||
if (isNull(screen)) return; |
if (_.isNull(screen)) return; |
||||
screen.setAttribute('contenteditable', 'true'); |
screen.setAttribute('contenteditable', 'true'); |
||||
screen.setAttribute('spellcheck', 'false'); |
screen.setAttribute('spellcheck', 'false'); |
||||
screen.setAttribute('autocorrect', 'false'); |
screen.setAttribute('autocorrect', 'false'); |
@ -0,0 +1,5 @@ |
|||||
|
import { Terminal } from 'xterm'; |
||||
|
|
||||
|
export class Term extends Terminal { |
||||
|
resizeTerm(): void {} |
||||
|
} |
@ -1,4 +1,4 @@ |
|||||
import * as io from 'socket.io-client'; |
import io from 'socket.io-client'; |
||||
|
|
||||
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(/\/*$/, ''); |
@ -0,0 +1,24 @@ |
|||||
|
import type { Socket } from 'socket.io-client'; |
||||
|
import _ from 'lodash'; |
||||
|
import { FitAddon } from 'xterm-addon-fit'; |
||||
|
import { Terminal } from 'xterm'; |
||||
|
|
||||
|
import type { Term } from './shared/type'; |
||||
|
import { configureTerm } from './term/confiruragtion.js'; |
||||
|
import { terminal as termElement } from '../shared/elements.js'; |
||||
|
|
||||
|
export function terminal(socket: typeof Socket): Term | undefined { |
||||
|
const term = new Terminal() as Term; |
||||
|
if (_.isNull(termElement)) return; |
||||
|
const fitAddon = new FitAddon(); |
||||
|
term.loadAddon(fitAddon); |
||||
|
term.open(termElement); |
||||
|
term.resizeTerm = () => { |
||||
|
fitAddon.fit(); |
||||
|
socket.emit('resize', { cols: term.cols, rows: term.rows }); |
||||
|
}; |
||||
|
configureTerm(term); |
||||
|
window.onresize = term.resizeTerm; |
||||
|
|
||||
|
return term; |
||||
|
} |
@ -0,0 +1,37 @@ |
|||||
|
import _ from 'lodash'; |
||||
|
|
||||
|
import type { Term } from '../shared/type'; |
||||
|
import { copySelected, copyShortcut } from './confiruragtion/clipboard'; |
||||
|
import { onInput } from './confiruragtion/editor'; |
||||
|
import { editor } from '../../shared/elements'; |
||||
|
import { loadOptions } from './confiruragtion/load'; |
||||
|
|
||||
|
export function configureTerm(term: Term): void { |
||||
|
const options = loadOptions(); |
||||
|
Object.entries(options).forEach(([key, value]) => { |
||||
|
term.setOption(key, value); |
||||
|
}); |
||||
|
const config = JSON.stringify(options, null, 2); |
||||
|
if (!_.isNull(editor)) { |
||||
|
editor.value = config; |
||||
|
editor.addEventListener('keyup', onInput(term)); |
||||
|
const toggle = document.querySelector('#options .toggler'); |
||||
|
const optionsElem = document.getElementById('options'); |
||||
|
if (!_.isNull(toggle) && !_.isNull(optionsElem)) { |
||||
|
toggle.addEventListener('click', e => { |
||||
|
optionsElem.classList.toggle('opened'); |
||||
|
e.preventDefault(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
term.attachCustomKeyEventHandler(copyShortcut); |
||||
|
|
||||
|
document.addEventListener( |
||||
|
'mouseup', |
||||
|
() => { |
||||
|
if (term.hasSelection()) copySelected(term.getSelection()); |
||||
|
}, |
||||
|
false, |
||||
|
); |
||||
|
} |
@ -1,7 +1,11 @@ |
|||||
// NOTE text selection on double click or select
|
/** |
||||
|
Copy text selection to clipboard on double click or select |
||||
|
@param text - the selected text to copy |
||||
|
@returns boolean to indicate success or failure |
||||
|
*/ |
||||
export function copySelected(text: string): boolean { |
export function copySelected(text: string): boolean { |
||||
if (window.clipboardData?.setData) { |
if ((window as any).clipboardData?.setData) { |
||||
window.clipboardData.setData('Text', text); |
(window as any).clipboardData.setData('Text', text); |
||||
return true; |
return true; |
||||
} |
} |
||||
if ( |
if ( |
@ -0,0 +1,23 @@ |
|||||
|
import JSON5 from 'json5'; |
||||
|
|
||||
|
import type { Term } from '../../shared/type'; |
||||
|
import { editor } from '../../../shared/elements'; |
||||
|
|
||||
|
export const onInput = (term: Term) => (): void => { |
||||
|
try { |
||||
|
const updated = JSON5.parse(editor.value); |
||||
|
const updatedConf = JSON.stringify(updated, null, 2); |
||||
|
if (localStorage.options === updatedConf) return; |
||||
|
Object.keys(updated).forEach(key => { |
||||
|
const value = updated[key]; |
||||
|
term.setOption(key, value); |
||||
|
}); |
||||
|
term.resizeTerm(); |
||||
|
editor.value = updatedConf; |
||||
|
editor.classList.remove('error'); |
||||
|
localStorage.options = updatedConf; |
||||
|
} catch { |
||||
|
// skip
|
||||
|
editor.classList.add('error'); |
||||
|
} |
||||
|
}; |
@ -1,9 +1,9 @@ |
|||||
import { isUndefined } from 'lodash'; |
import _ from 'lodash'; |
||||
|
|
||||
export default function loadOptions(): object { |
export function loadOptions(): object { |
||||
const defaultOptions = { fontSize: 14 }; |
const defaultOptions = { fontSize: 14 }; |
||||
try { |
try { |
||||
return isUndefined(localStorage.options) |
return _.isUndefined(localStorage.options) |
||||
? defaultOptions |
? defaultOptions |
||||
: JSON.parse(localStorage.options); |
: JSON.parse(localStorage.options); |
||||
} catch { |
} catch { |
@ -0,0 +1,106 @@ |
|||||
|
/** |
||||
|
* Create WeTTY server |
||||
|
* @module WeTTy |
||||
|
*/ |
||||
|
import yargs from 'yargs'; |
||||
|
import { logger } from './shared/logger.js'; |
||||
|
import { start } from './server.js'; |
||||
|
import { loadConfigFile, mergeCliConf } from './shared/config.js'; |
||||
|
|
||||
|
const opts = yargs |
||||
|
.options('conf', { |
||||
|
type: 'string', |
||||
|
description: 'config file to load config from', |
||||
|
}) |
||||
|
.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', |
||||
|
}) |
||||
|
.option('ssh-port', { |
||||
|
description: 'ssh server port', |
||||
|
type: 'number', |
||||
|
}) |
||||
|
.option('ssh-user', { |
||||
|
description: 'ssh user', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('title', { |
||||
|
description: 'window title', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('ssh-auth', { |
||||
|
description: |
||||
|
'defaults to "password", you can use "publickey,password" instead', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('ssh-pass', { |
||||
|
description: 'ssh password', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('ssh-key', { |
||||
|
demand: false, |
||||
|
description: |
||||
|
'path to an optional client private key (connection will be password-less and insecure!)', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('force-ssh', { |
||||
|
description: 'Connecting through ssh even if running as root', |
||||
|
type: 'boolean', |
||||
|
}) |
||||
|
.option('known-hosts', { |
||||
|
description: 'path to known hosts file', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('base', { |
||||
|
alias: 'b', |
||||
|
description: 'base path to wetty', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('port', { |
||||
|
alias: 'p', |
||||
|
description: 'wetty listen port', |
||||
|
type: 'number', |
||||
|
}) |
||||
|
.option('host', { |
||||
|
description: 'wetty listen host', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('command', { |
||||
|
alias: 'c', |
||||
|
description: 'command to run in shell', |
||||
|
type: 'string', |
||||
|
}) |
||||
|
.option('allow-iframe', { |
||||
|
description: |
||||
|
'Allow wetty to be embedded in an iframe, defaults to allowing same origin', |
||||
|
type: 'boolean', |
||||
|
}) |
||||
|
.option('help', { |
||||
|
alias: 'h', |
||||
|
type: 'boolean', |
||||
|
description: 'Print help message', |
||||
|
}) |
||||
|
.boolean('allow_discovery').argv; |
||||
|
|
||||
|
if (!opts.help) { |
||||
|
loadConfigFile(opts.conf) |
||||
|
.then(config => mergeCliConf(opts, config)) |
||||
|
.then(conf => |
||||
|
start(conf.ssh, conf.server, conf.command, conf.forceSSH, conf.ssl), |
||||
|
) |
||||
|
.catch((err: Error) => { |
||||
|
logger.error(err); |
||||
|
process.exitCode = 1; |
||||
|
}); |
||||
|
} else { |
||||
|
yargs.showHelp(); |
||||
|
process.exitCode = 0; |
||||
|
} |
@ -0,0 +1,73 @@ |
|||||
|
/** |
||||
|
* Create WeTTY server |
||||
|
* @module WeTTy |
||||
|
*/ |
||||
|
import type SocketIO from 'socket.io'; |
||||
|
import type { SSH, SSL, Server } from './shared/interfaces.js'; |
||||
|
import { getCommand } from './server/command.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 './shared/defaults.js'; |
||||
|
|
||||
|
/** |
||||
|
* Starts WeTTy Server |
||||
|
* @name startServer |
||||
|
* @returns Promise that resolves SocketIO server |
||||
|
*/ |
||||
|
export async function start( |
||||
|
ssh: SSH = sshDefault, |
||||
|
serverConf: Server = serverDefault, |
||||
|
command: string = defaultCommand, |
||||
|
forcessh: boolean = forceSSHDefault, |
||||
|
ssl?: SSL, |
||||
|
): Promise<SocketIO.Server> { |
||||
|
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 io = await server(serverConf, ssl); |
||||
|
/** |
||||
|
* Wetty server connected too |
||||
|
* @fires WeTTy#connnection |
||||
|
*/ |
||||
|
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(' '), |
||||
|
}); |
||||
|
|
||||
|
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); |
||||
|
} catch (error) { |
||||
|
logger.info('Disconnect signal sent', { err: error }); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
return io; |
||||
|
} |
@ -1,18 +0,0 @@ |
|||||
import * as yargs from 'yargs'; |
|
||||
import { logger } from '../utils'; |
|
||||
import WeTTy from '../wetty'; |
|
||||
import { CLI } from './options'; |
|
||||
import { unWrapArgs } from './parseArgs'; |
|
||||
|
|
||||
export default function init(opts: CLI): void { |
|
||||
if (!opts.help) { |
|
||||
const { ssh, server, command, forcessh, ssl } = unWrapArgs(opts); |
|
||||
WeTTy(ssh, server, command, forcessh, ssl).catch(err => { |
|
||||
logger.error(err); |
|
||||
process.exitCode = 1; |
|
||||
}); |
|
||||
} else { |
|
||||
yargs.showHelp(); |
|
||||
process.exitCode = 0; |
|
||||
} |
|
||||
} |
|
@ -1,22 +0,0 @@ |
|||||
export interface Options { |
|
||||
sshhost: string; |
|
||||
sshport: number; |
|
||||
sshuser: string; |
|
||||
sshauth: string; |
|
||||
sshkey?: string; |
|
||||
sshpass?: string; |
|
||||
knownhosts: string; |
|
||||
sslkey?: string; |
|
||||
sslcert?: string; |
|
||||
base: string; |
|
||||
host: string; |
|
||||
port: number; |
|
||||
title: string; |
|
||||
command?: string; |
|
||||
forcessh?: boolean; |
|
||||
bypasshelmet?: boolean; |
|
||||
} |
|
||||
|
|
||||
export interface CLI extends Options { |
|
||||
help: boolean; |
|
||||
} |
|
@ -1,32 +0,0 @@ |
|||||
import { isUndefined } from 'lodash'; |
|
||||
import { SSH, SSL, Server } from '../interfaces'; |
|
||||
import { Options } from './options'; |
|
||||
|
|
||||
export function unWrapArgs( |
|
||||
args: Options |
|
||||
): { ssh: SSH; server: Server; command?: string; forcessh?: boolean; ssl?: SSL } { |
|
||||
return { |
|
||||
ssh: { |
|
||||
user: args.sshuser, |
|
||||
host: args.sshhost, |
|
||||
auth: args.sshauth, |
|
||||
port: args.sshport, |
|
||||
pass: args.sshpass, |
|
||||
key: args.sshkey, |
|
||||
knownhosts: args.knownhosts, |
|
||||
}, |
|
||||
server: { |
|
||||
base: args.base, |
|
||||
host: args.host, |
|
||||
port: args.port, |
|
||||
title: args.title, |
|
||||
bypasshelmet: args.bypasshelmet || false, |
|
||||
}, |
|
||||
command: args.command, |
|
||||
forcessh: args.forcessh, |
|
||||
ssl: |
|
||||
isUndefined(args.sslkey) || isUndefined(args.sslcert) |
|
||||
? undefined |
|
||||
: { key: args.sslkey, cert: args.sslcert }, |
|
||||
}; |
|
||||
} |
|
@ -0,0 +1,52 @@ |
|||||
|
import url from 'url'; |
||||
|
import type { Socket } from 'socket.io'; |
||||
|
import type { SSH } from '../shared/interfaces'; |
||||
|
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 && |
||||
|
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1'); |
||||
|
|
||||
|
const urlArgs = ( |
||||
|
referer: string, |
||||
|
def: { [s: string]: string }, |
||||
|
): { [s: string]: string } => |
||||
|
Object.assign(def, url.parse(referer, true).query); |
||||
|
|
||||
|
export const getCommand = ( |
||||
|
{ |
||||
|
request: { |
||||
|
headers: { referer }, |
||||
|
}, |
||||
|
client: { |
||||
|
conn: { remoteAddress }, |
||||
|
}, |
||||
|
}: Socket, |
||||
|
{ user, host, port, auth, pass, key, knownHosts }: SSH, |
||||
|
command: string, |
||||
|
forcessh: boolean, |
||||
|
): { args: string[]; user: boolean } => ({ |
||||
|
args: |
||||
|
!forcessh && localhost(host) |
||||
|
? loginOptions(command, remoteAddress) |
||||
|
: sshOptions( |
||||
|
{ |
||||
|
...urlArgs(referer, { |
||||
|
port: `${port}`, |
||||
|
pass: pass || '', |
||||
|
command, |
||||
|
auth, |
||||
|
knownHosts, |
||||
|
}), |
||||
|
host: address(referer, user, host), |
||||
|
}, |
||||
|
key, |
||||
|
), |
||||
|
user: |
||||
|
(!forcessh && localhost(host)) || |
||||
|
user !== '' || |
||||
|
user.includes('@') || |
||||
|
address(referer, user, host).includes('@'), |
||||
|
}); |
@ -1,50 +0,0 @@ |
|||||
import * as url from 'url'; |
|
||||
import { Socket } from 'socket.io'; |
|
||||
import { SSH } from '../interfaces'; |
|
||||
import address from './address'; |
|
||||
import loginOptions from './login'; |
|
||||
import sshOptions from './ssh'; |
|
||||
|
|
||||
const localhost = (host: string): boolean => |
|
||||
process.getuid() === 0 && |
|
||||
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1'); |
|
||||
|
|
||||
const urlArgs = ( |
|
||||
referer: string, |
|
||||
def: { [s: string]: string } |
|
||||
): { [s: string]: string } => |
|
||||
Object.assign(def, url.parse(referer, true).query); |
|
||||
|
|
||||
export default ( |
|
||||
{ |
|
||||
request: { |
|
||||
headers: { referer }, |
|
||||
}, |
|
||||
client: { |
|
||||
conn: { remoteAddress }, |
|
||||
}, |
|
||||
}: Socket, |
|
||||
{ user, host, port, auth, pass, key, knownhosts }: SSH, |
|
||||
command: string, |
|
||||
forcessh: boolean |
|
||||
): { args: string[]; user: boolean } => ({ |
|
||||
args: !forcessh && localhost(host) |
|
||||
? loginOptions(command, remoteAddress) |
|
||||
: sshOptions( |
|
||||
{ ...urlArgs(referer, { |
|
||||
port: `${port}`, |
|
||||
pass: pass || '', |
|
||||
command, |
|
||||
auth, |
|
||||
knownhosts, |
|
||||
}), |
|
||||
host: address(referer, user, host) |
|
||||
}, |
|
||||
key |
|
||||
), |
|
||||
user: |
|
||||
(!forcessh && localhost(host)) || |
|
||||
user !== '' || |
|
||||
user.includes('@') || |
|
||||
address(referer, user, host).includes('@'), |
|
||||
}); |
|
@ -1,8 +0,0 @@ |
|||||
import { isUndefined } from 'lodash'; |
|
||||
|
|
||||
export default 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; |
|
||||
} |
|
@ -1,4 +0,0 @@ |
|||||
import WeTTy from './wetty'; |
|
||||
import init from './cli'; |
|
||||
|
|
||||
export default { start: WeTTy, init }; |
|
@ -0,0 +1,44 @@ |
|||||
|
import type SocketIO from 'socket.io'; |
||||
|
import express from 'express'; |
||||
|
import compression from 'compression'; |
||||
|
import winston from 'express-winston'; |
||||
|
|
||||
|
import type { SSL, SSLBuffer, Server } from '../shared/interfaces.js'; |
||||
|
import { favicon, redirect } from './socketServer/middleware.js'; |
||||
|
import { html } from './socketServer/html.js'; |
||||
|
import { listen } from './socketServer/socket.js'; |
||||
|
import { logger } from '../shared/logger.js'; |
||||
|
import { serveStatic, trim } from './socketServer/assets.js'; |
||||
|
import { policies } from './socketServer/security.js'; |
||||
|
import { loadSSL } from './socketServer/ssl.js'; |
||||
|
|
||||
|
export async function server( |
||||
|
{ base, port, host, title, allowIframe }: Server, |
||||
|
ssl?: SSL, |
||||
|
): Promise<SocketIO.Server> { |
||||
|
const basePath = trim(base); |
||||
|
logger.info('Starting server', { |
||||
|
ssl, |
||||
|
port, |
||||
|
base, |
||||
|
title, |
||||
|
}); |
||||
|
|
||||
|
const app = express(); |
||||
|
const client = html(basePath, title); |
||||
|
app |
||||
|
.use(`${basePath}/web_modules`, serveStatic('web_modules')) |
||||
|
.use(`${basePath}/assets`, serveStatic('assets')) |
||||
|
.use(`${basePath}/client`, serveStatic('client')) |
||||
|
.use(winston.logger(logger)) |
||||
|
.use(compression()) |
||||
|
.use(favicon) |
||||
|
.use(redirect) |
||||
|
.use(policies(allowIframe)) |
||||
|
.get(basePath, client) |
||||
|
.get(`${basePath}/ssh/:user`, client); |
||||
|
|
||||
|
const sslBuffer: SSLBuffer = await loadSSL(ssl); |
||||
|
|
||||
|
return listen(app, host, port, basePath, sslBuffer); |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
import { resolve } from 'path'; |
||||
|
import express from 'express'; |
||||
|
|
||||
|
export const trim = (str: string): string => str.replace(/\/*$/, ''); |
||||
|
export const serveStatic = (path: string) => |
||||
|
express.static(resolve(process.cwd(), 'build', path)); |
@ -1,78 +0,0 @@ |
|||||
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 '../interfaces'; |
|
||||
import html from './html'; |
|
||||
import logger from '../utils/logger'; |
|
||||
|
|
||||
const distDir = path.join(__dirname, 'client'); |
|
||||
|
|
||||
const trim = (str: string): string => str.replace(/\/*$/, ''); |
|
||||
|
|
||||
export default function createServer( |
|
||||
{ base, port, host, title, bypasshelmet }: Server, |
|
||||
{ key, cert }: SSLBuffer |
|
||||
): SocketIO.Server { |
|
||||
const basePath = trim(base); |
|
||||
|
|
||||
logger.info('Starting server', { |
|
||||
key, |
|
||||
cert, |
|
||||
port, |
|
||||
base, |
|
||||
title, |
|
||||
}); |
|
||||
|
|
||||
const app = express(); |
|
||||
app |
|
||||
.use(expressWinston.logger(logger)) |
|
||||
.use(compression()) |
|
||||
.use(favicon(path.join(distDir, 'favicon.ico'))) |
|
||||
.use(`${basePath}/public`, express.static(distDir)) |
|
||||
.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) |
|
||||
); |
|
||||
else next(); |
|
||||
}); |
|
||||
|
|
||||
// Allow helmet to be bypassed.
|
|
||||
// Unfortunately, order matters with middleware
|
|
||||
// which is why this is thrown in the middle
|
|
||||
if (!bypasshelmet) { |
|
||||
app.use(helmet()); |
|
||||
} |
|
||||
|
|
||||
const client = html(base, title); |
|
||||
app.get(basePath, client).get(`${basePath}/ssh/:user`, client); |
|
||||
|
|
||||
return socket( |
|
||||
!isUndefined(key) && !isUndefined(cert) |
|
||||
? https.createServer({ key, cert }, app).listen(port, host, () => { |
|
||||
logger.info('Server started', { |
|
||||
port, |
|
||||
connection: 'https', |
|
||||
}); |
|
||||
}) |
|
||||
: http.createServer(app).listen(port, host, () => { |
|
||||
logger.info('Server started', { |
|
||||
port, |
|
||||
connection: 'http', |
|
||||
}); |
|
||||
}), |
|
||||
{ |
|
||||
path: `${basePath}/socket.io`, |
|
||||
pingInterval: 3000, |
|
||||
pingTimeout: 7000 |
|
||||
} |
|
||||
); |
|
||||
} |
|
@ -0,0 +1,15 @@ |
|||||
|
import type express from 'express'; |
||||
|
import { join } from 'path'; |
||||
|
import { default as _favicon } from 'serve-favicon'; |
||||
|
|
||||
|
export const favicon = _favicon(join('build', 'assets', 'favicon.ico')); |
||||
|
|
||||
|
export function redirect( |
||||
|
req: express.Request, |
||||
|
res: express.Response, |
||||
|
next: Function, |
||||
|
) { |
||||
|
if (req.path.substr(-1) === '/' && req.path.length > 1) |
||||
|
res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length)); |
||||
|
else next(); |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import helmet from 'helmet'; |
||||
|
import type { Request, Response } from 'express'; |
||||
|
|
||||
|
export const policies = (allowIframe: boolean) => ( |
||||
|
req: Request, |
||||
|
res: Response, |
||||
|
next: (err?: unknown) => void, |
||||
|
) => { |
||||
|
helmet({ |
||||
|
frameguard: allowIframe ? false : { action: 'sameorigin' }, |
||||
|
referrerPolicy: { policy: ['no-referrer-when-downgrade'] }, |
||||
|
contentSecurityPolicy: { |
||||
|
directives: { |
||||
|
defaultSrc: ["'self'"], |
||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], |
||||
|
styleSrc: ["'self'", "'unsafe-inline'"], |
||||
|
fontSrc: ["'self'", 'data:'], |
||||
|
connectSrc: [ |
||||
|
"'self'", |
||||
|
(req.protocol === 'http' ? 'ws://' : 'wss://') + req.get('host'), |
||||
|
], |
||||
|
}, |
||||
|
}, |
||||
|
})(req, res, next); |
||||
|
}; |
@ -0,0 +1,36 @@ |
|||||
|
import type express from 'express'; |
||||
|
import socket from 'socket.io'; |
||||
|
import http from 'http'; |
||||
|
import https from 'https'; |
||||
|
import isUndefined from 'lodash/isUndefined.js'; |
||||
|
|
||||
|
import { logger } from '../../shared/logger.js'; |
||||
|
import type { SSLBuffer } from '../../shared/interfaces.js'; |
||||
|
|
||||
|
export const listen = ( |
||||
|
app: express.Express, |
||||
|
host: string, |
||||
|
port: number, |
||||
|
path: string, |
||||
|
{ key, cert }: SSLBuffer, |
||||
|
): SocketIO.Server => |
||||
|
socket( |
||||
|
!isUndefined(key) && !isUndefined(cert) |
||||
|
? https.createServer({ key, cert }, app).listen(port, host, () => { |
||||
|
logger.info('Server started', { |
||||
|
port, |
||||
|
connection: 'https', |
||||
|
}); |
||||
|
}) |
||||
|
: http.createServer(app).listen(port, host, () => { |
||||
|
logger.info('Server started', { |
||||
|
port, |
||||
|
connection: 'http', |
||||
|
}); |
||||
|
}), |
||||
|
{ |
||||
|
path: `${path}/socket.io`, |
||||
|
pingInterval: 3000, |
||||
|
pingTimeout: 7000, |
||||
|
}, |
||||
|
); |
@ -1,14 +1,14 @@ |
|||||
import { readFile } from 'fs-extra'; |
import fs from 'fs-extra'; |
||||
|
import isUndefined from 'lodash/isUndefined.js'; |
||||
import { resolve } from 'path'; |
import { resolve } from 'path'; |
||||
import { isUndefined } from 'lodash'; |
import type { SSL, SSLBuffer } from '../../shared/interfaces'; |
||||
import { SSL, SSLBuffer } from '../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(resolve(ssl.key)), |
||||
readFile(resolve(ssl.cert)), |
fs.readFile(resolve(ssl.cert)), |
||||
]); |
]); |
||||
return { key, cert }; |
return { key, cert }; |
||||
} |
} |
@ -1,5 +0,0 @@ |
|||||
import logger from './logger'; |
|
||||
|
|
||||
export { logger }; |
|
||||
|
|
||||
export * from './ssl'; |
|
@ -1,24 +0,0 @@ |
|||||
import { createLogger, format, transports } from 'winston'; |
|
||||
|
|
||||
const { combine, timestamp, label, simple, json, colorize } = format; |
|
||||
|
|
||||
const dev = combine( |
|
||||
colorize(), |
|
||||
label({ label: 'Wetty' }), |
|
||||
timestamp(), |
|
||||
simple() |
|
||||
); |
|
||||
|
|
||||
const prod = combine(label({ label: 'Wetty' }), timestamp(), json()); |
|
||||
|
|
||||
const logger = createLogger({ |
|
||||
format: process.env.NODE_ENV === 'development' ? dev : prod, |
|
||||
transports: [ |
|
||||
new transports.Console({ |
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', |
|
||||
handleExceptions: true, |
|
||||
}), |
|
||||
], |
|
||||
}); |
|
||||
|
|
||||
export default logger; |
|
@ -1,72 +0,0 @@ |
|||||
/** |
|
||||
* Create WeTTY server |
|
||||
* @module WeTTy |
|
||||
*/ |
|
||||
import server from '../socketServer'; |
|
||||
import getCommand from '../command'; |
|
||||
import { login, spawn } from './term'; |
|
||||
import { loadSSL, logger } from '../utils'; |
|
||||
import { SSH, SSL, SSLBuffer, Server } from '../interfaces'; |
|
||||
|
|
||||
/** |
|
||||
* Starts WeTTy Server |
|
||||
* @name startWeTTy |
|
||||
*/ |
|
||||
export default function startWeTTy( |
|
||||
ssh: SSH = { user: '', host: 'localhost', auth: 'password', port: 22 }, |
|
||||
serverConf: Server = { |
|
||||
base: '/wetty/', |
|
||||
port: 3000, |
|
||||
host: '0.0.0.0', |
|
||||
title: 'WeTTy', |
|
||||
bypasshelmet: false, |
|
||||
}, |
|
||||
command = '', |
|
||||
forcessh = false, |
|
||||
ssl?: SSL |
|
||||
): Promise<void> { |
|
||||
return loadSSL(ssl).then((sslBuffer: SSLBuffer) => { |
|
||||
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 io = server(serverConf, sslBuffer); |
|
||||
/** |
|
||||
* Wetty server connected too |
|
||||
* @fires WeTTy#connnection |
|
||||
*/ |
|
||||
io.on('connection', (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(' '), |
|
||||
}); |
|
||||
|
|
||||
if (sshUser) { |
|
||||
spawn(socket, args); |
|
||||
} else { |
|
||||
login(socket) |
|
||||
.then((username: string) => { |
|
||||
args[1] = `${username.trim()}@${args[1]}`; |
|
||||
logger.debug('Spawning term', { |
|
||||
username: username.trim(), |
|
||||
cmd: args.join(' ').trim(), |
|
||||
}); |
|
||||
return spawn(socket, args); |
|
||||
}) |
|
||||
.catch(() => { |
|
||||
logger.info('Disconnect signal sent'); |
|
||||
}); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
import spawn from './spawn'; |
|
||||
|
|
||||
export { spawn }; |
|
||||
|
|
||||
export * from './login'; |
|
@ -0,0 +1,133 @@ |
|||||
|
import fs from 'fs-extra'; |
||||
|
import path from 'path'; |
||||
|
import JSON5 from 'json5'; |
||||
|
import isUndefined from 'lodash/isUndefined.js'; |
||||
|
import type { Arguments } from 'yargs'; |
||||
|
|
||||
|
import type { Config, SSH, Server, SSL } from './interfaces'; |
||||
|
import { |
||||
|
sshDefault, |
||||
|
serverDefault, |
||||
|
forceSSHDefault, |
||||
|
defaultCommand, |
||||
|
} from './defaults.js'; |
||||
|
|
||||
|
type confValue = |
||||
|
| boolean |
||||
|
| string |
||||
|
| number |
||||
|
| undefined |
||||
|
| unknown |
||||
|
| SSH |
||||
|
| Server |
||||
|
| SSL; |
||||
|
/** |
||||
|
* Cast given value to boolean |
||||
|
* |
||||
|
* @param value - variable to cast |
||||
|
* @returns variable cast to boolean |
||||
|
*/ |
||||
|
function ensureBoolean(value: confValue): boolean { |
||||
|
switch (value) { |
||||
|
case true: |
||||
|
case 'true': |
||||
|
case 1: |
||||
|
case '1': |
||||
|
case 'on': |
||||
|
case 'yes': |
||||
|
return true; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load JSON5 config from file and merge with default args |
||||
|
* If no path is provided the default config is returned |
||||
|
* |
||||
|
* @param filepath - path to config to load |
||||
|
* @returns variable cast to boolean |
||||
|
*/ |
||||
|
export async function loadConfigFile(filepath?: string): Promise<Config> { |
||||
|
if (isUndefined(filepath)) { |
||||
|
return { |
||||
|
ssh: sshDefault, |
||||
|
server: serverDefault, |
||||
|
command: defaultCommand, |
||||
|
forceSSH: forceSSHDefault, |
||||
|
}; |
||||
|
} |
||||
|
const content = await fs.readFile(path.resolve(filepath)); |
||||
|
const parsed = JSON5.parse(content.toString()) as Config; |
||||
|
return { |
||||
|
ssh: isUndefined(parsed.ssh) |
||||
|
? sshDefault |
||||
|
: Object.assign(sshDefault, parsed.ssh), |
||||
|
server: isUndefined(parsed.server) |
||||
|
? serverDefault |
||||
|
: Object.assign(serverDefault, parsed.server), |
||||
|
command: isUndefined(parsed.command) ? defaultCommand : `${parsed.command}`, |
||||
|
forceSSH: isUndefined(parsed.forceSSH) |
||||
|
? forceSSHDefault |
||||
|
: ensureBoolean(parsed.forceSSH), |
||||
|
ssl: parsed.ssl, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Merge 2 objects removing undefined fields |
||||
|
* |
||||
|
* @param target - base object |
||||
|
* @param source - object to get new values from |
||||
|
* @returns merged object |
||||
|
* |
||||
|
*/ |
||||
|
const objectAssign = ( |
||||
|
target: SSH | Server, |
||||
|
source: Record<string, confValue>, |
||||
|
): SSH | Server => |
||||
|
Object.fromEntries( |
||||
|
Object.entries(source).map(([key, value]) => [ |
||||
|
key, |
||||
|
isUndefined(source[key]) ? target[key] : value, |
||||
|
]), |
||||
|
) as SSH | Server; |
||||
|
|
||||
|
/** |
||||
|
* Merge cli arguemens with config object |
||||
|
* |
||||
|
* @param opts - Object containing cli args |
||||
|
* @param config - Config object |
||||
|
* @returns merged configuration |
||||
|
* |
||||
|
*/ |
||||
|
export function mergeCliConf(opts: Arguments, config: Config): Config { |
||||
|
const ssl = { |
||||
|
key: opts['ssl-key'], |
||||
|
cert: opts['ssl-cert'], |
||||
|
...config.ssl, |
||||
|
} as SSL; |
||||
|
return { |
||||
|
ssh: objectAssign(config.ssh, { |
||||
|
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'], |
||||
|
}) as SSH, |
||||
|
server: objectAssign(config.server, { |
||||
|
base: opts.base, |
||||
|
host: opts.host, |
||||
|
port: opts.port, |
||||
|
title: opts.title, |
||||
|
allowIframe: opts['allow-iframe'], |
||||
|
}) as Server, |
||||
|
command: isUndefined(opts.command) ? config.command : `${opts.command}`, |
||||
|
forceSSH: isUndefined(opts['force-ssh']) |
||||
|
? config.forceSSH |
||||
|
: ensureBoolean(opts['force-ssh']), |
||||
|
ssl: isUndefined(ssl.key) || isUndefined(ssl.cert) ? undefined : ssl, |
||||
|
}; |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import type { SSH, Server } from './interfaces'; |
||||
|
|
||||
|
export const sshDefault: SSH = { |
||||
|
user: process.env.SSHUSER || '', |
||||
|
host: process.env.SSHHOST || 'localhost', |
||||
|
auth: process.env.SSHAUTH || 'password', |
||||
|
pass: process.env.SSHPASS || undefined, |
||||
|
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/', |
||||
|
port: parseInt(process.env.PORT || '3000', 10), |
||||
|
host: '0.0.0.0', |
||||
|
title: process.env.TITLE || 'WeTTy - The Web Terminal Emulator', |
||||
|
allowIframe: false, |
||||
|
}; |
||||
|
|
||||
|
export const forceSSHDefault = process.env.FORCESSH === 'true' || false; |
||||
|
export const defaultCommand = process.env.COMMAND || 'login'; |
@ -0,0 +1 @@ |
|||||
|
export const isDev = process.env.NODE_ENV === 'development'; |
@ -0,0 +1,24 @@ |
|||||
|
import winston from 'winston'; |
||||
|
|
||||
|
import { isDev } from './env.js'; |
||||
|
|
||||
|
const { combine, timestamp, label, simple, json, colorize } = winston.format; |
||||
|
|
||||
|
const dev = combine( |
||||
|
colorize(), |
||||
|
label({ label: 'Wetty' }), |
||||
|
timestamp(), |
||||
|
simple(), |
||||
|
); |
||||
|
|
||||
|
const prod = combine(label({ label: 'Wetty' }), timestamp(), json()); |
||||
|
|
||||
|
export const logger = winston.createLogger({ |
||||
|
format: isDev ? dev : prod, |
||||
|
transports: [ |
||||
|
new winston.transports.Console({ |
||||
|
level: isDev ? 'debug' : 'info', |
||||
|
handleExceptions: true, |
||||
|
}), |
||||
|
], |
||||
|
}); |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"extends": "./tsconfig.json", |
||||
|
"include": [ |
||||
|
"src/client" |
||||
|
] |
||||
|
} |
@ -1,13 +1,20 @@ |
|||||
{ |
{ |
||||
"compilerOptions": { |
"compilerOptions": { |
||||
"target": "es6", |
"module": "esnext", |
||||
"module": "commonjs", |
"target": "es2019", |
||||
"lib": ["es2015"], |
|
||||
"sourceMap": true, |
|
||||
"strict": true, |
|
||||
"moduleResolution": "node", |
"moduleResolution": "node", |
||||
"typeRoots": ["node_module/@types"], |
"declaration": true, |
||||
"outDir": "./dist" |
"downlevelIteration": true, |
||||
}, |
"esModuleInterop": true, |
||||
"include": ["./src/**/*.ts"] |
"forceConsistentCasingInFileNames": true, |
||||
|
"noFallthroughCasesInSwitch": true, |
||||
|
"noImplicitAny": true, |
||||
|
"noImplicitReturns": true, |
||||
|
"noImplicitThis": true, |
||||
|
"noUnusedLocals": true, |
||||
|
"noUnusedParameters": true, |
||||
|
"removeComments": true, |
||||
|
"skipLibCheck": true, |
||||
|
"strict": true |
||||
|
} |
||||
} |
} |
||||
|
@ -0,0 +1,14 @@ |
|||||
|
{ |
||||
|
"extends": "./tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"incremental": true, |
||||
|
"outDir": "./build", |
||||
|
"sourceMap": true |
||||
|
}, |
||||
|
"include": [ |
||||
|
"src" |
||||
|
], |
||||
|
"exclude": [ |
||||
|
"src/client" |
||||
|
] |
||||
|
} |
@ -1,137 +0,0 @@ |
|||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
|
||||
|
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; |
|
||||
import nodeExternals from 'webpack-node-externals'; |
|
||||
import path from 'path'; |
|
||||
import webpack from 'webpack'; |
|
||||
|
|
||||
const template = override => ({ |
|
||||
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', |
|
||||
}, |
|
||||
node: { |
|
||||
__filename: false, |
|
||||
__dirname: false, |
|
||||
}, |
|
||||
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', '@babel/plugin-proposal-class-properties'], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
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', |
|
||||
}), |
|
||||
]; |
|
File diff suppressed because it is too large
Loading…
Reference in new issue