Browse Source

Merge branch 'master' into master

pull/210/head
Mihir Kumar 6 years ago
committed by GitHub
parent
commit
d340518122
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-files.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. 69
      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. 211
      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. 34
      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. 1571
      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-files.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;

69
package.json

@ -1,24 +1,29 @@
{
"name": "wetty",
"version": "1.1.9",
"version": "1.2.1",
"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": {
@ -49,6 +54,8 @@
},
"preferGlobal": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"compression": "^1.7.4",
"express": "^4.17.1",
"file-type": "^12.3.0",
@ -86,6 +93,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 +104,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,27 +119,39 @@
"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>",
"Mihir Kumar <mihirpandey.13@gmail.com>"
"Mihir Kumar <mihirpandey.13@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>",
"Soura Dutta <duttasoura@gmail.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');

211
src/client/index.ts

@ -1,152 +1,80 @@
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 { library, dom } from "@fortawesome/fontawesome-svg-core";
import { faCogs } from "@fortawesome/free-solid-svg-icons/faCogs";
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';
// Setup for fontawesome
library.add(faCogs);
dom.watch();
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);
}
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}` : ''}`;
if (!isNull(overlay)) overlay.style.display = 'none';
window.addEventListener('beforeunload', verifyPrompt, false);
const blob = new Blob([new Uint8Array(bytes.buffer)], { type: mimeType });
const blobUrl = URL.createObjectURL(blob);
term.attachCustomKeyEventHandler(copyShortcut);
fileBuffer = [];
document.addEventListener(
'mouseup',
() => {
if (term.hasSelection()) copySelected(term.getSelection());
},
false
);
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 +112,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` }
);
}

34
src/server/socketServer/html.ts

@ -0,0 +1,34 @@
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" />
</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`);

1571
yarn.lock

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