Browse Source

Merge pull request #270 from butlerx/version-2

pull/272/head
Cian Butler 4 years ago
committed by GitHub
parent
commit
98e8158f9a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .babelrc
  2. 5
      .eslintignore
  3. 45
      .eslintrc.js
  4. 71
      .eslintrc.json
  5. 127
      .gitignore
  6. 4
      .npmignore
  7. 13
      .prettierrc.js
  8. 16
      .prettierrc.json
  9. 101
      README.md
  10. 27
      conf/config.json5
  11. 0
      conf/nginx.template
  12. 0
      conf/wetty.conf
  13. 2
      conf/wetty.service
  14. 0
      containers/ssh/Dockerfile
  15. 4
      containers/wetty/Dockerfile
  16. 11
      docker-compose.yml
  17. 6
      docs/flags.md
  18. 6
      docs/https.md
  19. 2
      docs/nginx.md
  20. 4
      docs/service.md
  21. 124
      index.js
  22. 178
      package.json
  23. 0
      src/assets/favicon.ico
  24. 52
      src/assets/scss/options.scss
  25. 28
      src/assets/scss/overlay.scss
  26. 25
      src/assets/scss/styles.scss
  27. 6
      src/assets/scss/terminal.scss
  28. 4
      src/assets/scss/variables.scss
  29. 6
      src/buffer.ts
  30. 5
      src/client/dev.ts
  31. 11
      src/client/disconnect.ts
  32. 160
      src/client/index.ts
  33. 3
      src/client/shared/elements.ts
  34. 4
      src/client/shared/verify.ts
  35. 195
      src/client/specs/download.spec.ts
  36. 4
      src/client/verify.ts
  37. 107
      src/client/wetty.scss
  38. 51
      src/client/wetty.ts
  39. 11
      src/client/wetty/disconnect.ts
  40. 236
      src/client/wetty/download.spec.ts
  41. 76
      src/client/wetty/download.ts
  42. 6
      src/client/wetty/mobile.ts
  43. 5
      src/client/wetty/shared/type.ts
  44. 2
      src/client/wetty/socket.ts
  45. 24
      src/client/wetty/term.ts
  46. 37
      src/client/wetty/term/confiruragtion.ts
  47. 10
      src/client/wetty/term/confiruragtion/clipboard.ts
  48. 23
      src/client/wetty/term/confiruragtion/editor.ts
  49. 6
      src/client/wetty/term/confiruragtion/load.ts
  50. 106
      src/main.ts
  51. 73
      src/server.ts
  52. 18
      src/server/cli/index.ts
  53. 22
      src/server/cli/options.ts
  54. 32
      src/server/cli/parseArgs.ts
  55. 52
      src/server/command.ts
  56. 6
      src/server/command/address.ts
  57. 50
      src/server/command/index.ts
  58. 7
      src/server/command/login.ts
  59. 8
      src/server/command/parse.ts
  60. 35
      src/server/command/ssh.ts
  61. 4
      src/server/index.ts
  62. 17
      src/server/login.ts
  63. 6
      src/server/shared/xterm.ts
  64. 44
      src/server/socketServer.ts
  65. 6
      src/server/socketServer/assets.ts
  66. 44
      src/server/socketServer/html.ts
  67. 78
      src/server/socketServer/index.ts
  68. 15
      src/server/socketServer/middleware.ts
  69. 25
      src/server/socketServer/security.ts
  70. 36
      src/server/socketServer/socket.ts
  71. 10
      src/server/socketServer/ssl.ts
  72. 20
      src/server/spawn.ts
  73. 5
      src/server/utils/index.ts
  74. 24
      src/server/utils/logger.ts
  75. 72
      src/server/wetty/index.ts
  76. 5
      src/server/wetty/term/index.ts
  77. 133
      src/shared/config.ts
  78. 22
      src/shared/defaults.ts
  79. 1
      src/shared/env.ts
  80. 14
      src/shared/interfaces.ts
  81. 24
      src/shared/logger.ts
  82. 6
      tsconfig.browser.json
  83. 25
      tsconfig.json
  84. 14
      tsconfig.node.json
  85. 137
      webpack.config.babel.js
  86. 6275
      yarn.lock

5
.babelrc

@ -1,5 +0,0 @@
{
"presets": ["@babel/preset-typescript", ["@babel/env"]],
"compact": true,
"plugins": ["lodash", "@babel/plugin-proposal-class-properties"]
}

5
.eslintignore

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

45
.eslintrc.js

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

71
.eslintrc.json

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

127
.gitignore

@ -1,19 +1,118 @@
lib-cov
*.seed
web_modules
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
*.csv
*.dat
*.out
*.pid
*.gz
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
tmp
# Runtime data
pids
logs
results
*.pid
*.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
node_modules
.esm-cache
# TypeScript v1 declaration files
typings/
# 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
.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

4
.npmignore

@ -19,10 +19,8 @@ src
*.yml
Dockerfile
*.png
.babelrc
.dockerignore
.eslint*
.prettierrc.js
tsconfig.json
webpack.config.babel.js
tsconfig
docs

13
.prettierrc.js

@ -1,13 +0,0 @@
module.exports = {
singleQuote: true,
trailingComma: 'es5',
proseWrap: 'always',
overrides: [
{
files: ['*.js', '*.ts'],
options: {
printWidth: 80,
},
},
],
};

16
.prettierrc.json

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

101
README.md

