committed by
GitHub
57 changed files with 2559 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 => { |
|||
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; |
|||
} |
|||
|
@ -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,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>`);
|
|||
}; |
@ -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