Browse Source
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 contributorpull/208/head
Cian Butler
5 years ago
committed by
GitHub
57 changed files with 2533 additions and 1480 deletions
@ -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 |
|||
} |
@ -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> |
|||
``` |
@ -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>` |
@ -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. |
@ -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. |
@ -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` |
@ -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.** |
@ -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 |
|||
``` |
@ -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; |
|||
} |
|||
``` |
@ -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 |
|||
``` |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@ -1,26 +1,38 @@ |
|||
// NOTE text selection on double click or select
|
|||
const copyToClipboard = (text: string) : boolean => { |
|||
export function copySelected(text: string): boolean { |
|||
if (window.clipboardData && window.clipboardData.setData) { |
|||
window.clipboardData.setData("Text", text); |
|||
window.clipboardData.setData('Text', text); |
|||
return true; |
|||
} if (document.queryCommandSupported && document.queryCommandSupported("copy")) { |
|||
const textarea = document.createElement("textarea"); |
|||
} |
|||
if ( |
|||
document.queryCommandSupported && |
|||
document.queryCommandSupported('copy') |
|||
) { |
|||
const textarea = document.createElement('textarea'); |
|||
textarea.textContent = text; |
|||
textarea.style.position = "fixed"; |
|||
textarea.style.position = 'fixed'; |
|||
document.body.appendChild(textarea); |
|||
textarea.select(); |
|||
try { |
|||
document.execCommand("copy"); |
|||
document.execCommand('copy'); |
|||
return true; |
|||
} catch (ex) { |
|||
console.warn("Copy to clipboard failed.", ex); |
|||
console.warn('Copy to clipboard failed.', ex); |
|||
return false; |
|||
} finally { |
|||
document.body.removeChild(textarea); |
|||
} |
|||
} |
|||
console.warn("Copy to clipboard failed."); |
|||
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; |
|||
} |
|||
|
@ -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); |
|||
} |
@ -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(); |
|||
} |
@ -0,0 +1,2 @@ |
|||
export const overlay = document.getElementById('overlay'); |
|||
export const terminal = document.getElementById('terminal'); |
@ -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'); |
|||
*/ |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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 }); |
|||
}; |
|||
} |
@ -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`, |
|||
}); |
@ -0,0 +1,4 @@ |
|||
export default function verifyPrompt(e: { returnValue: string }): string { |
|||
e.returnValue = 'Are you sure?'; |
|||
return e.returnValue; |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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 }, |
|||
}; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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('@'), |
|||
}); |
@ -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]; |
|||
} |
@ -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; |
|||
} |
@ -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]); |
|||
} |
@ -1,3 +0,0 @@ |
|||
import WeTTy from './wetty'; |
|||
|
|||
export default new WeTTy(); |
@ -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 }; |
|||
|
@ -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; |
@ -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` } |
|||
); |
|||
} |
@ -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>`);
|
|||
}; |
@ -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` } |
|||
); |
|||
} |
@ -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 }; |
|||
} |
@ -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(); |
|||
}); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
import logger from './logger'; |
|||
|
|||
export { logger }; |
|||
|
|||
export * from './ssl'; |
@ -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; |
@ -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 }; |
|||
} |
@ -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, |
|||
}); |
|||
} |
|||
} |
@ -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'); |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
} |
@ -0,0 +1,5 @@ |
|||
import spawn from './spawn'; |
|||
|
|||
export { spawn }; |
|||
|
|||
export * from './login'; |
@ -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(); |
|||
}); |
|||
}); |
|||
} |
@ -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 }); |
|||
}); |
|||
} |
@ -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] })) |
|||
), |
|||
}; |
File diff suppressed because it is too large
Loading…
Reference in new issue