@ -2,7 +2,7 @@
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)
[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
@ -35,7 +35,30 @@ yarn global add wetty
## Usage
```sh
wetty [-h] [--port PORT] [--base BASE] [--sshhost SSH_HOST] [--sshport SSH_PORT] [--sshuser SSH_USER] [--host HOST] [--command COMMAND] [--forcessh] [--bypasshelmet] [--title TITLE] [--sslkey SSL_KEY_PATH] [--sslcert SSL_CERT_PATH]
$ wetty --help
Options:
--help, -h Print help message [boolean]
--version Show version number [boolean]
--conf config file to load config from [string]
--ssl-key path to SSL key [string]
--ssl-cert path to SSL certificate [string]
--ssh-host ssh server host [string]
--ssh-port ssh server port [number]
--ssh-user ssh user [string]
--title window title [string]
--ssh-auth defaults to "password", you can use "publickey,password"
instead [string]
--ssh-pass ssh password [string]
--ssh-key path to an optional client private key (connection will be
password-less and insecure!) [string]
--force-ssh Connecting through ssh even if running as root [boolean]
--known-hosts path to known hosts file [string]
--base, -b base path to wetty [string]
--port, -p wetty listen port [number]
--host wetty listen host [string]
--command, -c command to run in shell [string]
--bypass-helmet disable helmet from placing security restrictions [boolean]
```
Open your browser on `http://yourserver:3000/wetty` and you will prompted to
@ -44,11 +67,11 @@ user before hand.
If you run it as root it will launch `/bin/login` (where you can specify the
user name), else it will launch `ssh` and connect by default to `localhost`. The
SSH connection can be forced using the `--forcessh` option.
SSH connection can be forced using the `--force-ssh` option.
If instead you wish to connect to a remote host you can specify the `--sshhost`
option, the SSH port using the `--sshport` option and the SSH user using the
`--sshuser` option.
If instead you wish to connect to a remote host you can specify the `--ssh-host`
option, the SSH port using the `--ssh-port` option and the SSH user using the
`--ssh-user` option.
Check out the
[Flags docs](https://github.com/butlerx/wetty/blob/master/docs/flags.md) for a
@ -94,47 +117,47 @@ Thanks goes to these wonderful people
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://cianbutler.ie"><img src="https://avatars1.githubusercontent.com/u/867930?v=4" width="100px;" alt="Cian Butler"/><br /><sub><b>Cian Butler</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=butlerx" title="Code">💻</a> <a href="https://github.com/butlerx/WeTTy/commits?author=butlerx" title="Documentation">📖</a></td>
<td align="center"><a href="http://about.me/krishnasrinivas"><img src="https://avatars0.githubusercontent.com/u/634494?v=4" width="100px;" alt="Krishna Srinivas"/><br /><sub><b>Krishna Srinivas</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=krishnasrinivas" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/acalatrava"><img src="https://avatars1.githubusercontent.com/u/8502129?v=4" width="100px;" alt="acalatrava"/><br /><sub><b>acalatrava</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=acalatrava" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Strubbl"><img src="https://avatars3.githubusercontent.com/u/97055?v=4" width="100px;" alt="Strubbl"/><br /><sub><b>Strubbl</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=Strubbl" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/2sheds"><img src="https://avatars3.githubusercontent.com/u/16163?v=4" width="100px;" alt="Oleg Kurapov"/><br /><sub><b>Oleg Kurapov</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=2sheds" title="Code">💻</a></td>
<td align="center"><a href="http://www.rabchev.com"><img src="https://avatars0.githubusercontent.com/u/1876061?v=4" width="100px;" alt="Boyan Rabchev"/><br /><sub><b>Boyan Rabchev</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=rabchev" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nosemeocurrenada"><img src="https://avatars1.githubusercontent.com/u/3845708?v=4" width="100px;" alt="Jimmy"/><br /><sub><b>Jimmy</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nosemeocurrenada" title="Code">💻</a></td>
<td align="center"><a href="http://cianbutler.ie"><img src="https://avatars1.githubusercontent.com/u/867930?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cian Butler</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=butlerx" title="Code">💻</a> <a href="https://github.com/butlerx/WeTTy/commits?author=butlerx" title="Documentation">📖</a></td>
<td align="center"><a href="http://about.me/krishnasrinivas"><img src="https://avatars0.githubusercontent.com/u/634494?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Srinivas</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=krishnasrinivas" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/acalatrava"><img src="https://avatars1.githubusercontent.com/u/8502129?v=4?s=100" width="100px;" alt=""/><br /><sub><b>acalatrava</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=acalatrava" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Strubbl"><img src="https://avatars3.githubusercontent.com/u/97055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Strubbl</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=Strubbl" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/2sheds"><img src="https://avatars3.githubusercontent.com/u/16163?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oleg Kurapov</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=2sheds" title="Code">💻</a></td>
<td align="center"><a href="http://www.rabchev.com"><img src="https://avatars0.githubusercontent.com/u/1876061?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Boyan Rabchev</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=rabchev" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nosemeocurrenada"><img src="https://avatars1.githubusercontent.com/u/3845708?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jimmy</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nosemeocurrenada" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://www.gerritforge.com"><img src="https://avatars3.githubusercontent.com/u/182893?v=4" width="100px;" alt="Luca Milanesio"/><br /><sub><b>Luca Milanesio</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=lucamilanesio" title="Code">💻</a></td>
<td align="center"><a href="http://anthonyjund.com"><img src="https://avatars3.githubusercontent.com/u/39376331?v=4" width="100px;" alt="Anthony Jund"/><br /><sub><b>Anthony Jund</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=antonyjim" title="Code">💻</a></td>
<td align="center"><a href="https://www.mirtouf.fr"><img src="https://avatars3.githubusercontent.com/u/5165058?v=4" width="100px;" alt="mirtouf"/><br /><sub><b>mirtouf</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=mirtouf" title="Code">💻</a></td>
<td align="center"><a href="https://cor-net.org"><img src="https://avatars1.githubusercontent.com/u/556693?v=4" width="100px;" alt="Bertrand Roussel"/><br /><sub><b>Bertrand Roussel</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=CoRfr" title="Code">💻</a></td>
<td align="center"><a href="https://www.benl.com.au/"><img src="https://avatars0.githubusercontent.com/u/6703966?v=4" width="100px;" alt="Ben Letchford"/><br /><sub><b>Ben Letchford</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=benletchford" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SouraDutta"><img src="https://avatars0.githubusercontent.com/u/33066261?v=4" width="100px;" alt="SouraDutta"/><br /><sub><b>SouraDutta</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=SouraDutta" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/koushikmln"><img src="https://avatars3.githubusercontent.com/u/8670988?v=4" width="100px;" alt="Koushik M.L.N"/><br /><sub><b>Koushik M.L.N</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=koushikmln" title="Code">💻</a></td>
<td align="center"><a href="http://www.gerritforge.com"><img src="https://avatars3.githubusercontent.com/u/182893?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Luca Milanesio</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=lucamilanesio" title="Code">💻</a></td>
<td align="center"><a href="http://anthonyjund.com"><img src="https://avatars3.githubusercontent.com/u/39376331?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Anthony Jund</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=antonyjim" title="Code">💻</a></td>
<td align="center"><a href="https://www.mirtouf.fr"><img src="https://avatars3.githubusercontent.com/u/5165058?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mirtouf</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=mirtouf" title="Code">💻</a></td>
<td align="center"><a href="https://cor-net.org"><img src="https://avatars1.githubusercontent.com/u/556693?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bertrand Roussel</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=CoRfr" title="Code">💻</a></td>
<td align="center"><a href="https://www.benl.com.au/"><img src="https://avatars0.githubusercontent.com/u/6703966?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Letchford</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=benletchford" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SouraDutta"><img src="https://avatars0.githubusercontent.com/u/33066261?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SouraDutta</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=SouraDutta" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/koushikmln"><img src="https://avatars3.githubusercontent.com/u/8670988?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koushik M.L.N</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=koushikmln" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://imu.li/"><img src="https://avatars3.githubusercontent.com/u/4085046?v=4" width="100px;" alt="Imuli"/><br /><sub><b>Imuli</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=imuli" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/perpen"><img src="https://avatars2.githubusercontent.com/u/9963805?v=4" width="100px;" alt="perpen"/><br /><sub><b>perpen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=perpen" title="Code">💻</a></td>
<td align="center"><a href="https://nathanleclaire.com"><img src="https://avatars3.githubusercontent.com/u/1476820?v=4" width="100px;" alt="Nathan LeClaire"/><br /><sub><b>Nathan LeClaire</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nathanleclaire" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/MiKr13"><img src="https://avatars2.githubusercontent.com/u/34394719?v=4" width="100px;" alt="Mihir Kumar"/><br /><sub><b>Mihir Kumar</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=MiKr13" title="Code">💻</a></td>
<td align="center"><a href="http://redhat.com"><img src="https://avatars0.githubusercontent.com/u/540893?v=4" width="100px;" alt="Chris Suszynski"/><br /><sub><b>Chris Suszynski</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=cardil" title="Code">💻</a></td>
<td align="center"><a href="http://9wd.de"><img src="https://avatars1.githubusercontent.com/u/1257835?v=4" width="100px;" alt="Felix Bartels"/><br /><sub><b>Felix Bartels</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=fbartels" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jarrettgilliam"><img src="https://avatars3.githubusercontent.com/u/5099690?v=4" width="100px;" alt="Jarrett Gilliam"/><br /><sub><b>Jarrett Gilliam</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jarrettgilliam" title="Code">💻</a></td>
<td align="center"><a href="https://imu.li/"><img src="https://avatars3.githubusercontent.com/u/4085046?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Imuli</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=imuli" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/perpen"><img src="https://avatars2.githubusercontent.com/u/9963805?v=4?s=100" width="100px;" alt=""/><br /><sub><b>perpen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=perpen" title="Code">💻</a></td>
<td align="center"><a href="https://nathanleclaire.com"><img src="https://avatars3.githubusercontent.com/u/1476820?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nathan LeClaire</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nathanleclaire" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/MiKr13"><img src="https://avatars2.githubusercontent.com/u/34394719?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihir Kumar</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=MiKr13" title="Code">💻</a></td>
<td align="center"><a href="http://redhat.com"><img src="https://avatars0.githubusercontent.com/u/540893?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chris Suszynski</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=cardil" title="Code">💻</a></td>
<td align="center"><a href="http://9wd.de"><img src="https://avatars1.githubusercontent.com/u/1257835?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Felix Bartels</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=fbartels" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jarrettgilliam"><img src="https://avatars3.githubusercontent.com/u/5099690?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jarrett Gilliam</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jarrettgilliam" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://harrylee.me"><img src="https://avatars0.githubusercontent.com/u/7056279?v=4" width="100px;" alt="Harry Lee"/><br /><sub><b>Harry Lee</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=harryleesan" title="Code">💻</a></td>
<td align="center"><a href="http://andreask.cs.illinois.edu"><img src="https://avatars3.githubusercontent.com/u/352067?v=4" width="100px;" alt="Andreas Klöckner"/><br /><sub><b>Andreas Klöckner</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=inducer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/DenisKramer"><img src="https://avatars1.githubusercontent.com/u/23534092?v=4" width="100px;" alt="DenisKramer"/><br /><sub><b>DenisKramer</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=DenisKramer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/vamship"><img src="https://avatars0.githubusercontent.com/u/7143376?v=4" width="100px;" alt="Vamshi K Ponnapalli"/><br /><sub><b>Vamshi K Ponnapalli</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=vamship" title="Code">💻</a></td>
<td align="center"><a href="https://tridnguyen.com"><img src="https://avatars1.githubusercontent.com/u/1652595?v=4" width="100px;" alt="Tri Nguyen"/><br /><sub><b>Tri Nguyen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=tnguyen14" title="Documentation">📖</a></td>
<td align="center"><a href="https://felix.pojtinger.com/"><img src="https://avatars1.githubusercontent.com/u/28832235?v=4" width="100px;" alt="Felix Pojtinger"/><br /><sub><b>Felix Pojtinger</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=pojntfx" title="Documentation">📖</a></td>
<td align="center"><a href="https://nealey.github.io/"><img src="https://avatars3.githubusercontent.com/u/423780?v=4" width="100px;" alt="Neale Pickett"/><br /><sub><b>Neale Pickett</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nealey" title="Code">💻</a></td>
<td align="center"><a href="https://harrylee.me"><img src="https://avatars0.githubusercontent.com/u/7056279?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Harry Lee</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=harryleesan" title="Code">💻</a></td>
<td align="center"><a href="http://andreask.cs.illinois.edu"><img src="https://avatars3.githubusercontent.com/u/352067?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andreas Klöckner</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=inducer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/DenisKramer"><img src="https://avatars1.githubusercontent.com/u/23534092?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DenisKramer</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=DenisKramer" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/vamship"><img src="https://avatars0.githubusercontent.com/u/7143376?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vamshi K Ponnapalli</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=vamship" title="Code">💻</a></td>
<td align="center"><a href="https://tridnguyen.com"><img src="https://avatars1.githubusercontent.com/u/1652595?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tri Nguyen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=tnguyen14" title="Documentation">📖</a></td>
<td align="center"><a href="https://felix.pojtinger.com/"><img src="https://avatars1.githubusercontent.com/u/28832235?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Felix Pojtinger</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=pojntfx" title="Documentation">📖</a></td>
<td align="center"><a href="https://nealey.github.io/"><img src="https://avatars3.githubusercontent.com/u/423780?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Neale Pickett</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=nealey" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://www.matthewpiercey.ml"><img src="https://avatars3.githubusercontent.com/u/22581026?v=4" width="100px;" alt="Matthew Piercey"/><br /><sub><b>Matthew Piercey</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=mtpiercey" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/kholbekj"><img src="https://avatars3.githubusercontent.com/u/2786571?v=4" width="100px;" alt="Kasper Holbek Jensen"/><br /><sub><b>Kasper Holbek Jensen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=kholbekj" title="Documentation">📖</a></td>
<td align="center"><a href="https://mastodon.technology/@farhan"><img src="https://avatars1.githubusercontent.com/u/10103765?v=4" width="100px;" alt="Farhan Khan"/><br /><sub><b>Farhan Khan</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=khanzf" title="Code">💻</a></td>
<td align="center"><a href="https://www.jurrevriesen.nl"><img src="https://avatars1.githubusercontent.com/u/7419259?v=4" width="100px;" alt="Jurre Vriesen"/><br /><sub><b>Jurre Vriesen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jurruh" title="Code">💻</a></td>
<td align="center"><a href="https://www.kartar.net/"><img src="https://avatars3.githubusercontent.com/u/4365?v=4" width="100px;" alt="James Turnbull"/><br /><sub><b>James Turnbull</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jamtur01" title="Code">💻</a></td>
<td align="center"><a href="https://www.matthewpiercey.ml"><img src="https://avatars3.githubusercontent.com/u/22581026?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matthew Piercey</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=mtpiercey" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/kholbekj"><img src="https://avatars3.githubusercontent.com/u/2786571?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kasper Holbek Jensen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=kholbekj" title="Documentation">📖</a></td>
<td align="center"><a href="https://mastodon.technology/@farhan"><img src="https://avatars1.githubusercontent.com/u/10103765?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Farhan Khan</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=khanzf" title="Code">💻</a></td>
<td align="center"><a href="https://www.jurrevriesen.nl"><img src="https://avatars1.githubusercontent.com/u/7419259?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jurre Vriesen</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jurruh" title="Code">💻</a></td>
<td align="center"><a href="https://www.kartar.net/"><img src="https://avatars3.githubusercontent.com/u/4365?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James Turnbull</b></sub></a><br /><a href="https://github.com/butlerx/WeTTy/commits?author=jamtur01" title="Code">💻</a></td>
</tr>
</table>

27
conf/config.json5

@ -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',
}
*/
}

