Browse Source

Feature/remediation (#162)

clean up server package to be for ogranised
No longer use event emitting for logging
ensure process env is typed correctly
remove 301 redirect
reenable text on mobile
split static classes up to make repo more structured
split docs up

Add List of contributors
Docs: Add @butlerx as a contributor
Docs: Add @krishnasrinivas as a contributor
Docs: Add @acalatrava as a contributor
Docs: Add @Strubbl as a contributor
Docs: Add @2sheds as a contributor
Docs: Add @rabchev as a contributor
Docs: Add @nosemeocurrenada as a contributor
Docs: Add @lucamilanesio as a contributor
Docs: Add @antonyjim as a contributor
Docs: Add @mirtouf as a contributor
Docs: Add @CoRfr as a contributor
Docs: Add @benletchford as a contributor
Docs: Add @SouraDutta as a contributor
Docs: Add @koushikmln as a contributor
Docs: Add @imuli as a contributor
Docs: Add @perpen as a contributor
Docs: Add @nathanleclaire as a contributor
Docs: Add @MiKr13 as a contributor
Docs: Add @cardil as a contributor
Docs: Add @fbartels as a contributor
Docs: Add @jarrettgilliam as a contributor
Docs: Add @harryleesan as a contributor
Docs: Add @inducer as a contributor
Docs: Add @DenisKramer as a contributor
Docs: Add @vamship as a contributor
Docs: Add @tnguyen14 as a contributor
Docs: Add @pojntfx as a contributor
Docs: Add @nealey as a contributor
Docs: Add @mtpiercey as a contributor
Docs: Add @kholbekj as a contributor
Docs: Add @khanzf as a contributor
Docs: Add @jurruh as a contributor
Docs: Add @jamtur01 as a contributor
pull/208/head
Cian Butler 5 years ago
committed by GitHub
parent
commit
b035e8668a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 313
      .all-contributorsrc
  2. 3
      .eslintrc.js
  3. 355
      README.md
  4. 12
      docker-compose.yml
  5. 20
      docs/README.md
  6. 28
      docs/apache.md
  7. 22
      docs/auto-login.md
  8. 20
      docs/development.md
  9. 27
      docs/docker.md
  10. 26
      docs/downloading-file.md
  11. 38
      docs/flags.md
  12. 21
      docs/https.md
  13. 33
      docs/nginx.md
  14. 29
      docs/service.md
  15. 0
      docs/terminal.png
  16. 2
      index.js
  17. 65
      package.json
  18. 56
      src/client/copyToClipboard.ts
  19. 11
      src/client/disconnect.ts
  20. 55
      src/client/download.ts
  21. 2
      src/client/elements.ts
  22. 207
      src/client/index.ts
  23. 14
      src/client/mobile.ts
  24. 12
      src/client/options.ts
  25. 10
      src/client/resize.ts
  26. 9
      src/client/socket.ts
  27. 4
      src/client/verify.ts
  28. 2
      src/server/buffer.ts
  29. 18
      src/server/cli/index.ts
  30. 20
      src/server/cli/options.ts
  31. 30
      src/server/cli/parseArgs.ts
  32. 101
      src/server/command.ts
  33. 9
      src/server/command/address.ts
  34. 47
      src/server/command/index.ts
  35. 15
      src/server/command/login.ts
  36. 8
      src/server/command/parse.ts
  37. 33
      src/server/command/ssh.ts
  38. 3
      src/server/emitter.ts
  39. 85
      src/server/index.ts
  40. 4
      src/server/interfaces.ts
  41. 32
      src/server/logger.ts
  42. 93
      src/server/server.ts
  43. 35
      src/server/socketServer/html.ts
  44. 70
      src/server/socketServer/index.ts
  45. 11
      src/server/ssl.ts
  46. 80
      src/server/term.ts
  47. 5
      src/server/utils/index.ts
  48. 26
      src/server/utils/logger.ts
  49. 14
      src/server/utils/ssl.ts
  50. 136
      src/server/wetty.ts
  51. 71
      src/server/wetty/index.ts
  52. 5
      src/server/wetty/term/index.ts
  53. 34
      src/server/wetty/term/login.ts
  54. 40
      src/server/wetty/term/spawn.ts
  55. 15
      src/server/wetty/term/xterm.ts
  56. 25
      webpack.config.babel.js
  57. 1552
      yarn.lock

313
.all-contributorsrc

@ -0,0 +1,313 @@
{
"projectName": "WeTTy",
"projectOwner": "butlerx",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "eslint",
"contributors": [
{
"login": "butlerx",
"name": "Cian Butler",
"avatar_url": "https://avatars1.githubusercontent.com/u/867930?v=4",
"profile": "http://cianbutler.ie",
"contributions": [
"code",
"doc"
]
},
{
"login": "krishnasrinivas",
"name": "Krishna Srinivas",
"avatar_url": "https://avatars0.githubusercontent.com/u/634494?v=4",
"profile": "http://about.me/krishnasrinivas",
"contributions": [
"code"
]
},
{
"login": "acalatrava",
"name": "acalatrava",
"avatar_url": "https://avatars1.githubusercontent.com/u/8502129?v=4",
"profile": "https://github.com/acalatrava",
"contributions": [
"code"
]
},
{
"login": "Strubbl",
"name": "Strubbl",
"avatar_url": "https://avatars3.githubusercontent.com/u/97055?v=4",
"profile": "https://github.com/Strubbl",
"contributions": [
"code"
]
},
{
"login": "2sheds",
"name": "Oleg Kurapov",
"avatar_url": "https://avatars3.githubusercontent.com/u/16163?v=4",
"profile": "https://github.com/2sheds",
"contributions": [
"code"
]
},
{
"login": "rabchev",
"name": "Boyan Rabchev",
"avatar_url": "https://avatars0.githubusercontent.com/u/1876061?v=4",
"profile": "http://www.rabchev.com",
"contributions": [
"code"
]
},
{
"login": "nosemeocurrenada",
"name": "Jimmy",
"avatar_url": "https://avatars1.githubusercontent.com/u/3845708?v=4",
"profile": "https://github.com/nosemeocurrenada",
"contributions": [
"code"
]
},
{
"login": "lucamilanesio",
"name": "Luca Milanesio",
"avatar_url": "https://avatars3.githubusercontent.com/u/182893?v=4",
"profile": "http://www.gerritforge.com",
"contributions": [
"code"
]
},
{
"login": "antonyjim",
"name": "Anthony Jund",
"avatar_url": "https://avatars3.githubusercontent.com/u/39376331?v=4",
"profile": "http://anthonyjund.com",
"contributions": [
"code"
]
},
{
"login": "mirtouf",
"name": "mirtouf",
"avatar_url": "https://avatars3.githubusercontent.com/u/5165058?v=4",
"profile": "https://www.mirtouf.fr",
"contributions": [
"code"
]
},
{
"login": "CoRfr",
"name": "Bertrand Roussel",
"avatar_url": "https://avatars1.githubusercontent.com/u/556693?v=4",
"profile": "https://cor-net.org",
"contributions": [
"code"
]
},
{
"login": "benletchford",
"name": "Ben Letchford",
"avatar_url": "https://avatars0.githubusercontent.com/u/6703966?v=4",
"profile": "https://www.benl.com.au/",
"contributions": [
"code"
]
},
{
"login": "SouraDutta",
"name": "SouraDutta",
"avatar_url": "https://avatars0.githubusercontent.com/u/33066261?v=4",
"profile": "https://github.com/SouraDutta",
"contributions": [
"code"
]
},
{
"login": "koushikmln",
"name": "Koushik M.L.N",
"avatar_url": "https://avatars3.githubusercontent.com/u/8670988?v=4",
"profile": "https://github.com/koushikmln",
"contributions": [
"code"
]
},
{
"login": "imuli",
"name": "Imuli",
"avatar_url": "https://avatars3.githubusercontent.com/u/4085046?v=4",
"profile": "https://imu.li/",
"contributions": [
"code"
]
},
{
"login": "perpen",
"name": "perpen",
"avatar_url": "https://avatars2.githubusercontent.com/u/9963805?v=4",
"profile": "https://github.com/perpen",
"contributions": [
"code"
]
},
{
"login": "nathanleclaire",
"name": "Nathan LeClaire",
"avatar_url": "https://avatars3.githubusercontent.com/u/1476820?v=4",
"profile": "https://nathanleclaire.com",
"contributions": [
"code"
]
},
{
"login": "MiKr13",
"name": "Mihir Kumar",
"avatar_url": "https://avatars2.githubusercontent.com/u/34394719?v=4",
"profile": "https://github.com/MiKr13",
"contributions": [
"code"
]
},
{
"login": "cardil",
"name": "Chris Suszynski",
"avatar_url": "https://avatars0.githubusercontent.com/u/540893?v=4",
"profile": "http://redhat.com",
"contributions": [
"code"
]
},
{
"login": "fbartels",
"name": "Felix Bartels",
"avatar_url": "https://avatars1.githubusercontent.com/u/1257835?v=4",
"profile": "http://9wd.de",
"contributions": [
"code"
]
},
{
"login": "jarrettgilliam",
"name": "Jarrett Gilliam",
"avatar_url": "https://avatars3.githubusercontent.com/u/5099690?v=4",
"profile": "https://github.com/jarrettgilliam",
"contributions": [
"code"
]
},
{
"login": "harryleesan",
"name": "Harry Lee",
"avatar_url": "https://avatars0.githubusercontent.com/u/7056279?v=4",
"profile": "https://harrylee.me",
"contributions": [
"code"
]
},
{
"login": "inducer",
"name": "Andreas Klöckner",
"avatar_url": "https://avatars3.githubusercontent.com/u/352067?v=4",
"profile": "http://andreask.cs.illinois.edu",
"contributions": [
"code"
]
},
{
"login": "DenisKramer",
"name": "DenisKramer",
"avatar_url": "https://avatars1.githubusercontent.com/u/23534092?v=4",
"profile": "https://github.com/DenisKramer",
"contributions": [
"code"
]
},
{
"login": "vamship",
"name": "Vamshi K Ponnapalli",
"avatar_url": "https://avatars0.githubusercontent.com/u/7143376?v=4",
"profile": "https://github.com/vamship",
"contributions": [
"code"
]
},
{
"login": "tnguyen14",
"name": "Tri Nguyen",
"avatar_url": "https://avatars1.githubusercontent.com/u/1652595?v=4",
"profile": "https://tridnguyen.com",
"contributions": [
"doc"
]
},
{
"login": "pojntfx",
"name": "Felix Pojtinger",
"avatar_url": "https://avatars1.githubusercontent.com/u/28832235?v=4",
"profile": "https://felix.pojtinger.com/",
"contributions": [
"doc"
]
},
{
"login": "nealey",
"name": "Neale Pickett",
"avatar_url": "https://avatars3.githubusercontent.com/u/423780?v=4",
"profile": "https://nealey.github.io/",
"contributions": [
"code"
]
},
{
"login": "mtpiercey",
"name": "Matthew Piercey",
"avatar_url": "https://avatars3.githubusercontent.com/u/22581026?v=4",
"profile": "https://www.matthewpiercey.ml",
"contributions": [
"doc"
]
},
{
"login": "kholbekj",
"name": "Kasper Holbek Jensen",
"avatar_url": "https://avatars3.githubusercontent.com/u/2786571?v=4",
"profile": "https://github.com/kholbekj",
"contributions": [
"doc"
]
},
{
"login": "khanzf",
"name": "Farhan Khan",
"avatar_url": "https://avatars1.githubusercontent.com/u/10103765?v=4",
"profile": "https://mastodon.technology/@farhan",
"contributions": [
"code"
]
},
{
"login": "jurruh",
"name": "Jurre Vriesen",
"avatar_url": "https://avatars1.githubusercontent.com/u/7419259?v=4",
"profile": "https://www.jurrevriesen.nl",
"contributions": [
"code"
]
},
{
"login": "jamtur01",
"name": "James Turnbull",
"avatar_url": "https://avatars3.githubusercontent.com/u/4365?v=4",
"profile": "https://www.kartar.net/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7
}

3
.eslintrc.js

@ -20,6 +20,7 @@ module.exports = {
'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',
},
settings: {
'import/resolver': {
@ -27,5 +28,5 @@ module.exports = {
extensions: ['.ts', '.js'],
},
},
}
},
};

355
README.md

@ -1,58 +1,37 @@
## WeTTy = Web + TTy
# WeTTY = Web + TTY.
<!-- 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)](#contributors-)<!-- ALL-CONTRIBUTORS-BADGE:END -->
![Version](https://img.shields.io/badge/version-1.1.7-blue.svg?cacheSeconds=2592000)
![Node Version](https://img.shields.io/badge/node-%3E%3D6.9-blue.svg)
[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://github.com/butlerx/wetty/tree/master/docs)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/butlerx/wetty/blob/master/LICENSE)
[![Twitter: cianbutlerx](https://img.shields.io/twitter/follow/cianbutlerx.svg?style=social)](https://twitter.com/cianbutlerx)
> Terminal access in browser over http/https
Terminal over HTTP and https. WeTTy is an alternative to ajaxterm and anyterm
but much better than them because WeTTy uses xterm.js which is a full fledged
implementation of terminal emulation written entirely in JavaScript. WeTTy uses
websockets rather then Ajax and hence better response time.
![WeTTy](/terminal.png?raw=true)
## Install
### Requiments
## Prerequisites
To instal WeTTy you'll need to have the following installed:
- Node.JS 10+
- node >=6.9
- make
- python
- build-essential
### From source
WeTTy can be installed from source or from npm.
To install from source run:
## Install
```bash
$ git clone https://github.com/butlerx/wetty.git
$ cd wetty
$ yarn
$ yarn build
```sh
yarn global add wetty
```
### From NPM
To install it globally from npm use yarn or npm:
## Usage
- yarn, `yarn global add wetty`
- npm, `npm i -g wetty`
### Autologin
For auto-login feature you'll need sshpass installed (NOT required for rest of
the program).
- `apt-get install sshpass` (debian eg. Ubuntu)
- `yum install sshpass` (red hat flavours eg. CentOs)
## Running WeTTy
Wetty can either be run as a standalone service or from another node script. To
see how to use WeTTy from node see the [API Doc](./docs)
```bash
$ node index.js
```sh
wetty [-h] [--port PORT] [--base BASE] [--sshhost SSH_HOST] [--sshport SSH_PORT] [--sshuser SSH_USER] [--host HOST] [--command COMMAND] [--bypasshelmet] [--title TITLE] [--sslkey SSL_KEY_PATH] [--sslcert SSL_CERT_PATH]
```
Open your browser on `http://yourserver:3000/wetty` and you will prompted to
@ -66,207 +45,111 @@ 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.
### Flags
WeTTy can be run with the `--help` flag to get a full list of flags.
#### Server Port
WeTTy runs on port `3000` by default. You can change the default port by
starting with the `--port` or `-p` flag.
#### SSH Host
If WeTTy is run as root while the host is set as the local machine it will use
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
connect to.
#### Default User
You can specify the default user used to ssh to a host using the `--sshuser`.
This user can overwritten by going to `http://yourserver:3000/ssh/<username>`.
If this is left blank a user will 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`.
#### WeTTy URL
If you'd prefer an HTTP base prefix other than `/wetty`, you can specify that
with `--base`.
**Do not set this to `/ssh/${something}`, as this will break username matching
code.**
#### HTTPS
Always use HTTPS especially with a terminal to your server. You can add HTTPS by
either using WeTTy behind a proxy or directly.
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:
```bash
node index.js --sslkey key.pem --sslcert cert.pem
```
If you don't have SSL certificates from a CA you can create a self signed
certificate using this command:
```
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```
### Auto Login:
You can also pass the ssh password as an optional query parameter to auto-login
the user like this (Only while running wetty as a non root account):
`http://yourserver:3000/wetty/ssh/<username>?sshpass=<password>`
This is not a required feature and the security implications for passing the
password in the url will have to be considered by the user
### File Downloading
Wetty supports file downloads by printing terminal escape sequences between a
base64 encoded file.
Check out the
[Flags docs](https://github.com/butlerx/wetty/blob/master/docs/flags.md) for a
full list of flags
The terminal escape sequences used are `^[[5i` and `^[[4i` (VT100 for "enter
auto print" and "exit auto print" respectively -
https://vt100.net/docs/tp83/appendixc.html).
An example of a helper script that prints the terminal escape characters and
base64s stdin:
```
$ cat wetty-download.sh
#!/bin/sh
echo '^[[5i'$(cat /dev/stdin | base64)'^[[4i'
```
You are then able to download files via wetty!
```
$ cat my-pdf-file.pdf | ./wetty-download.sh
```
Wetty will then issue a popup like the following that links to a local file
blob:
`Download ready: file-20191015233654.pdf`
## Run wetty behind nginx or apache
As said earlier you can use a proxy to add https to WeTTy.
**Note** that if your proxy is configured for https you should run WeTTy without
SSL
If your proxy uses a base path other than `/wetty`, specify the path with the
`--base` flag, or the `BASE` environment variable.
The following confs assume you want to serve wetty on the url
`example.com/wetty` and are running wetty with the default base and serving it
on the same server
#### Nginx
For a more detailed look see the [nginx.conf](./bin/nginx.template) used for
testing
Put the following configuration in nginx's conf:
```nginx
location /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";
proxy_read_timeout 43200000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
```
#### Apache
Put the following configuration in apache's conf:
```apache
RewriteCond %{REQUEST_URI} ^/wetty/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /wetty/socket.io/(.*) ws://localhost:3000/wetty/socket.io/$1 [P,L]
<LocationMatch ^/wetty/(.*)>
DirectorySlash On
Require all granted
ProxyPassMatch http://127.0.0.1:3000
ProxyPassReverse /wetty/
</LocationMatch>
```
### Dockerized Version
WeTTy can be run from a container to ssh to a remote host or the host system.
This is handy for quick deployments. Just modify `docker-compose.yml` for your
host and run:
```sh
$ docker-compose up -d
```
Visit the appropriate URL in your browser
(`[localhost|$(boot2docker ip)]:PORT`).
The default username is `term` and the password is `term`, if you did not modify
`SSHHOST`
In the docker version all flags can be accessed as environment variables such as
`SSHHOST` or `SSHPORT`.
If you dont want to build the image yourself just remove the line `build; .`
## Run WeTTy as a service daemon
### init.d
```bash
$ sudo yarn global add wetty
$ sudo cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.conf /etc/init
$ sudo start wetty
```
### systemd
```bash
$ yarn global add wetty
$ cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.service ~/.config/systemd/user/
$ systemctl --user enable wetty
$ systemctl --user start wetty
```
## FAQ
This will start WeTTy on port 3000. If you want to change the port or redirect
stdout/stderr you should change the last line in `wetty.conf` file, something
like this:
Check out the [docs](https://github.com/butlerx/wetty/tree/master/docs)
```systemd
exec sudo -u root wetty -p 80 >> /var/log/wetty.log 2>&1
```
## FAQ
- [Running as daemon](https://github.com/butlerx/wetty/blob/master/docs/service.md)
- [SSL Support](https://github.com/butlerx/wetty/blob/master/docs/ssl.md)
- [Using NGINX](https://github.com/butlerx/wetty/blob/master/docs/nginx.md)
- [Using Apache](https://github.com/butlerx/wetty/blob/master/docs/apache.md)
- [Automatic Login](https://github.com/butlerx/wetty/blob/master/docs/auto-login.md)
- [Downloading Files](https://github.com/butlerx/wetty/blob/master/docs/downloading-files.md)
### What browsers are supported?
WeTTy supports all browsers that
[xterm.js supports](https://github.com/xtermjs/xterm.js#browser-support).
## Author
👤 **Cian Butler <butlerx@notthe.cloud>**
- Twitter: [@cianbutlerx](https://twitter.com/cianbutlerx)
- Github: [@butlerx](https://github.com/butlerx)
## Contributing ✨
Contributions, issues and feature requests are welcome!<br />Feel free to check
[issues page](https://github.com/butlerx/wetty/issues).
Please read the
[development docs](https://github.com/butlerx/wetty/blob/master/docs/development.md)
for installing from source and running is dev node
Thanks goes to these wonderful people
([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- 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>
</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>
</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>
</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>
</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>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the
[all-contributors](https://github.com/all-contributors/all-contributors)
specification. Contributions of any kind welcome!
## Show your support
Give a ⭐️ if this project helped you!
## 📝 License
Copyright © 2019
[Cian Butler <butlerx@notthe.cloud>](https://github.com/butlerx).<br /> This
project is [MIT](https://github.com/butlerx/wetty/blob/master/LICENSE) licensed.
---

12
docker-compose.yml

@ -1,5 +1,5 @@
---
version: "3.5"
version: '3.5'
services:
wetty:
build: .
@ -8,7 +8,7 @@ services:
tty: true
working_dir: /usr/src/app
ports:
- "3000:3000"
- '3000:3000'
environment:
SSHHOST: 'wetty-ssh'
SSHPORT: 22
@ -20,13 +20,17 @@ services:
volumes:
- ./bin/nginx.template:/etc/nginx/conf.d/wetty.template
ports:
- "80:80"
- '80:80'
environment:
- NGINX_DOMAIN=wetty.com
- NGINX_PORT=80
- WETTY_HOST=wetty
- WETTY_PORT=3000
command: /bin/bash -c "envsubst '$${NGINX_DOMAIN},$${NGINX_PORT},$${WETTY_HOST},$${WETTY_PORT}' < /etc/nginx/conf.d/wetty.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
command: >-
/bin/bash -c "envsubst
'$${NGINX_DOMAIN},$${NGINX_PORT},$${WETTY_HOST},$${WETTY_PORT}' <
/etc/nginx/conf.d/wetty.template > /etc/nginx/conf.d/default.conf && nginx
-g 'daemon off;'"
wetty-ssh:
build:

20
docs/README.md

@ -1,6 +1,20 @@
# Docs
## Getting started
![WeTTy](./terminal.png?raw=true)
- [Running as daemon](./service.md)
- [SSL Support](./ssl.md)
- [Using NGINX](./nginx.md)
- [Using Apache](./apache.md)
- [Automatic Login](./auto-login.md)
- [Downloading Files](./downloading-files.md)
- [Development Docs](./development.md)
## API
For WeTTy options and event details please refer to the [api docs](./API.md)
### Getting started
WeTTy is event driven. To Spawn a new server call `wetty.start()` with no
arguments.
@ -18,7 +32,3 @@ wetty.start(/* server settings, see Options */).then(() => {
/* code you want to execute */
});
```
## API
For WeTTy options and event details please refer to the [api docs](./API.md)

28
docs/apache.md

@ -0,0 +1,28 @@
## Run wetty behind nginx or apache
As said earlier you can use a proxy to add https to WeTTy.
**Note** that if your proxy is configured for https you should run WeTTy without
SSL
If your proxy uses a base path other than `/wetty`, specify the path with the
`--base` flag, or the `BASE` environment variable.
The following confs assume you want to serve wetty on the url
`example.com/wetty` and are running wetty with the default base and serving it
on the same server
Put the following configuration in apache's conf:
```apache
RewriteCond %{REQUEST_URI} ^/wetty/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /wetty/socket.io/(.*) ws://localhost:3000/wetty/socket.io/$1 [P,L]
<LocationMatch ^/wetty/(.*)>
DirectorySlash On
Require all granted
ProxyPassMatch http://127.0.0.1:3000
ProxyPassReverse /wetty/
</LocationMatch>
```

22
docs/auto-login.md

@ -0,0 +1,22 @@
# Auto Login
WeTTY Supports a form of auto login by passing a users password though url
params.
This is not a required feature and the security implications for passing the
password in the url will have to be considered by the user.
## Requirements
For auto-login feature you'll need sshpass installed
- `apt-get install sshpass` (debian eg. Ubuntu)
- `yum install sshpass` (red hat flavours eg. CentOs)
## Usage
You can also pass the ssh password as an optional query parameter to auto-login
the user like this (Only while running WeTTy as a non root account or when
specifying the ssh host):
`http://yourserver:3000/wetty/ssh/<username>?pass=<password>`

20
docs/development.md

@ -0,0 +1,20 @@
# Installation from Source
WeTTy can be installed from source or from npm.
To install from source run:
```bash
$ git clone https://github.com/butlerx/wetty.git
$ cd wetty
$ yarn
$ yarn build
```
## Development Env
To run WeTTy in dev mode you can run `yarn dev` this will build latest version
of WeTTy and start the server pointing at `localhost` on port `22`. The Dev
server will rebuild WeTTy when ever a file is edited and restart the server with
the new build. Any current ssh session in WeTTy will be killed and the user
logged out.

27
docs/docker.md

@ -0,0 +1,27 @@
# Dockerized Version
WeTTy can be run from a container to ssh to a remote host or the host system.
This is handy for quick deployments. Just modify `docker-compose.yml` for your
host and run:
```sh
$ docker-compose up -d
```
This will start 2 containers, one will be WeTTy container running ssh client the
other will be a container running ssh server.
Visit the appropriate URL in your browser
(`[localhost|$(boot2docker ip)]:PORT`).
The default username is `term` and the password is `term`, if you did not modify
`SSHHOST`
In the docker version all flags can be accessed as environment variables such as
`SSHHOST` or `SSHPORT`.
If you dont want to build the image yourself just remove the line `build; .`
If you wish to use the WeTTy container in prod just modify the WeTTy container
to have `SSHHOST` point to the server you want to ssh to and remove the ssh
server container.

26
docs/downloading-file.md

@ -0,0 +1,26 @@
# File Downloading
WeTTy supports file downloads by printing terminal escape sequences between a
base64 encoded file.
The terminal escape sequences used are `^[[5i` and `^[[4i` (VT100 for "enter
auto print" and "exit auto print" respectively -
https://vt100.net/docs/tp83/appendixc.html).
An example of a helper script that prints the terminal escape characters and
base64s stdin:
```bash
$ cat wetty-download.sh
#!/bin/sh
echo '^[[5i'$(cat /dev/stdin | base64)'^[[4i'
```
You are then able to download files via WeTTy!
```bash
$ cat my-pdf-file.pdf | ./wetty-download.sh
```
WeTTy will then issue a popup like the following that links to a local file
blob: `Download ready: file-20191015233654.pdf`

38
docs/flags.md

@ -0,0 +1,38 @@
# Flags
WeTTy can be run with the `--help` flag to get a full list of flags.
## Server Port
WeTTy runs on port `3000` by default. You can change the default port by
starting with the `--port` or `-p` flag.
## SSH Host
If WeTTy is run as root while the host is set as the local machine it will use
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
connect to.
## Default User
You can specify the default user used to ssh to a host using the `--sshuser`.
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.
## 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`.
## WeTTy URL
If you'd prefer an HTTP base prefix other than `/wetty`, you can specify that
with `--base`.
**Do not set this to `/ssh/${something}`, as this will break username matching
code.**

21
docs/https.md

@ -0,0 +1,21 @@
# HTTPS
Always use HTTPS especially with a terminal to your server. You can add HTTPS by
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:
```bash
wetty --sslkey key.pem --sslcert cert.pem
```
If you don't have SSL certificates from a CA you can create a self signed
certificate using this command:
```bash
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```

33
docs/nginx.md

@ -0,0 +1,33 @@
## Run WeTTy behind nginx
As said earlier you can use Nginx to add https to WeTTy.
**Note** that if your proxy is configured for https you should run WeTTy without
SSL
If you configure nginx to use a base path other than `/WeTTy`, then specify that
path with the `--base` flag, or the `BASE` environment variable.
The following confs assume you want to serve WeTTy on the url
`example.com/wetty` and are running WeTTy with the default base and serving it
on the same server
For a more detailed look see the [nginx.conf](../bin/nginx.template) used for
testing
Put the following configuration in your nginx conf:
```nginx
location ^~ /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";
proxy_read_timeout 43200000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
```

29
docs/service.md

@ -0,0 +1,29 @@
## Run WeTTy as a service daemon
WeTTy can be run as a daemon on your service init confs and systemd services are
bundled with the npm package to make this easier.
### init.d
```bash
$ yarn global add wetty
$ sudo cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.conf /etc/init
$ sudo start wetty
```
### systemd
```bash
$ yarn global add wetty
$ cp ~/.config/yarn/global/node_modules/wetty/bin/wetty.service ~/.config/systemd/user/
$ systemctl --user enable wetty
$ systemctl --user start wetty
```
This will start WeTTy on port 3000. If you want to change the port or redirect
stdout/stderr you should change the last line in `wetty.conf` file, something
like this:
```systemd
exec sudo -u root wetty -p 80 >> /var/log/wetty.log 2>&1
```

0
terminal.png → docs/terminal.png

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

2
index.js

@ -1,5 +1,5 @@
#! /usr/bin/env node
/* eslint-disable typescript/no-var-requires */
/* eslint-disable @typescript-eslint/no-var-requires */
const yargs = require('yargs');
const wetty = require('./dist').default;

65
package.json

@ -1,24 +1,29 @@
{
"name": "wetty",
"version": "1.1.9",
"version": "1.2.0",
"description": "WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/krishnasrinivas/wetty",
"homepage": "https://github.com/butlerx/wetty",
"repository": {
"type": "git",
"url": "git://github.com/krishnasrinivas/wetty.git"
"url": "git://github.com/butlerx/wetty.git"
},
"author": {
"name": "Cian Butler",
"email": "butlerx@notthe.cloud",
"url": "cianbutler.ie"
},
"author": "Krishna Srinivas <krishna.srinivas@gmail.com> (https://github.com/krishnasrinivas)",
"license": "MIT",
"bugs": {
"url": "https://github.com/krishnasrinivas/wetty/issues"
"url": "https://github.com/butlerx/wetty/issues"
},
"main": "index.js",
"scripts": {
"lint": "eslint --ext .ts .",
"build": "babel-node node_modules/.bin/webpack",
"start": "node .",
"contributor": "all-contributors",
"dev": "NODE_ENV=development concurrently --kill-others --success first \"babel-node node_modules/.bin/webpack --watch\" \"nodemon .\"",
"prepublishOnly": "NODE_ENV=production yarn build"
"lint": "eslint --ext .ts,.js .",
"prepublishOnly": "NODE_ENV=production yarn build",
"start": "node ."
},
"husky": {
"hooks": {
@ -86,6 +91,7 @@
"@types/yargs": "^13.0.2",
"@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",
"concurrently": "^4.1.2",
@ -96,7 +102,8 @@
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0",
"file-loader": "^4.2.0",
"husky": "^3.0.5",
"git-authors-cli": "^1.0.18",
"husky": "^3.0.9",
"lint-staged": "~9.2.5",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
@ -110,25 +117,37 @@
"webpack-node-externals": "^1.7.2"
},
"contributors": [
"Andreas Kloeckner <inform@tiker.net>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Boyan Rabchev <TELERIK\\rabchev@rabchevlnx.telerik.com>",
"Krishna Srinivas <krishna.srinivas@gmail.com>",
"Boyan Rabchev <boyan@rabchev.com>",
"Cian Butler <butlerx@notthe.cloud>",
"Farhan Khan <khanzf@gmail.com>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Strubbl <github@linux4tw.de>",
"Oleg Kurapov <ok@2sheds.ru>",
"Anthony Jund <antonyjund@gmail.com>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>",
"Henri <henri.ducrocq@gmail.com>",
"Imuli <i@imu.li>",
"James Turnbull <james@lovedthanlost.net>",
"Koushik M.L.N <mln02koushik@gmail.com>",
"cbutler <cbutler@demonware.net>",
"mirtouf <mirtouf@gmail.com>",
"Denis Kramer <d.kramer@soton.ac.uk>",
"Jarrett Gilliam <jarrettgilliam@gmail.com>",
"Kasper Holbek Jensen <kholbekj@gmail.com>",
"Krishna Srinivas <krishna@minio.io>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"Nathan LeClaire <nathan.leclaire@docker.com>",
"Andreas Kloeckner <inform@tiker.net>",
"Ben Letchford <contact@benl.com.au>",
"Bertrand Roussel <broussel@sierrawireless.com>",
"Farhan Khan <khanzf@gmail.com>",
"Felix Bartels <felix@host-consultants.de>",
"Felix Pojtinger <felix@pojtinger.com>",
"James Turnbull <james@lovedthanlost.net>",
"Josh Samuelson <js@puppetlabs.com>",
"Jurre Vriesen <jurrevriesen@gmail.com>",
"Kasper Holbek Jensen <kholbekj@gmail.com>",
"Krzysztof Suszyński <krzysztof.suszynski@wavesoftware.pl>",
"Matthew Piercey <piercey.matthew@gmail.com>",
"Neale Pickett <neale@woozle.org>",
"Robert <robert@n5qm.com>",
"Strubbl <github@linux4tw.de>",
"koushikmln <mln02koushik@gmail.com>",
"mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>",
"Ben Letchford <contact@benl.com.au>"
"Tri Nguyen <tri@tridnguyen.com>",
"harryleesan <harry.lee.san.temp@gmail.com>"
]
}

56
src/client/copyToClipboard.ts

@ -1,26 +1,38 @@
// NOTE text selection on double click or select
const copyToClipboard = (text: string) : boolean => {
if (window.clipboardData && window.clipboardData.setData) {
window.clipboardData.setData("Text", text);
return true;
} if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
const textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
return true;
} catch (ex) {
console.warn("Copy to clipboard failed.", ex);
return false;
} finally {
document.body.removeChild(textarea);
}
export function copySelected(text: string): boolean {
if (window.clipboardData && window.clipboardData.setData) {
window.clipboardData.setData('Text', text);
return true;
}
if (
document.queryCommandSupported &&
document.queryCommandSupported('copy')
) {
const textarea = document.createElement('textarea');
textarea.textContent = text;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return true;
} catch (ex) {
console.warn('Copy to clipboard failed.', ex);
return false;
} finally {
document.body.removeChild(textarea);
}
console.warn("Copy to clipboard failed.");
return false;
}
console.warn('Copy to clipboard failed.');
return false;
}
export default copyToClipboard;
export function copyShortcut(e: KeyboardEvent): boolean {
// Ctrl + Shift + C
if (e.ctrlKey && e.shiftKey && e.keyCode === 67) {
e.preventDefault();
document.execCommand('copy');
return false;
}
return true;
}

11
src/client/disconnect.ts

@ -0,0 +1,11 @@
import { isUndefined, isNull } 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);
}

55
src/client/download.ts

@ -0,0 +1,55 @@
import * as fileType from 'file-type';
import Toastify from 'toastify-js';
export const FILE_BEGIN = '\u001b[5i';
export const FILE_END = '\u001b[4i';
export let fileBuffer = [];
export function onCompleteFile() {
let bufferCharacters = fileBuffer.join('');
bufferCharacters = bufferCharacters.substring(
bufferCharacters.lastIndexOf(FILE_BEGIN) + FILE_BEGIN.length,
bufferCharacters.lastIndexOf(FILE_END)
);
// Try to decode it as base64, if it fails we assume it's not base64
try {
bufferCharacters = window.atob(bufferCharacters);
} catch (err) {
// Assuming it's not base64...
}
const bytes = new Uint8Array(bufferCharacters.length);
for (let i = 0; i < bufferCharacters.length; i += 1) {
bytes[i] = bufferCharacters.charCodeAt(i);
}
let mimeType = 'application/octet-stream';
let fileExt = '';
const typeData = fileType(bytes);
if (typeData) {
mimeType = typeData.mime;
fileExt = typeData.ext;
}
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);
fileBuffer = [];
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();
}

2
src/client/elements.ts

@ -0,0 +1,2 @@
export const overlay = document.getElementById('overlay');
export const terminal = document.getElementById('terminal');

207
src/client/index.ts

@ -1,152 +1,74 @@
import { Terminal } from 'xterm';
import { isUndefined } from 'lodash';
import * as io from 'socket.io-client';
import { fit } from 'xterm/lib/addons/fit/fit';
import * as fileType from 'file-type';
import Toastify from 'toastify-js';
import copyToClipboard from "./copyToClipboard";
import { isNull } from 'lodash';
import { socket } from './socket';
import { overlay, terminal } from './elements';
import { fileBuffer, onCompleteFile, FILE_BEGIN, FILE_END } from './download';
import verifyPrompt from './verify';
import disconnect from './disconnect';
import mobileKeyboard from './mobile';
import resize from './resize';
import loadOptions from './options';
import { copySelected, copyShortcut } from './copyToClipboard';
import './wetty.scss';
import './favicon.ico';
const userRegex = new RegExp('ssh/[^/]+$');
const trim = (str: string): string => str.replace(/\/*$/, '');
const socketBase = trim(window.location.pathname).replace(userRegex, '');
const socket = io(window.location.origin, {
path: `${trim(socketBase)}/socket.io`,
});
const FILE_BEGIN = '\u001b[5i';
const FILE_END = '\u001b[4i';
socket.on('connect', () => {
const term = new Terminal();
let fileBuffer = [];
term.open(document.getElementById('terminal'));
const defaultOptions = {
fontSize: 14
};
let options: object;
if (isNull(terminal)) return;
term.open(terminal);
try {
if (localStorage.options === undefined) {
options = defaultOptions;
} else {
options = JSON.parse(localStorage.options);
}
} catch {
options = defaultOptions;
}
Object.keys(options).forEach(key => {
const value = options[key];
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') || {value: code};
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);
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(term)();
} 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();
});
resize();
} catch {
// skip
editor.classList.add('error');
}
});
document.getElementById('overlay').style.display = 'none';
document.querySelector('#options .toggler').addEventListener('click', e => {
document.getElementById('options').classList.toggle('opened');
e.preventDefault();
});
window.addEventListener('beforeunload', handler, false);
/*
term.scrollPort_.screen_.setAttribute('contenteditable', 'false');
*/
term.attachCustomKeyEventHandler(e => {
// Ctrl + Shift + C
if (e.ctrlKey && e.shiftKey && e.keyCode === 67) {
e.preventDefault();
document.execCommand('copy');
return false;
}
return true;
});
// NOTE copytoclipboard
document.addEventListener('mouseup', () => {
if (term.hasSelection())
copyToClipboard(term.getSelection())
}, false);
function resize(): void {
fit(term);
socket.emit('resize', {
cols: term.cols,
rows: term.rows
});
}
window.onresize = resize;
resize();
term.focus();
function kill(data: string): void {
disconnect(data);
}
if (!isNull(overlay)) overlay.style.display = 'none';
window.addEventListener('beforeunload', verifyPrompt, false);
function onCompleteFile() {
let bufferCharacters = fileBuffer.join('');
bufferCharacters = bufferCharacters.substring(bufferCharacters.lastIndexOf(FILE_BEGIN) + FILE_BEGIN.length, bufferCharacters.lastIndexOf(FILE_END));
term.attachCustomKeyEventHandler(copyShortcut);
// Try to decode it as base64, if it fails we assume it's not base64
try {
bufferCharacters = window.atob(bufferCharacters);
} catch (err) {
// Assuming it's not base64...
}
const bytes = new Uint8Array(bufferCharacters.length);
for (let i = 0; i < bufferCharacters.length; i += 1) {
bytes[i] = bufferCharacters.charCodeAt(i);
}
document.addEventListener(
'mouseup',
() => {
if (term.hasSelection()) copySelected(term.getSelection());
},
false
);
let mimeType = 'application/octet-stream';
let fileExt = '';
const typeData = fileType(bytes);
if (typeData) {
mimeType = typeData.mime;
fileExt = typeData.ext;
}
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);
fileBuffer = [];
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();
}
window.onresize = resize(term);
resize(term)();
term.focus();
mobileKeyboard();
term.on('data', data => {
socket.emit('input', data);
@ -184,24 +106,11 @@ socket.on('connect', () => {
})
.on('login', () => {
term.writeln('');
resize();
resize(term)();
})
.on('logout', kill)
.on('disconnect', kill)
.on('logout', disconnect)
.on('disconnect', disconnect)
.on('error', (err: string | null) => {
if (err) disconnect(err);
});
});
function disconnect(reason: string): void {
document.getElementById('overlay').style.display = 'block';
if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason;
window.removeEventListener('beforeunload', handler, false);
}
function handler(e: {
returnValue: string
}): string {
e.returnValue = 'Are you sure?';
return e.returnValue;
}

14
src/client/mobile.ts

@ -0,0 +1,14 @@
import { isNull } from 'lodash';
export default function mobileKeyboard(): void {
const [screen] = document.getElementsByClassName('xterm-screen');
if (isNull(screen)) return;
screen.setAttribute('contenteditable', 'true');
screen.setAttribute('spellcheck', 'false');
screen.setAttribute('autocorrect', 'false');
screen.setAttribute('autocomplete', 'false');
screen.setAttribute('autocapitalize', 'false');
/*
term.scrollPort_.screen_.setAttribute('contenteditable', 'false');
*/
}

12
src/client/options.ts

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

10
src/client/resize.ts

@ -0,0 +1,10 @@
import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit';
import { socket } from './socket';
export default function resize(term: Terminal): Function {
return (): void => {
fit(term);
socket.emit('resize', { cols: term.cols, rows: term.rows });
};
}

9
src/client/socket.ts

@ -0,0 +1,9 @@
import * as io from 'socket.io-client';
const userRegex = new RegExp('ssh/[^/]+$');
export const trim = (str: string): string => str.replace(/\/*$/, '');
const socketBase = trim(window.location.pathname).replace(userRegex, '');
export const socket = io(window.location.origin, {
path: `${trim(socketBase)}/socket.io`,
});

4
src/client/verify.ts

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

2
src/server/buffer.ts

@ -2,7 +2,7 @@ import { createInterface } from 'readline';
ask('Enter your username');
export default function ask(question: string): Promise<string> {
function ask(question: string): Promise<string> {
const r = createInterface({
input: process.stdin,
output: process.stdout,

18
src/server/cli/index.ts

@ -0,0 +1,18 @@
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, ssl } = unWrapArgs(opts);
WeTTy(ssh, server, command, ssl).catch(err => {
logger.error(err);
process.exitCode = 1;
});
} else {
yargs.showHelp();
process.exitCode = 0;
}
}

20
src/server/cli/options.ts

@ -0,0 +1,20 @@
export interface Options {
sshhost: string;
sshport: number;
sshuser: string;
sshauth: string;
sshkey?: string;
sshpass?: string;
sslkey?: string;
sslcert?: string;
base: string;
host: string;
port: number;
title: string;
command?: string;
bypasshelmet?: boolean;
}
export interface CLI extends Options {
help: boolean;
}

30
src/server/cli/parseArgs.ts

@ -0,0 +1,30 @@
import { isUndefined } from 'lodash';
import { Options } from './options';
import { SSL, SSH, Server } from '../interfaces';
export function unWrapArgs(
args: Options
): { ssh: SSH; server: Server; command?: string; ssl?: SSL } {
return {
ssh: {
user: args.sshuser,
host: args.sshhost,
auth: args.sshauth,
port: args.sshport,
pass: args.sshpass,
key: args.sshkey,
},
server: {
base: args.base,
host: args.host,
port: args.port,
title: args.title,
bypasshelmet: args.bypasshelmet || false,
},
command: args.command,
ssl:
isUndefined(args.sslkey) || isUndefined(args.sslcert)
? undefined
: { key: args.sslkey, cert: args.sslcert },
};
}

101
src/server/command.ts

@ -1,101 +0,0 @@
import * as url from 'url';
import { Socket } from 'socket.io';
import logger from './logger';
import { SSH } from './interfaces';
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);
const getRemoteAddress = (remoteAddress: string): string =>
remoteAddress.split(':')[3] === undefined
? 'localhost'
: remoteAddress.split(':')[3];
export default (
{
request: {
headers: { referer },
},
client: {
conn: { remoteAddress },
},
}: Socket,
{ user, host, port, auth, pass, key }: SSH,
command: string
): { args: string[]; user: boolean } => ({
args: localhost(host)
? loginOptions(command, remoteAddress)
: sshOptions(
urlArgs(referer, {
host: address(referer, user, host),
port: `${port}`,
pass,
command,
auth,
}),
key
),
user:
localhost(host) ||
user !== '' ||
user.includes('@') ||
address(referer, user, host).includes('@'),
});
function parseCommand(command: string, path?: string): string {
if (command === 'login' && path === undefined) return '';
return path !== undefined
? `$SHELL -c "cd ${path};${command === 'login' ? '$SHELL' : command}"`
: command;
}
function sshOptions(
{ pass, path, command, host, port, auth }: { [s: string]: string },
key?: string
): string[] {
const cmd = parseCommand(command, path);
const sshRemoteOptsBase = [
'ssh',
host,
'-t',
'-p',
port,
'-o',
`PreferredAuthentications=${auth}`,
];
logger.info(`Authentication Type: ${auth}`);
if (key) {
return sshRemoteOptsBase.concat(['-i', key, cmd]);
}
if (pass) {
return ['sshpass', '-p', pass].concat(sshRemoteOptsBase, [cmd]);
}
if (auth === 'none') {
sshRemoteOptsBase.splice(sshRemoteOptsBase.indexOf('-o'), 2);
return sshRemoteOptsBase.concat([cmd]);
}
if (cmd === '') {
return sshRemoteOptsBase;
}
return sshRemoteOptsBase.concat([cmd]);
}
function loginOptions(command: string, remoteAddress: string): string[] {
return command === 'login'
? [command, '-h', getRemoteAddress(remoteAddress)]
: [command];
}
function address(referer: string, user: string, host: string): string {
const match = referer.match('.+/ssh/([^/]+)$');
const fallback = user ? `${user}@${host}` : host;
return match ? `${match[1]}@${host}` : fallback;
}

9
src/server/command/address.ts

@ -0,0 +1,9 @@
export default 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;
}

47
src/server/command/index.ts

@ -0,0 +1,47 @@
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 }: SSH,
command: string
): { args: string[]; user: boolean } => ({
args: localhost(host)
? loginOptions(command, remoteAddress)
: sshOptions(
urlArgs(referer, {
host: address(referer, user, host),
port: `${port}`,
pass: pass || '',
command,
auth,
}),
key
),
user:
localhost(host) ||
user !== '' ||
user.includes('@') ||
address(referer, user, host).includes('@'),
});

15
src/server/command/login.ts

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

8
src/server/command/parse.ts

@ -0,0 +1,8 @@
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;
}

33
src/server/command/ssh.ts

@ -0,0 +1,33 @@
import { isUndefined } from 'lodash';
import parseCommand from './parse';
import logger from '../utils/logger';
export default function sshOptions(
{ pass, path, command, host, port, auth }: { [s: string]: string },
key?: string
): string[] {
const cmd = parseCommand(command, path);
const sshRemoteOptsBase = [
'ssh',
host,
'-t',
'-p',
port,
'-o',
`PreferredAuthentications=${auth}`,
];
logger.info(`Authentication Type: ${auth}`);
if (!isUndefined(key)) {
return sshRemoteOptsBase.concat(['-i', key, cmd]);
}
if (pass !== '') {
return ['sshpass', '-p', pass].concat(sshRemoteOptsBase, [cmd]);
}
if (auth === 'none') {
sshRemoteOptsBase.splice(sshRemoteOptsBase.indexOf('-o'), 2);
}
if (cmd === '') {
return sshRemoteOptsBase;
}
return sshRemoteOptsBase.concat([cmd]);
}

3
src/server/emitter.ts

@ -1,3 +0,0 @@
import WeTTy from './wetty';
export default new WeTTy();

85
src/server/index.ts

@ -1,85 +1,4 @@
import * as yargs from 'yargs';
import logger from './logger';
import wetty from './emitter';
import WeTTy from './wetty';
import init from './cli';
export interface Options {
sshhost: string;
sshport: number;
sshuser: string;
sshauth: string;
sshkey?: string;
sshpass?: string;
sslkey?: string;
sslcert?: string;
base: string;
host: string;
port: number;
title: string;
command?: string;
bypasshelmet?: boolean;
}
interface CLI extends Options {
help: boolean;
}
export default class Server {
public static start({
sshuser,
sshhost,
sshauth,
sshport,
sshkey,
sshpass,
base,
host,
port,
title,
command,
sslkey,
sslcert,
bypasshelmet,
}: Options): Promise<void> {
wetty
.on('exit', ({ code, msg }: { code: number; msg: string }) => {
logger.info(`Exit with code: ${code} ${msg}`);
})
.on('disconnect', () => {
logger.info('disconnect');
})
.on('spawn', ({ msg }) => logger.info(msg))
.on('connection', ({ msg, date }) => logger.info(`${date} ${msg}`))
.on('server', ({ msg }) => logger.info(msg))
.on('debug', (msg: string) => logger.debug(msg));
return wetty.start(
{
user: sshuser,
host: sshhost,
auth: sshauth,
port: sshport,
pass: sshpass,
key: sshkey,
},
{ base, host, port, title, bypasshelmet},
command,
{ key: sslkey, cert: sslcert }
);
}
public static get wetty(): WeTTy {
return wetty;
}
public static init(opts: CLI): void {
if (!opts.help) {
this.start(opts).catch(err => {
logger.error(err);
process.exitCode = 1;
});
} else {
yargs.showHelp();
process.exitCode = 0;
}
}
}
export default { start: WeTTy, init };

4
src/server/interfaces.ts

@ -8,8 +8,8 @@ export interface SSH {
}
export interface SSL {
key?: string;
cert?: string;
key: string;
cert: string;
}
export interface SSLBuffer {

32
src/server/logger.ts

@ -1,32 +0,0 @@
import { createLogger, format, transports } from 'winston';
const { combine, timestamp, label, printf, colorize } = format;
const logger = createLogger({
format:
process.env.NODE_ENV === 'development'
? combine(
colorize({ all: true }),
label({ label: 'Wetty' }),
timestamp(),
printf(
info =>
`${info.timestamp} [${info.label}] ${info.level}: ${info.message}`
)
)
: format.json(),
transports: [
new transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
handleExceptions: true,
}),
],
});
logger.stream = {
write(message: string): void {
logger.info(message);
},
};
export default logger;

93
src/server/server.ts

@ -1,93 +0,0 @@
import * as compression from 'compression';
import * as express from 'express';
import * as favicon from 'serve-favicon';
import * as helmet from 'helmet';
import * as http from 'http';
import * as https from 'https';
import * as path from 'path';
import * as socket from 'socket.io';
import { isUndefined } from 'lodash';
import * as morgan from 'morgan';
import logger from './logger';
import events from './emitter';
import { SSLBuffer, Server } from './interfaces';
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);
events.emit(
'debug',
`key: ${key}, cert: ${cert}, port: ${port}, base: ${base}, title: ${title}`
);
const html = (
req: express.Request,
res: express.Response
): express.Response => {
const resourcePath = /^\/ssh\//.test(req.url.replace(base, '/')) ? '../' : '';
res.send(`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>${title}</title>
<link rel="stylesheet" href="${resourcePath}public/index.css" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
</head>
<body>
<div id="overlay">
<div class="error">
<div id="msg"></div>
<input type="button" onclick="location.reload();" value="reconnect" />
</div>
</div>
<div id="options">
<a class="toggler"
href="#"
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>
</body>
</html>`);
}
const app = express();
app
.use(morgan('combined', { stream: logger.stream }))
.use(compression())
.use(favicon(path.join(distDir, 'favicon.ico')))
.use(`${basePath}/public`, express.static(distDir))
.use((req, res, next) => {
if (req.url === basePath) res.redirect(301, `${req.url }/`);
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());
}
app.get(basePath, html).get(`${basePath}/ssh/:user`, html);
return socket(
!isUndefined(key) && !isUndefined(cert)
? https.createServer({ key, cert }, app).listen(port, host, () => {
events.server(port, 'https');
})
: http.createServer(app).listen(port, host, () => {
events.server(port, 'http');
}),
{ path: `${basePath}/socket.io` }
);
}

35
src/server/socketServer/html.ts

@ -0,0 +1,35 @@
import * as express from 'express';
export default (base: string, title: string) => (
req: express.Request,
res: express.Response
): express.Response => {
const resourcePath = /^\/ssh\//.test(req.url.replace(base, '/')) ? '../' : '';
res.send(`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>${title}</title>
<link rel="stylesheet" href="${resourcePath}public/index.css" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
</head>
<body>
<div id="overlay">
<div class="error">
<div id="msg"></div>
<input type="button" onclick="location.reload();" value="reconnect" />
</div>
</div>
<div id="options">
<a class="toggler"
href="#"
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>
</body>
</html>`);
};

70
src/server/socketServer/index.ts

@ -0,0 +1,70 @@
import * as compression from 'compression';
import * as express from 'express';
import * as favicon from 'serve-favicon';
import * as helmet from 'helmet';
import * as http from 'http';
import * as https from 'https';
import * as path from 'path';
import * as socket from 'socket.io';
import { isUndefined } from 'lodash';
import * as morgan from 'morgan';
import logger from '../utils/logger';
import { SSLBuffer, Server } from '../interfaces';
import html from './html';
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(morgan('combined', { stream: logger.stream }))
.use(compression())
.use(favicon(path.join(distDir, 'favicon.ico')))
.use(`${basePath}/public`, express.static(distDir))
.use((req, res, next) => {
if (req.url === basePath) res.redirect(301, `${req.url}/`);
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` }
);
}

11
src/server/ssl.ts

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

80
src/server/term.ts

@ -1,80 +0,0 @@
import { spawn } from 'node-pty';
import { isUndefined } from 'lodash';
import events from './emitter';
const xterm = {
name: 'xterm-256color',
cols: 80,
rows: 30,
cwd: process.cwd(),
env: process.env,
};
export default class Term {
public static spawn(socket: SocketIO.Socket, args: string[]): void {
const term = spawn('/usr/bin/env', args, xterm);
const address = args[0] === 'ssh' ? args[1] : 'localhost';
events.spawned(term.pid, address);
socket.emit('login');
term.on('exit', code => {
events.exited(code, term.pid);
socket.emit('logout');
socket
.removeAllListeners('disconnect')
.removeAllListeners('resize')
.removeAllListeners('input');
});
term.on('data', data => {
socket.emit('data', data);
});
socket
.on('resize', ({ cols, rows }) => {
term.resize(cols, rows);
})
.on('input', input => {
if (!isUndefined(term)) term.write(input);
})
.on('disconnect', () => {
const { pid } = term;
term.kill();
events.exited(0, pid);
});
}
public static login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username
const remoteUser = socket.request.headers['remote-user'];
if (remoteUser) {
return new Promise(resolve => {
resolve(remoteUser);
});
}
// Request carries no username information
// Create terminal and ask user for username
const term = spawn(
'/usr/bin/env',
['node', `${__dirname}/buffer.js`],
xterm
);
let buf = '';
return new Promise((resolve, reject) => {
term.on('exit', () => {
resolve(buf);
});
term.on('data', data => {
socket.emit('data', data);
});
socket
.on('input', (input: string) => {
term.write(input);
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input;
})
.on('disconnect', () => {
term.kill();
reject();
});
});
}
}

5
src/server/utils/index.ts

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

26
src/server/utils/logger.ts

@ -0,0 +1,26 @@
import { createLogger, format, transports } from 'winston';
const { combine, timestamp, label, simple, json, colorize } = format;
const logger = createLogger({
format: combine(
colorize({ all: process.env.NODE_ENV === 'development' }),
label({ label: 'Wetty' }),
timestamp(),
process.env.NODE_ENV === 'development' ? simple() : json()
),
transports: [
new transports.Console({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
handleExceptions: true,
}),
],
});
logger.stream = {
write(message: string): void {
logger.info(message);
},
};
export default logger;

14
src/server/utils/ssl.ts

@ -0,0 +1,14 @@
import { readFile } from 'fs-extra';
import { resolve } from 'path';
import { isUndefined } from 'lodash';
import { SSL, SSLBuffer } from '../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)),
]);
return { key, cert };
}

136
src/server/wetty.ts

@ -1,136 +0,0 @@
/**
* Create WeTTY server
* @module WeTTy
*/
import * as EventEmitter from 'events';
import server from './server';
import getCommand from './command';
import term from './term';
import loadSSL from './ssl';
import { SSL, SSH, SSLBuffer, Server } from './interfaces';
export default class WeTTy extends EventEmitter {
/**
* Starts WeTTy Server
* @name start
*/
public start(
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 = '',
ssl?: SSL
): Promise<void> {
return loadSSL(ssl).then((sslBuffer: SSLBuffer) => {
if (ssh.key) {
this.emit(
'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
*/
this.emit('connection', {
msg: `Connection accepted.`,
date: new Date(),
});
const { args, user: sshUser } = getCommand(socket, ssh, command);
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`);
if (sshUser) {
term.spawn(socket, args);
} else {
term
.login(socket)
.then((username: string) => {
this.emit('debug', `username: ${username.trim()}`);
args[1] = `${username.trim()}@${args[1]}`;
this.emit('debug', `cmd : ${args.join(' ')}`);
return term.spawn(socket, args);
})
.catch(() => this.disconnected());
}
});
});
}
/**
* terminal spawned
*
* @fires module:WeTTy#spawn
*/
public spawned(pid: number, address: string): void {
/**
* Terminal process spawned
* @event WeTTy#spawn
* @name spawn
* @type {object}
*/
this.emit('spawn', {
msg: `PID=${pid} STARTED on behalf of ${address}`,
pid,
address,
});
}
/**
* terminal exited
*
* @fires WeTTy#exit
*/
public exited(code: number, pid: number): void {
/**
* Terminal process exits
* @event WeTTy#exit
* @name exit
*/
this.emit('exit', { code, msg: `PID=${pid} ENDED` });
}
/**
* Disconnect from WeTTY
*
* @fires WeTTy#disconnet
*/
private disconnected(): void {
/**
* @event WeTTY#disconnect
* @name disconnect
*/
this.emit('disconnect');
}
/**
* Wetty server started
* @fires WeTTy#server
*/
public server(port: number, connection: string): void {
/**
* @event WeTTy#server
* @type {object}
* @name server
*/
this.emit('server', {
msg: `${connection} on port ${port}`,
port,
connection,
});
}
}

71
src/server/wetty/index.ts

@ -0,0 +1,71 @@
/**
* Create WeTTY server
* @module WeTTy
*/
import server from '../socketServer';
import getCommand from '../command';
import { spawn, login } from './term';
import { logger, loadSSL } from '../utils';
import { SSL, SSH, 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 = '',
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);
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

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

34
src/server/wetty/term/login.ts

@ -0,0 +1,34 @@
import { spawn } from 'node-pty';
import { xterm } from './xterm';
export function login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username
const remoteUser = socket.request.headers['remote-user'];
if (remoteUser) {
return new Promise(resolve => {
resolve(remoteUser);
});
}
// Request carries no username information
// Create terminal and ask user for username
const term = spawn('/usr/bin/env', ['node', `${__dirname}/buffer.js`], xterm);
let buf = '';
return new Promise((resolve, reject) => {
term.on('exit', () => {
resolve(buf);
});
term.on('data', data => {
socket.emit('data', data);
});
socket
.on('input', (input: string) => {
term.write(input);
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input;
})
.on('disconnect', () => {
term.kill();
reject();
});
});
}

40
src/server/wetty/term/spawn.ts

@ -0,0 +1,40 @@
import { spawn } from 'node-pty';
import { isUndefined } from 'lodash';
import { logger } from '../../utils';
import { xterm } from './xterm';
export default function spawnTerm(
socket: SocketIO.Socket,
args: string[]
): void {
const term = 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', {
pid,
address,
});
socket.emit('login');
term.on('exit', code => {
logger.info('Process exited', { code, pid });
socket.emit('logout');
socket
.removeAllListeners('disconnect')
.removeAllListeners('resize')
.removeAllListeners('input');
});
term.on('data', data => {
socket.emit('data', data);
});
socket
.on('resize', ({ cols, rows }) => {
term.resize(cols, rows);
})
.on('input', input => {
if (!isUndefined(term)) term.write(input);
})
.on('disconnect', () => {
term.kill();
logger.info('Process exited', { code: 0, pid });
});
}

15
src/server/wetty/term/xterm.ts

@ -0,0 +1,15 @@
import { IPtyForkOptions } from 'node-pty';
import { isUndefined } from 'lodash';
export const xterm: IPtyForkOptions = {
name: 'xterm-256color',
cols: 80,
rows: 30,
cwd: process.cwd(),
env: Object.assign(
{},
...Object.keys(process.env)
.filter((key: string) => !isUndefined(process.env[key]))
.map((key: string) => ({ [key]: process.env[key] }))
),
};

25
webpack.config.babel.js

@ -1,21 +1,22 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import path from 'path';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import nodeExternals from 'webpack-node-externals';
const template = override =>
({
mode: process.env.NODE_ENV || 'development',
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.ts', '.json', '.js', '.node'],
},
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
});
stats: {
colors: true,
},
...override,
});
const entry = (folder, file) =>
path.join(__dirname, 'src', folder, `${file}.ts`);

1552
yarn.lock

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