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
|
// 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) { |
if (window.clipboardData && window.clipboardData.setData) { |
||||
window.clipboardData.setData("Text", text); |
window.clipboardData.setData('Text', text); |
||||
return true; |
return true; |
||||
} if (document.queryCommandSupported && document.queryCommandSupported("copy")) { |
} |
||||
const textarea = document.createElement("textarea"); |
if ( |
||||
textarea.textContent = text; |
document.queryCommandSupported && |
||||
textarea.style.position = "fixed"; |
document.queryCommandSupported('copy') |
||||
document.body.appendChild(textarea); |
) { |
||||
textarea.select(); |
const textarea = document.createElement('textarea'); |
||||
try { |
textarea.textContent = text; |
||||
document.execCommand("copy"); |
textarea.style.position = 'fixed'; |
||||
return true; |
document.body.appendChild(textarea); |
||||
} catch (ex) { |
textarea.select(); |
||||
console.warn("Copy to clipboard failed.", ex); |
try { |
||||
return false; |
document.execCommand('copy'); |
||||
} finally { |
return true; |
||||
document.body.removeChild(textarea); |
} 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 WeTTy from './wetty'; |
||||
|
import init from './cli'; |
||||
|
|
||||
export interface Options { |
export default { start: WeTTy, init }; |
||||
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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
@ -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