0
bin/nginx.template → conf/nginx.template

0
bin/wetty.conf → conf/wetty.conf

2
bin/wetty.service → conf/wetty.service

@ -11,7 +11,7 @@ After=network.target
[Service]
WorkingDirectory=$HOME/.config/yarn/global/node_modules/wetty/
ExecStart=/usr/bin/node index.js -p 3000 --host 127.0.0.1
ExecStart=/usr/bin/node . -p 3000 --host 127.0.0.1
[Install]
WantedBy=multi-user.target

0
ssh.Dockerfile → containers/ssh/Dockerfile

4
Dockerfile → containers/wetty/Dockerfile

@ -1,4 +1,4 @@
FROM node:dubnium-alpine as builder
FROM node:current-alpine as builder
RUN apk add -U build-base python
WORKDIR /usr/src/app
COPY . /usr/src/app
@ -6,7 +6,7 @@ RUN yarn && \
yarn build && \
yarn install --production --ignore-scripts --prefer-offline
FROM node:dubnium-alpine
FROM node:current-alpine
LABEL maintainer="butlerx@notthe.cloud"
WORKDIR /usr/src/app
ENV NODE_ENV=production

11
docker-compose.yml

@ -2,9 +2,10 @@
version: '3.5'
services:
wetty:
build: .
image: wettyoss/wetty
container_name: wetty
build:
context: .
dockerfile: containers/wetty/Dockerfile
tty: true
working_dir: /usr/src/app
ports:
@ -16,9 +17,8 @@ services:
web:
image: nginx
container_name: wetty-nginx
volumes:
- ./bin/nginx.template:/etc/nginx/conf.d/wetty.template
- ./conf/nginx.template:/etc/nginx/conf.d/wetty.template
ports:
- '80:80'
environment:
@ -35,9 +35,8 @@ services:
wetty-ssh:
build:
context: .
dockerfile: ssh.Dockerfile
dockerfile: containers/ssh/Dockerfile
image: wettyoss/wetty:ssh
container_name: 'wetty-ssh'
networks:
default:

6
docs/flags.md

@ -14,12 +14,12 @@ the `login` binary rather than ssh. If no host is specified it will use
`localhost` as the ssh host.
If instead you wish to connect to a remote host you can specify the host with
the `--sshhost` flag and pass the IP or DNS address of the host you want to
the `--ssh-host` flag and pass the IP or DNS address of the host you want to
connect to.
## Default User
You can specify the default user used to ssh to a host using the `--sshuser`.
You can specify the default user used to ssh to a host using the `--ssh-user`.
This user can overwritten by going to
`http://yourserver:3000/wetty/ssh/<username>`. If this is left blank a user will
be prompted to enter their username when they connect.
@ -27,7 +27,7 @@ be prompted to enter their username when they connect.
## SSH Port
By default WeTTy will try to ssh to port `22`, if your host uses an alternative
ssh port this can be specified with the flag `--sshport`.
ssh port this can be specified with the flag `--ssh-port`.
## WeTTy URL

6
docs/https.md

@ -6,11 +6,11 @@ either using WeTTy behind a proxy or directly.
See docs for [NGinX](./nginx.md) and [Apache](./apache.md) for running behind a
proxy.
To run WeTTy directly with SSL use both the `--sslkey` and `--sslcert` flags and
pass them the path too your cert and key as follows:
To run WeTTy directly with SSL use both the `--ssl-key` and `--ssl-cert` flags
and pass them the path too your cert and key as follows:
```bash
wetty --sslkey key.pem --sslcert cert.pem
wetty --ssl-key key.pem --ssl-cert cert.pem
```
If you don't have SSL certificates from a CA you can create a self signed

2
docs/nginx.md

@ -19,7 +19,7 @@ Put the following configuration in your nginx conf:
```nginx
location ^~ /wetty {
proxy_pass http://127.0.0.1:3000/WeTTy;
proxy_pass http://127.0.0.1:3000/wetty;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

4
docs/service.md

@ -7,7 +7,7 @@ bundled with the npm package to make this easier.
```bash
$ yarn global add wetty
$ sudo cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.conf /etc/init
$ sudo cp ~/.config/yarn/global/node_modules/wetty/conf/wetty.conf /etc/init
$ sudo start wetty
```
@ -15,7 +15,7 @@ $ sudo start wetty
```bash
$ yarn global add wetty
$ cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.service ~/.config/systemd/user/
$ cp ~/.config/yarn/global/node_modules/wetty/conf/wetty.service ~/.config/systemd/user/
$ systemctl --user enable wetty
$ systemctl --user start wetty
```

124
index.js

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

178
package.json

@ -1,8 +1,25 @@
{
"name": "wetty",
"version": "1.4.1",
"version": "2.0.0",
"description": "WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/butlerx/wetty",
"license": "MIT",
"type": "module",
"main": "./build/main.js",
"module": "./build/server.js",
"files": [
"build/"
],
"scripts": {
"build": "snowpack build",
"dev": "NODE_ENV=development concurrently --kill-others --success first \"snowpack dev\" \"nodemon .\"",
"prepublishOnly": "snowpack build",
"lint": "eslint src/**/*.ts",
"start": "NODE_ENV=production node .",
"contributor": "all-contributors",
"test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register src/**/*.spec.ts",
"clean": "rm -rf build yarn-error.log"
},
"repository": {
"type": "git",
"url": "git://github.com/butlerx/wetty.git"
@ -12,20 +29,9 @@
"email": "butlerx@notthe.cloud",
"url": "cianbutler.ie"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/butlerx/wetty/issues"
},
"main": "index.js",
"scripts": {
"build": "babel-node node_modules/.bin/webpack",
"contributor": "all-contributors",
"dev": "NODE_ENV=development concurrently --kill-others --success first \"babel-node node_modules/.bin/webpack --watch\" \"nodemon .\"",
"lint": "eslint --ext .ts,.js .",
"prepublishOnly": "NODE_ENV=production yarn build",
"start": "NODE_ENV=production node .",
"test": "mocha -r babel-register-ts src/**/*.spec.ts"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
@ -33,102 +39,122 @@
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"git add"
"eslint --fix"
],
"*.{json,scss,md}": [
"prettier --write",
"git add"
"prettier --write"
]
},
"bin": {
"wetty": "./index.js"
},
"engines": {
"node": ">=10.22"
},
"nodemonConfig": {
"ignore": [
"*.scss",
"src/*",
"*.json"
]
},
"preferGlobal": true,
"snowpack": {
"installOptions": {
"sourceMap": true,
"installTypes": true
},
"mount": {
"src/client": "/client",
"src/assets": "/assets"
},
"exclude": [
"src/server/**/*.ts",
"src/client/**/*.spec.ts",
"src/*.ts"
],
"plugins": [
[
"@snowpack/plugin-run-script",
{
"cmd": "tsc -p tsconfig.browser.json --noEmit",
"watch": "$1 --watch"
}
],
[
"@snowpack/plugin-run-script",
{
"cmd": "sass src/assets/scss:build/assets/css --load-path=node_modules -s compressed --no-source-map",
"watch": "$1 --watch"
}
],
[
"@snowpack/plugin-run-script",
{
"cmd": "tsc -p tsconfig.node.json",
"watch": "$1 --watch"
}
]
]
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"compression": "^1.7.4",
"express": "^4.17.1",
"express-winston": "^4.0.1",
"express-winston": "^4.0.5",
"file-type": "^12.3.0",
"fs-extra": "^8.1.0",
"helmet": "^3.20.1",
"lodash": "^4.17.15",
"fs-extra": "^9.0.1",
"helmet": "^4.1.0",
"json5": "^2.1.3",
"lodash": "^4.17.20",
"node-pty": "^0.9.0",
"sass": "^1.26.10",
"serve-favicon": "^2.5.0",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
"toastify-js": "^1.6.1",
"winston": "^3.2.1",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"toastify-js": "^1.9.1",
"winston": "^3.3.3",
"xterm": "^4.8.1",
"xterm-addon-fit": "^0.4.0",
"yargs": "^14.0.0"
"yargs": "^15.4.1"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/node": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@babel/register": "^7.5.5",
"@types/chai": "^4.2.5",
"@types/compression": "^1.0.1",
"@types/express": "^4.17.1",
"@types/fs-extra": "^8.0.0",
"@types/helmet": "^0.0.44",
"@types/chai": "^4.2.12",
"@types/compression": "^1.7.0",
"@types/express": "^4.17.8",
"@types/fs-extra": "^9.0.1",
"@types/helmet": "^0.0.48",
"@types/jsdom": "^12.2.4",
"@types/lodash": "^4.14.138",
"@types/mocha": "^5.2.7",
"@types/lodash": "^4.14.161",
"@types/mocha": "^8.0.3",
"@types/morgan": "^1.7.37",
"@types/node": "^12.7.3",
"@types/serve-favicon": "^2.2.31",
"@types/node": "^14.6.3",
"@types/serve-favicon": "^2.5.0",
"@types/sinon": "^7.5.1",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/webpack-env": "^1.14.0",
"@types/yargs": "^13.0.2",
"@types/socket.io": "^2.1.11",
"@types/socket.io-client": "^1.4.33",
"@types/winston": "^2.4.4",
"@types/yargs": "^15.0.5",
"@typescript-eslint/eslint-plugin": "^2.5.0",
"@typescript-eslint/parser": "^2.5.0",
"all-contributors-cli": "^6.9.3",
"babel-loader": "^8.0.6",
"babel-plugin-lodash": "^3.3.4",
"babel-register-ts": "^7.0.0",
"@typescript-eslint/parser": "^4.3.0",
"all-contributors-cli": "^6.17.2",
"chai": "^4.2.0",
"concurrently": "^4.1.2",
"css-loader": "^3.2.0",
"eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0",
"file-loader": "^4.2.0",
"git-authors-cli": "^1.0.18",
"husky": "^3.0.9",
"concurrently": "^5.2.0",
"eslint": "^7.8.1",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-prettier": "^3.1.4",
"git-authors-cli": "^1.0.28",
"husky": "^4.2.5",
"jsdom": "^15.2.1",
"lint-staged": "~9.2.5",
"mini-css-extract-plugin": "^0.8.0",
"mocha": "^6.2.2",
"node-sass": "^4.12.0",
"nodemon": "^1.19.2",
"prettier": "^1.18.2",
"sass-loader": "^8.0.0",
"lint-staged": "^10.2.13",
"mocha": "^8.1.3",
"nodemon": "^2.0.4",
"prettier": "^2.1.1",
"sinon": "^7.5.0",
"style-loader": "^1.0.0",
"typescript": "~3.6.2",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-node-externals": "^1.7.2"
"snowpack": "^2.10.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
},
"contributors": [
"Krishna Srinivas <krishna.srinivas@gmail.com>",

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

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

52
src/assets/scss/options.scss

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

28
src/assets/scss/overlay.scss

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

25
src/assets/scss/styles.scss

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

6
src/assets/scss/terminal.scss

@ -0,0 +1,6 @@
#terminal {
display: flex;
height: 100%;
position: relative;
width: 100%;
}

4
src/assets/scss/variables.scss

@ -0,0 +1,4 @@
$black: #000;
$grey: rgba(0, 0, 0, 0.75);
$white: #fff;
$lgrey: #ccc;

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

@ -3,13 +3,13 @@ import { createInterface } from 'readline';
ask('Enter your username');
function ask(question: string): Promise<string> {
const r = createInterface({
const rlp = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => {
r.question(`${question}: `, answer => {
r.close();
rlp.question(`${question}: `, answer => {
rlp.close();
resolve(answer);
});
});

5
src/client/dev.ts

@ -0,0 +1,5 @@
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});

11
src/client/disconnect.ts

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

160
src/client/index.ts

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

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

@ -1,2 +1,5 @@
export const overlay = document.getElementById('overlay');
export const terminal = document.getElementById('terminal');
export const editor = document.querySelector(
'#options .editor',
) as HTMLInputElement;

4
src/client/shared/verify.ts

@ -0,0 +1,4 @@
export function verifyPrompt(e: { returnValue: string }): string {
e.returnValue = 'Are you sure?';
return e.returnValue;
}

195
src/client/specs/download.spec.ts

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

4
src/client/verify.ts

@ -1,4 +0,0 @@
export default function verifyPrompt(e: { returnValue: string }): string {
e.returnValue = 'Are you sure?';
return e.returnValue;
}

107
src/client/wetty.scss

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

51
src/client/wetty.ts

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

11
src/client/wetty/disconnect.ts

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

236
src/client/wetty/download.spec.ts

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

76
src/client/download.ts → src/client/wetty/download.ts

@ -1,6 +1,61 @@
// @ts-ignore
import Toastify from 'toastify-js';
import fileType from 'file-type';
const DEFAULT_FILE_BEGIN = '\u001b[5i';
const DEFAULT_FILE_END = '\u001b[4i';
function onCompleteFile(bufferCharacters: string): void {
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();
}
export class FileDownloader {
fileBuffer: string[];
fileBegin: string;
@ -9,15 +64,15 @@ export class FileDownloader {
onCompleteFileCallback: Function;
constructor(
onCompleteFileCallback: (file: string) => void,
onCompleteFileCallback: Function = onCompleteFile,
fileBegin: string = DEFAULT_FILE_BEGIN,
fileEnd: string = DEFAULT_FILE_END
fileEnd: string = DEFAULT_FILE_END,
) {
this.fileBuffer = [];
this.onCompleteFileCallback = onCompleteFileCallback;
this.fileBegin = fileBegin;
this.fileEnd = fileEnd;
this.partialFileBegin = '';
this.onCompleteFileCallback = onCompleteFileCallback;
}
bufferCharacter(character: string): string {
@ -69,13 +124,13 @@ export class FileDownloader {
this.fileBuffer.length >= this.fileBegin.length + this.fileEnd.length &&
this.fileBuffer.slice(-this.fileEnd.length).join('') === this.fileEnd
) {
this.onCompleteFile(
this.onCompleteFileCallback(
this.fileBuffer
.slice(
this.fileBegin.length,
this.fileBuffer.length - this.fileEnd.length
this.fileBuffer.length - this.fileEnd.length,
)
.join('')
.join(''),
);
this.fileBuffer = [];
}
@ -93,13 +148,6 @@ export class FileDownloader {
) {
return data;
}
return data
.split('')
.map(this.bufferCharacter.bind(this))
.join('');
}
onCompleteFile(bufferCharacters: string): void {
this.onCompleteFileCallback(bufferCharacters);
return data.split('').map(this.bufferCharacter.bind(this)).join('');
}
}

6
src/client/mobile.ts → src/client/wetty/mobile.ts

@ -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');
if (isNull(screen)) return;
if (_.isNull(screen)) return;
screen.setAttribute('contenteditable', 'true');
screen.setAttribute('spellcheck', 'false');
screen.setAttribute('autocorrect', 'false');

5
src/client/wetty/shared/type.ts

@ -0,0 +1,5 @@
import { Terminal } from 'xterm';
export class Term extends Terminal {
resizeTerm(): void {}
}

2
src/client/socket.ts → src/client/wetty/socket.ts

@ -1,4 +1,4 @@
import * as io from 'socket.io-client';
import io from 'socket.io-client';
const userRegex = new RegExp('ssh/[^/]+$');
export const trim = (str: string): string => str.replace(/\/*$/, '');

24
src/client/wetty/term.ts

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

37
src/client/wetty/term/confiruragtion.ts

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

10
src/client/copyToClipboard.ts → src/client/wetty/term/confiruragtion/clipboard.ts

@ -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 {
if (window.clipboardData?.setData) {
window.clipboardData.setData('Text', text);
if ((window as any).clipboardData?.setData) {
(window as any).clipboardData.setData('Text', text);
return true;
}
if (

23
src/client/wetty/term/confiruragtion/editor.ts

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

6
src/client/options.ts → src/client/wetty/term/confiruragtion/load.ts

@ -1,9 +1,9 @@
import { isUndefined } from 'lodash';
import _ from 'lodash';
export default function loadOptions(): object {
export function loadOptions(): object {
const defaultOptions = { fontSize: 14 };
try {
return isUndefined(localStorage.options)
return _.isUndefined(localStorage.options)
? defaultOptions
: JSON.parse(localStorage.options);
} catch {

106
src/main.ts

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

73
src/server.ts

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

18
src/server/cli/index.ts

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

22
src/server/cli/options.ts

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

32
src/server/cli/parseArgs.ts

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

52
src/server/command.ts

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

6
src/server/command/address.ts

@ -1,8 +1,4 @@
export default function address(
referer: string,
user: string,
host: string
): string {
export function address(referer: string, user: string, host: string): string {
const match = referer.match('.+/ssh/([^/]+)$');
const fallback = user ? `${user}@${host}` : host;
return match ? `${match[1].split('?')[0]}@${host}` : fallback;

50
src/server/command/index.ts

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

7
src/server/command/login.ts

@ -1,14 +1,11 @@
import { isUndefined } from 'lodash';
import isUndefined from 'lodash/isUndefined.js';
const getRemoteAddress = (remoteAddress: string): string =>
isUndefined(remoteAddress.split(':')[3])
? 'localhost'
: remoteAddress.split(':')[3];
export default function loginOptions(
command: string,
remoteAddress: string
): string[] {
export function loginOptions(command: string, remoteAddress: string): string[] {
return command === 'login'
? [command, '-h', getRemoteAddress(remoteAddress)]
: [command];

8
src/server/command/parse.ts

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

35
src/server/command/ssh.ts

@ -1,21 +1,12 @@
import { isUndefined } from 'lodash';
import parseCommand from './parse';
import logger from '../utils/logger';
import isUndefined from 'lodash/isUndefined.js';
import { logger } from '../../shared/logger.js';
export default function sshOptions(
{
pass,
path,
command,
host,
port,
auth,
knownhosts,
}: { [s: string]: string },
key?: string
export function sshOptions(
{ pass, path, command, host, port, auth, knownHosts }: Record<string, string>,
key?: string,
): string[] {
const cmd = parseCommand(command, path);
const hostChecking = knownhosts !== '/dev/null' ? 'yes' : 'no';
const hostChecking = knownHosts !== '/dev/null' ? 'yes' : 'no';
const sshRemoteOptsBase = [
'ssh',
host,
@ -25,7 +16,7 @@ export default function sshOptions(
'-o',
`PreferredAuthentications=${auth}`,
'-o',
`UserKnownHostsFile=${knownhosts}`,
`UserKnownHostsFile=${knownHosts}`,
'-o',
`StrictHostKeyChecking=${hostChecking}`,
];
@ -40,8 +31,12 @@ export default function sshOptions(
sshRemoteOptsBase.splice(sshRemoteOptsBase.indexOf('-o'), 2);
}
if (cmd === '') {
return sshRemoteOptsBase;
}
return 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;
}

4
src/server/index.ts

@ -1,4 +0,0 @@
import WeTTy from './wetty';
import init from './cli';
export default { start: WeTTy, init };

17
src/server/wetty/term/login.ts → src/server/login.ts

@ -1,5 +1,14 @@
import { spawn } from 'node-pty';
import { xterm } from './xterm';
import type SocketIO from 'socket.io';
import pty from 'node-pty';
import { dirname, resolve as resolvePath } from 'path';
import { fileURLToPath } from 'url';
import { xterm } from './shared/xterm.js';
const executable = resolvePath(
dirname(fileURLToPath(import.meta.url)),
'..',
'buffer.js',
);
export function login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username
@ -12,13 +21,13 @@ export function login(socket: SocketIO.Socket): Promise<string> {
// Request carries no username information
// Create terminal and ask user for username
const term = spawn('/usr/bin/env', ['node', `${__dirname}/buffer.js`], xterm);
const term = pty.spawn('/usr/bin/env', ['node', executable], xterm);
let buf = '';
return new Promise((resolve, reject) => {
term.on('exit', () => {
resolve(buf);
});
term.on('data', data => {
term.on('data', (data: string) => {
socket.emit('data', data);
});
socket

6
src/server/wetty/term/xterm.ts → src/server/shared/xterm.ts

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

44
src/server/socketServer.ts

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

6
src/server/socketServer/assets.ts

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

44
src/server/socketServer/html.ts

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

78
src/server/socketServer/index.ts

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

15
src/server/socketServer/middleware.ts

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

25
src/server/socketServer/security.ts

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

36
src/server/socketServer/socket.ts

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

10
src/server/utils/ssl.ts → src/server/socketServer/ssl.ts

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

20
src/server/wetty/term/spawn.ts → src/server/spawn.ts

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

5
src/server/utils/index.ts

@ -1,5 +0,0 @@
import logger from './logger';
export { logger };
export * from './ssl';

24
src/server/utils/logger.ts

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

72
src/server/wetty/index.ts

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

5
src/server/wetty/term/index.ts

@ -1,5 +0,0 @@
import spawn from './spawn';
export { spawn };
export * from './login';

133
src/shared/config.ts

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

22
src/shared/defaults.ts

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

1
src/shared/env.ts

@ -0,0 +1 @@
export const isDev = process.env.NODE_ENV === 'development';

14
src/server/interfaces.ts → src/shared/interfaces.ts

@ -1,9 +1,10 @@
export interface SSH {
[s: string]: string | number | boolean | undefined;
user: string;
host: string;
auth: string;
port: number;
knownhosts: string;
knownHosts: string;
pass?: string;
key?: string;
}
@ -19,9 +20,18 @@ export interface SSLBuffer {
}
export interface Server {
[s: string]: string | number | boolean;
port: number;
host: string;
title: string;
base: string;
bypasshelmet: boolean;
allowIframe: boolean;
}
export interface Config {
ssh: SSH;
server: Server;
forceSSH: boolean;
command: string;
ssl?: SSL;
}

24
src/shared/logger.ts

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

6
tsconfig.browser.json

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"include": [
"src/client"
]
}

25
tsconfig.json

@ -1,13 +1,20 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["es2015"],
"sourceMap": true,
"strict": true,
"module": "esnext",
"target": "es2019",
"moduleResolution": "node",
"typeRoots": ["node_module/@types"],
"outDir": "./dist"
},
"include": ["./src/**/*.ts"]
"declaration": true,
"downlevelIteration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": true,
"skipLibCheck": true,
"strict": true
}
}

14
tsconfig.node.json

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": true,
"outDir": "./build",
"sourceMap": true
},
"include": [
"src"
],
"exclude": [
"src/client"
]
}

137
webpack.config.babel.js

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

6275
yarn.lock

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