Browse Source

Merge pull request #48 from butlerx/feature/types

replace esm with typescript
pull/126/head
Cian Butler 6 years ago
committed by GitHub
parent
commit
08cc4da170
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .babelrc
  2. 2
      .eslintignore
  3. 32
      .eslintrc.js
  4. 3
      .gitignore
  5. 2
      .prettierrc.js
  6. 15
      Dockerfile
  7. 3
      Dockerfile-ssh
  8. 104
      README.md
  9. 20
      docker-compose.yml
  10. 95
      index.js
  11. 30
      lib/command.mjs
  12. 149
      lib/emitter.mjs
  13. 100
      lib/index.mjs
  14. 46
      lib/server.mjs
  15. 11
      lib/ssl.mjs
  16. 124
      package.json
  17. 20
      public/index.html
  18. 0
      src/client/favicon.ico
  19. 31
      src/client/index.ts
  20. 0
      src/client/wetty.scss
  21. 39
      src/fit.js
  22. 6
      src/server/buffer.ts
  23. 94
      src/server/command.ts
  24. 3
      src/server/emitter.ts
  25. 80
      src/server/index.ts
  26. 18
      src/server/interfaces.ts
  27. 24
      src/server/logger.ts
  28. 84
      src/server/server.ts
  29. 11
      src/server/ssl.ts
  30. 25
      src/server/term.ts
  31. 131
      src/server/wetty.ts
  32. 22
      tsconfig.json
  33. 134
      webpack.config.babel.js
  34. 74
      webpack.config.js
  35. 5936
      yarn.lock

10
.babelrc

@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-typescript",
["@babel/env"]
],
"compact": true,
"plugins": [
"lodash"
]
}

2
.eslintignore

@ -1,3 +1,5 @@
node_modules/
.esm-cache
dist
public/
*hterm*

32
.eslintrc.js

@ -1,16 +1,32 @@
module.exports = {
parser: 'eslint-plugin-typescript/parser',
plugins: ['typescript', 'prettier'],
env: {
es6: true,
node: true,
browser: true
browser: true,
},
root: true,
extends: ["airbnb-base", "plugin:prettier/recommended"],
extends: [
'airbnb-base',
'plugin:typescript/recommended',
'plugin:prettier/recommended',
],
rules: {
"linebreak-style": ["error", "unix"],
"arrow-parens": ["error", "as-needed"],
"no-param-reassign": ["error", { props: false }],
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"no-use-before-define": ["error", { functions: false }]
}
'typescript/indent': 'off',
'linebreak-style': ['error', 'unix'],
'arrow-parens': ['error', 'as-needed'],
'no-param-reassign': ['error', { props: false }],
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-use-before-define': ['error', { functions: false }],
'typescript/no-use-before-define': ['error', { functions: false }],
},
settings: {
'import/resolver': {
'typescript-eslint-parser': ['.ts', '.tsx'],
node: {
extensions: ['.ts', '.js'],
},
},
},
};

3
.gitignore

@ -13,6 +13,7 @@ logs
results
npm-debug.log
node_modules/*
node_modules
.esm-cache
dist
.idea

2
.prettierrc.js

@ -4,7 +4,7 @@ module.exports = {
proseWrap: 'always',
overrides: [
{
files: ['*.js', '*.mjs'],
files: ['*.js', '*.ts'],
options: {
printWidth: 80,
},

15
Dockerfile

@ -8,12 +8,15 @@ RUN yarn && \
FROM node:boron-alpine
LABEL maintainer="butlerx@notthe.cloud"
WORKDIR /app
WORKDIR /usr/src/app
ENV NODE_ENV=production
RUN apk add -U openssh && \
adduser -D -h /home/term -s /bin/sh term && \
echo "term:term" | chpasswd
RUN apk add -U openssh-client sshpass
EXPOSE 3000
COPY --from=builder /usr/src/app /app
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
COPY --from=builder /usr/src/app/node_modules /usr/src/app/node_modules
COPY package.json /usr/src/app
COPY index.js /usr/src/app
RUN mkdir ~/.ssh
RUN ssh-keyscan -H wetty-ssh >> ~/.ssh/known_hosts
ENTRYPOINT [ "/usr/local/bin/yarn", "start" ]
ENTRYPOINT [ "node", "." ]

3
Dockerfile-ssh

@ -0,0 +1,3 @@
FROM sickp/alpine-sshd:latest
RUN adduser -D -h /home/term -s /bin/sh term && \
( echo "term:term" | chpasswd )

104
README.md

@ -9,23 +9,29 @@ websockets rather then Ajax and hence better response time.
![WeTTy](/terminal.png?raw=true)
This fork was originally bug fixes and updates, but has since evolved in to a
full rewrite to use xterm.js to have better support and make it more
maintainable.
## Install
WeTTy can be installed from source or from npm. To install from source run:
WeTTy can be installed from source or from npm.
To install from source run:
```bash
$ git clone https://github.com/butlerx/wetty
$ git clone https://github.com/krishnasrinivas/wetty.git
$ cd wetty
$ yarn
$ yarn build
```
or install it globally with yarn, `yarn -g add wetty.js`, or npm,
`npm i -g wetty.js`
To install it globally from npm use yarn or npm:
- yarn, `yarn -g add wetty.js`
- npm, `npm i -g wetty.js`
For auto-login feature you'll need sshpass installed(NOT required for rest of
the program".
- `apt-get install sshpass` (debian eg. Ubuntu)
- `yum install sshpass` (red hat flavours eg. CentOs)
## Running WeTTy
@ -36,16 +42,27 @@ see how to use WeTTy from node see the [API Doc](./docs)
$ node index.js
```
Open your browser on `http://yourserver:3000/` and you will prompted to login.
Or go to `http://yourserver:3000/ssh/<username>` to specify the user before
hand.
Open your browser on `http://yourserver:3000/wetty` and you will prompted to
login. Or go to `http://yourserver:3000/wetty/ssh/<username>` to specify the
user before hand.
If you run it as root it will launch `/bin/login` (where you can specify the
user name), else it will launch `ssh` and connect by default to `localhost`.
If instead you wish to connect to a remote host you can specify the `--sshhost`
option, the SSH port using the `--sshport` option and the SSH user using the
`--sshuser` option.
### Flags
WeTTy can be run with the `--help` flag to get a full list of flags.
WeTTy runs on port `3000` by default. You can change the default port by tunning
with the `--port` or `-p` flag.
#### 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
@ -55,55 +72,75 @@ If instead you wish to connect to a remote host you can specify the host with
the `--sshhost` flag and pass the IP or DNS address of the host you want to
connect to.
#### Default User
You can specify the default user used to ssh to a host using the `--sshuser`.
This user can overwritten by going to `http://yourserver:3000/ssh/<username>`.
If this is left blank a user will be prompted to enter their username when they
connect.
#### SSH Port
By default WeTTy will try to ssh to port `22`, if your host uses an alternative
ssh port this can be specified with the flag `--sshport`.
#### WeTTy URL
If you'd prefer an HTTP base prefix other than `/wetty`, you can specify that
with `--base`. Do not set this to `/ssh/${something}`, as this will break
username matching code.
with `--base`.
### https
**Do not set this to `/ssh/${something}`, as this will break username matching
code.**
Always use https especially with a terminal to your server. You can add https by
#### HTTPS
Always use HTTPS especially with a terminal to your server. You can add HTTPS by
either using WeTTy behind a proxy or directly.
If you don't have SSL certificates from a CA you can create a self signed
certificate using this command:
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
$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
node index.js --sslkey key.pem --sslcert cert.pem
```
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:
If you don't have SSL certificates from a CA you can create a self signed
certificate using this command:
```bash
node index.js --sslkey key.pem --sslcert cert.pem -p 3000
```
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30000 -nodes
```
### Auto Login:
You can also pass the ssh password as an optional query parameter to auto-login
the user like this (Only while running wetty as a non root account):
`http://yourserver:3000/wetty/ssh/<username>?sshpass=<password>`
This is not a required feature and the security implications for passing the
password in the url will have to be considered by the user
### Behind a Proxy
## 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.
If your proxy uses a base path other than `/wetty`, specify the path with the
`--base` flag, or the `BASE` environment variable.
#### Nginx
For a more detailed look see the [nginx.conf](./bin/nginx.template) used for
testing
Put the following configuration in nginx's conf:
```nginx
location ^~ /wetty {
proxy_pass http://127.0.0.1:3000;
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";
@ -116,9 +153,6 @@ location ^~ /wetty {
}
```
For a more detailed look see the [nginx.conf](./bin/nginx.template) used for
testing
#### Apache
Put the following configuration in apache's conf:
@ -155,9 +189,9 @@ The default username is `term` and the password is `term`, if you did not modify
In the docker version all flags can be accessed as environment variables such as
`SSHHOST` or `SSHPORT`.
## Run WeTTy as a service daemon
If you dont want to build the image yourself just remove the line `build; .`
Install WeTTy globally with global option:
## Run WeTTy as a service daemon
### init.d

20
docker-compose.yml

@ -1,20 +1,20 @@
version: "3"
---
version: "3.5"
services:
wetty:
build: .
image: butlerx/wetty
container_name: wetty
tty: true
restart: always
working_dir: /app
ports:
- "3000:3000"
environment:
SSHHOST: 'localhost'
SSHHOST: 'wetty-ssh'
SSHPORT: 22
NODE_ENV: 'development'
command: yarn start --sshhost redbrick.dcu.ie
volumes:
- ./lib:/app/lib
web:
image: nginx
volumes:
@ -27,3 +27,13 @@ services:
- WETTY_HOST=wetty
- WETTY_PORT=3000
command: /bin/bash -c "envsubst '$${NGINX_DOMAIN},$${NGINX_PORT},$${WETTY_HOST},$${WETTY_PORT}' < /etc/nginx/conf.d/wetty.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
wetty-ssh:
build:
context: .
dockerfile: Dockerfile-ssh
container_name: 'wetty-ssh'
networks:
default:
name: wetty

95
index.js

@ -1,12 +1,93 @@
/* eslint-disable */
require = require('@std/esm')(module, {
cjs: 'true',
esm: 'js',
});
const wetty = require('./lib/index.mjs').default;
/* eslint-disable typescript/no-var-requires */
const yargs = require('yargs');
const wetty = require('./dist').default;
module.exports = wetty.wetty;
/**
* Check if being run by cli or require
*/
if (require.main === module) wetty.init();
if (require.main === module) {
wetty.init(
yargs
.options({
sslkey: {
demand: false,
type: 'string',
description: 'path to SSL key',
},
sslcert: {
demand: false,
type: 'string',
description: 'path to SSL certificate',
},
sshhost: {
demand: false,
description: 'ssh server host',
type: 'string',
default: process.env.SSHHOST || 'localhost',
},
sshport: {
demand: false,
description: 'ssh server port',
type: 'number',
default: parseInt(process.env.SSHPORT, 10) || 22,
},
sshuser: {
demand: false,
description: 'ssh user',
type: 'string',
default: process.env.SSHUSER || '',
},
sshauth: {
demand: false,
description:
'defaults to "password", you can use "publickey,password" instead',
type: 'string',
default: process.env.SSHAUTH || 'password',
},
sshpass: {
demand: false,
description: 'ssh password',
type: 'string',
default: process.env.SSHPASS || undefined,
},
sshkey: {
demand: false,
description:
'path to an optional client private key (connection will be password-less and insecure!)',
type: 'string',
default: process.env.SSHKEY || undefined,
},
base: {
demand: false,
alias: 'b',
description: 'base path to wetty',
type: 'string',
default: process.env.BASE || '/wetty/',
},
port: {
demand: false,
alias: 'p',
description: 'wetty listen port',
type: 'number',
default: parseInt(process.env.PORT, 10) || 3000,
},
command: {
demand: false,
alias: 'c',
description: 'command to run in shell',
type: 'string',
default: process.env.COMMAND || 'login',
},
help: {
demand: false,
alias: 'h',
type: 'boolean',
description: 'Print help message',
},
})
.boolean('allow_discovery').argv
);
}

30
lib/command.mjs

@ -1,30 +0,0 @@
const localhost = host =>
process.getuid() === 0 &&
(host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1');
export default (
{ request: { headers }, client: { conn } },
{ user, host, port, auth }
) => ({
args: localhost(host)
? ['login', '-h', conn.remoteAddress.split(':')[3]]
: [
'ssh',
address(headers, user, host),
'-p',
port,
'-o',
`PreferredAuthentications=${auth}`,
],
user:
localhost(host) ||
user !== '' ||
user.includes('@') ||
address(headers, user, host).includes('@'),
});
function address(headers, user, host) {
const match = headers.referer.match('.+/ssh/([^/]+)$');
const fallback = user ? `${user}@${host}` : host;
return match ? `${match[1]}@${host}` : fallback;
}

149
lib/emitter.mjs

@ -1,149 +0,0 @@
/**
* Create WeTTY server
* @module WeTTy
*/
import EventEmitter from 'events';
import server from './server.mjs';
import command from './command.mjs';
import term from './term.mjs';
import loadSSL from './ssl.mjs';
class WeTTy extends EventEmitter {
/**
* Starts WeTTy Server
* @name start
* @async
* @param {Object} [ssh] SSH settings
* @param {string} [ssh.user=''] default user for ssh
* @param {string} [ssh.host=localhost] machine to ssh too
* @param {string} [ssh.auth=password] authtype to use
* @param {number} [ssh.port=22] port to connect to over ssh
* @param {number} [basePath=/wetty/] base part of URL
* @param {number} [serverPort=3000] Port to run server on
* @param {Object} [ssl] SSL settings
* @param {?string} [ssl.key] Path to ssl key
* @param {?string} [ssl.cert] Path to ssl cert
* @return {Promise} Promise resolves once server is running
*/
start(
{ user = '', host = 'localhost', auth = 'password', port = 22 },
basePath = '/wetty/',
serverPort = 3000,
{ key, cert }
) {
return loadSSL(key, cert).then(ssl => {
const io = server(basePath, serverPort, ssl);
/**
* Wetty server connected too
* @fires WeTTy#connnection
*/
io.on('connection', socket => {
/**
* @event wetty#connection
* @name connection
* @type {object}
* @property {string} msg Message for logs
* @property {Date} date date and time of connection
*/
this.emit('connection', {
msg: `Connection accepted.`,
date: new Date(),
});
const { args, user: sshUser } = command(socket, {
user,
host,
auth,
port,
});
this.emit('debug', `sshUser: ${sshUser}, cmd: ${args.join(' ')}`);
if (sshUser) {
term.spawn(socket, args);
} else {
term
.login(socket)
.then(username => {
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
*/
spawned(pid, address) {
/**
* Terminal process spawned
* @event WeTTy#spawn
* @name spawn
* @type {object}
* @property {string} msg Message containing pid info and status
* @property {number} pid Pid of the terminal
* @property {string} address address of connecting user
*/
this.emit('spawn', {
msg: `PID=${pid} STARTED on behalf of ${address}`,
pid,
address,
});
}
/**
* terminal exited
*
* @fires WeTTy#exit
*/
exited(code, pid) {
/**
* Terminal process exits
* @event WeTTy#exit
* @name exit
* @type {object}
* @property {number} code the exit code
* @property {string} msg Message containing pid info and status
*/
this.emit('exit', { code, msg: `PID=${pid} ENDED` });
}
/**
* Disconnect from WeTTY
*
* @fires WeTTy#disconnet
*/
disconnected() {
/**
* @event WeTTY#disconnect
* @name disconnect
*/
this.emit('disconnect');
}
/**
* Wetty server started
* @fires WeTTy#server
*/
server(port, connection) {
/**
* @event WeTTy#server
* @type {object}
* @name server
* @property {string} msg Message for logging
* @property {number} port port sever is on
* @property {string} connection connection type for web traffic
*/
this.emit('server', {
msg: `${connection} on port ${port}`,
port,
connection,
});
}
}
export default new WeTTy();

100
lib/index.mjs

@ -1,100 +0,0 @@
import optimist from 'optimist';
import logger from './logger.mjs';
import wetty from './emitter.mjs';
const opts = optimist
.options({
sslkey: {
demand: false,
description: 'path to SSL key',
},
sslcert: {
demand: false,
description: 'path to SSL certificate',
},
sshhost: {
demand: false,
description: 'ssh server host',
},
sshport: {
demand: false,
description: 'ssh server port',
},
sshuser: {
demand: false,
description: 'ssh user',
},
sshauth: {
demand: false,
description:
'defaults to "password", you can use "publickey,password" instead',
},
base: {
demand: false,
alias: 'b',
description: 'base path to wetty',
},
port: {
demand: false,
alias: 'p',
description: 'wetty listen port',
},
help: {
demand: false,
alias: 'h',
description: 'Print help message',
},
})
.boolean('allow_discovery').argv;
export default class {
static start({
sshuser = process.env.SSHUSER || '',
sshhost = process.env.SSHHOST || 'localhost',
sshauth = process.env.SSHAUTH || 'password',
sshport = process.env.SSHPOST || 22,
base = process.env.BASE || '/wetty/',
port = process.env.PORT || 3000,
sslkey,
sslcert,
}) {
wetty
.on('exit', ({ code, msg }) => {
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 => logger.debug(msg));
return wetty.start(
{
user: sshuser,
host: sshhost,
auth: sshauth,
port: sshport,
},
base,
port,
{ key: sslkey, cert: sslcert }
);
}
static get wetty() {
return wetty;
}
static init() {
if (!opts.help) {
this.start(opts).catch(err => {
logger.error(err);
process.exitCode = 1;
});
} else {
optimist.showHelp();
process.exitCode = 0;
}
}
}

46
lib/server.mjs

@ -1,46 +0,0 @@
import compression from 'compression';
import express from 'express';
import favicon from 'serve-favicon';
import helmet from 'helmet';
import http from 'http';
import https from 'https';
import path from 'path';
import socket from 'socket.io';
import { isUndefined } from 'lodash';
import morgan from 'morgan';
import logger from './logger.mjs';
import events from './emitter.mjs';
const pubDir = path.join(__dirname, '..', 'public');
const distDir = path.join(__dirname, '..', 'dist');
export default function createServer(base, port, { key, cert }) {
base = base.replace(/\/*$/, "");
events.emit('debug', `key: ${key}, cert: ${cert}, port: ${port}, base: ${base}`);
const app = express();
const html = (req, res) => res.sendFile(path.join(pubDir, 'index.html'));
const css = (req, res) => res.sendFile(path.join(distDir, 'main.css'));
const js = (req, res) => res.sendFile(path.join(distDir, 'main.js'));
app
.use(morgan('combined', { stream: logger.stream }))
.use(helmet())
.use(compression())
.use(favicon(path.join(pubDir, 'favicon.ico')))
.get(`${base}/`, html)
.get(`${base}/main.css`, css)
.get(`${base}/main.js`, js)
.get(`${base}/ssh/main.css`, css)
.get(`${base}/ssh/main.js`, js)
.get(`${base}/ssh/:user`, html)
return socket(
!isUndefined(key) && !isUndefined(cert)
? https.createServer({ key, cert }, app).listen(port, () => {
events.server(port, 'https');
})
: http.createServer(app).listen(port, () => {
events.server(port, 'http');
}),
{ path: `${base}/socket.io` }
);
}

11
lib/ssl.mjs

@ -1,11 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
import { isUndefined } from 'lodash';
export default (sslkey, sslcert) =>
isUndefined(sslkey) || isUndefined(sslcert)
? Promise.resolve({})
: Promise.all([
fs.readFile(path.resolve(sslkey)),
fs.readFile(path.resolve(sslcert)),
]).then(([key, cert]) => ({ key, cert }));

124
package.json

@ -2,27 +2,31 @@
"name": "wetty.js",
"version": "1.0.3",
"description": "WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/butlerx/wetty",
"homepage": "https://github.com/krishnasrinivas/wetty",
"repository": {
"type": "git",
"url": "git://github.com/butlerx/wetty.git"
"url": "git://github.com/krishnasrinivas/wetty.git"
},
"author": "Cian Butler <butlerx@notthe.cloud> (https://github.com/butlerx)",
"author": "Krishna Srinivas <krishna.srinivas@gmail.com> (https://github.com/krishnasrinivas)",
"license": "MIT",
"bugs": {
"url": "https://github.com/butlerx/wetty/issues"
"url": "https://github.com/krishnasrinivas/wetty/issues"
},
"main": "index.js",
"scripts": {
"lint": "eslint --ext .js,.mjs .",
"build": "webpack",
"lint": "eslint --ext .js,.ts .",
"build": "babel-node node_modules/.bin/webpack",
"start": "node .",
"dev": "NODE_ENV=development concurrently --kill-others --success first \"webpack --watch\" \"nodemon .\"",
"prepublishOnly": "yarn build",
"precommit": "lint-staged"
"dev": "NODE_ENV=development concurrently --kill-others --success first \"babel-node node_modules/.bin/webpack --watch\" \"nodemon .\"",
"prepublishOnly": "yarn build"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,mjs}": [
"*.{js,ts}": [
"eslint --fix",
"git add"
],
@ -39,66 +43,88 @@
},
"nodemonConfig": {
"ignore": [
"dist/*",
"src/*",
"*.json"
]
},
"preferGlobal": "true",
"dependencies": {
"@std/esm": "^0.12.1",
"compression": "^1.7.1",
"express": "^4.15.3",
"express": "^4.16.4",
"fs-extra": "^4.0.1",
"helmet": "^3.9.0",
"jsdoc-to-markdown": "^4.0.1",
"lodash": "^4.17.4",
"morgan": "^1.9.0",
"morgan": "^1.9.1",
"node-pty": "^0.7.4",
"optimist": "^0.6",
"serve-favicon": "^2.4.3",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
"winston": "^3.0.0-rc1",
"xterm": "^3.0.1"
"serve-favicon": "^2.5.0",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
"winston": "^3.1.0",
"xterm": "^3.10.0",
"yargs": "^12.0.5"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-lodash": "^3.3.2",
"babel-preset-env": "^1.6.1",
"@babel/core": "^7.2.2",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"@babel/preset-typescript": "^7.1.0",
"@babel/register": "^7.0.0",
"@types/compression": "^0.0.36",
"@types/express": "^4.16.0",
"@types/fs-extra": "^5.0.4",
"@types/helmet": "^0.0.42",
"@types/lodash": "^4.14.119",
"@types/morgan": "^1.7.35",
"@types/node": "^10.12.18",
"@types/serve-favicon": "^2.2.30",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/webpack-env": "^1.13.6",
"@types/yargs": "^12.0.5",
"babel-loader": "^8.0.5",
"babel-plugin-lodash": "^3.3.4",
"concurrently": "^3.5.1",
"css-loader": "^0.28.8",
"eslint": "^4.18.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-prettier": "^2.6.0",
"extract-text-webpack-plugin": "^3.0.2",
"husky": "^0.14.3",
"css-loader": "^2.1.0",
"eslint": "^5.12.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^3.3.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-typescript": "^1.0.0-rc.1",
"file-loader": "^3.0.1",
"husky": "^1.3.1",
"lint-staged": "^6.1.1",
"node-sass": "^4.7.2",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0",
"nodemon": "^1.14.10",
"prettier": "^1.10.2",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.1",
"uglifyjs-webpack-plugin": "^1.1.6",
"webpack": "^3.10.0"
"prettier": "^1.15.3",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"typescript": "~3.1.1",
"webpack": "^4.28.3",
"webpack-cli": "^3.2.0",
"webpack-node-externals": "^1.7.2"
},
"contributors": [
"Krishna Srinivas <krishna.srinivas@gmail.com>",
"Boyan Rabchev <boyan@rabchev.com>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Strubbl <github@linux4tw.de>",
"Jarrett Gilliam <jarrettgilliam@gmail.com>",
"Nathan LeClaire <nathan.leclaire@docker.com>",
"mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>",
"Andreas Kloeckner <inform@tiker.net>",
"Antonio Calatrava <antonio@antoniocalatrava.com>",
"Boyan Rabchev <TELERIK\\rabchev@rabchevlnx.telerik.com>",
"Boyan Rabchev <boyan@rabchev.com>",
"Cian Butler <butlerx@notthe.cloud>",
"Farhan Khan <khanzf@gmail.com>",
"Imuli <i@imu.li>",
"James Turnbull <james@lovedthanlost.net>",
"Jarrett Gilliam <jarrettgilliam@gmail.com>",
"Kasper Holbek Jensen <kholbekj@gmail.com>",
"Krishna Srinivas <krishna@minio.io>",
"Luca Milanesio <luca.milanesio@gmail.com>",
"Nathan LeClaire <nathan.leclaire@docker.com>",
"Neale Pickett <neale@woozle.org>",
"Robert <robert@n5qm.com>"
"Robert <robert@n5qm.com>",
"Strubbl <github@linux4tw.de>",
"koushikmln <mln02koushik@gmail.com>",
"mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>"
]
}

20
public/index.html

@ -1,20 +0,0 @@
<!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>WeTTy - The Web Terminal Emulator</title>
<link rel="stylesheet" href="main.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="terminal"></div>
<script src="main.js"></script>
</body>
</html>

0
public/favicon.ico → src/client/favicon.ico

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

31
src/index.js → src/client/index.ts

@ -1,17 +1,20 @@
import { Terminal } from 'xterm';
import { isUndefined } from 'lodash';
import io from 'socket.io-client';
import * as fit from './fit';
import * as io from 'socket.io-client';
import { fit } from 'xterm/lib/addons/fit/fit';
import './wetty.scss';
import './favicon.ico';
Terminal.applyAddon(fit);
var userRegex = new RegExp("ssh/\[^/]+$");
var socketPath = window.location.pathname.replace(userRegex, "");
var socket = io(window.location.origin, { path: socketPath + "socket.io" });
const userRegex = new RegExp('ssh/[^/]+$');
const trim = (str: string): string => str.replace(/\/*$/, '');
const socketBase = trim(window.location.pathname).replace(userRegex, '');
const socket = io(window.location.origin, {
path: `${trim(socketBase)}/socket.io`,
});
socket.on('connect', () => {
const term = new Terminal();
term.open(document.getElementById('terminal'), { focus: true });
term.open(document.getElementById('terminal'));
term.setOption('fontSize', 14);
document.getElementById('overlay').style.display = 'none';
window.addEventListener('beforeunload', handler, false);
@ -29,15 +32,15 @@ socket.on('connect', () => {
return true;
});
function resize() {
term.fit();
function resize(): void {
fit(term);
socket.emit('resize', { cols: term.cols, rows: term.rows });
}
window.onresize = resize;
resize();
term.focus();
function kill(data) {
function kill(data: string): void {
disconnect(data);
}
@ -48,7 +51,7 @@ socket.on('connect', () => {
socket.emit('resize', size);
});
socket
.on('data', data => {
.on('data', (data: string) => {
term.write(data);
})
.on('login', () => {
@ -57,18 +60,18 @@ socket.on('connect', () => {
})
.on('logout', kill)
.on('disconnect', kill)
.on('error', err => {
.on('error', (err: string | null) => {
if (err) disconnect(err);
});
});
function disconnect(reason) {
function disconnect(reason: string): void {
document.getElementById('overlay').style.display = 'block';
if (!isUndefined(reason)) document.getElementById('msg').innerHTML = reason;
window.removeEventListener('beforeunload', handler, false);
}
function handler(e) {
function handler(e: { returnValue: string }): string {
e.returnValue = 'Are you sure?';
return e.returnValue;
}

0
src/wetty.scss → src/client/wetty.scss

39
src/fit.js

@ -1,39 +0,0 @@
import { isUndefined } from 'lodash';
export function proposeGeometry({ element, renderer }) {
if (!element.parentElement) return null;
const parentElementStyle = window.getComputedStyle(element.parentElement);
return {
cols: Math.floor(
Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10)) /
renderer.dimensions.actualCellWidth,
),
rows: Math.floor(
parseInt(parentElementStyle.getPropertyValue('height'), 10) /
renderer.dimensions.actualCellHeight,
),
};
}
export function fit(term) {
const { rows, cols } = proposeGeometry(term);
if (!isUndefined(rows) && !isUndefined(cols)) {
// Force a full render
if (term.rows !== rows || term.cols !== cols) {
term.renderer.clear();
term.resize(cols, rows);
}
}
}
export function apply({ prototype }) {
prototype.proposeGeometry = function proProposeGeometry() {
return proposeGeometry(this);
};
prototype.fit = function proFit() {
return fit(this);
};
}

6
lib/buffer.mjs → src/server/buffer.ts

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

94
src/server/command.ts

@ -0,0 +1,94 @@
import * as url from 'url';
import { Socket } from 'socket.io';
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}`,
];
if (key) {
return sshRemoteOptsBase.concat(['-i', key, cmd]);
}
if (pass) {
return ['sshpass', '-p', pass].concat(sshRemoteOptsBase, [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;
}

3
src/server/emitter.ts

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

80
src/server/index.ts

@ -0,0 +1,80 @@
import * as yargs from 'yargs';
import logger from './logger';
import wetty from './emitter';
import WeTTy from './wetty';
export interface Options {
sshhost: string;
sshport: number;
sshuser: string;
sshauth: string;
sshkey?: string;
sshpass?: string;
sslkey?: string;
sslcert?: string;
base: string;
port: number;
command?: string;
}
interface CLI extends Options {
help: boolean;
}
export default class Server {
public static start({
sshuser,
sshhost,
sshauth,
sshport,
sshkey,
sshpass,
base,
port,
command,
sslkey,
sslcert,
}: 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,
port,
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;
}
}
}

18
src/server/interfaces.ts

@ -0,0 +1,18 @@
export interface SSH {
user: string;
host: string;
auth: string;
port: number;
pass?: string;
key?: string;
}
export interface SSL {
key?: string;
cert?: string;
}
export interface SSLBuffer {
key?: Buffer;
cert?: Buffer;
}

24
lib/logger.mjs → src/server/logger.ts

@ -3,14 +3,18 @@ import { createLogger, format, transports } from 'winston';
const { combine, timestamp, label, printf, colorize } = format;
const logger = createLogger({
format: combine(
colorize({ all: true }),
label({ label: 'Wetty' }),
timestamp(),
printf(
info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`
)
),
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',
@ -20,8 +24,8 @@ const logger = createLogger({
});
logger.stream = {
write(message) {
logger.verbose(message);
write(message: string): void {
logger.info(message);
},
};

84
src/server/server.ts

@ -0,0 +1,84 @@
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 } from './interfaces';
const distDir = path.join('./', 'dist', 'client');
const trim = (str: string): string => str.replace(/\/*$/, '');
export default function createServer(
base: string,
port: number,
{ key, cert }: SSLBuffer
): SocketIO.Server {
const basePath = trim(base);
events.emit(
'debug',
`key: ${key}, cert: ${cert}, port: ${port}, base: ${base}`
);
const html = (
req: express.Request,
res: express.Response
): express.Response =>
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>WeTTy - The Web Terminal Emulator</title>
<link rel="stylesheet" href="${basePath}/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="terminal"></div>
<script src="${basePath}/public/index.js"></script>
</body>
</html>`);
const app = express();
app
.use(morgan('combined', { stream: logger.stream }))
.use(helmet())
.use(compression())
.use(favicon(path.join(distDir, 'favicon.ico')))
.use(`${basePath}/public`, express.static(distDir))
.use((req, res, next) => {
if (
req.url.substr(-1) === '/' &&
req.url.length > 1 &&
!/\?[^]*\//.test(req.url)
)
res.redirect(301, req.url.slice(0, -1));
else next();
})
.get(basePath, html)
.get(`${basePath}/ssh/:user`, html);
return socket(
!isUndefined(key) && !isUndefined(cert)
? https.createServer({ key, cert }, app).listen(port, () => {
events.server(port, 'https');
})
: http.createServer(app).listen(port, () => {
events.server(port, 'http');
}),
{ path: `${basePath}/socket.io` }
);
}

11
src/server/ssl.ts

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

25
lib/term.mjs → src/server/term.ts

@ -1,6 +1,6 @@
import { spawn } from 'node-pty';
import { isUndefined } from 'lodash';
import events from './emitter.mjs';
import events from './emitter';
const xterm = {
name: 'xterm-256color',
@ -11,15 +11,15 @@ const xterm = {
};
export default class Term {
static spawn(socket, args) {
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
.emit('logout')
.removeAllListeners('disconnect')
.removeAllListeners('resize')
.removeAllListeners('input');
@ -35,18 +35,14 @@ export default class Term {
if (!isUndefined(term)) term.write(input);
})
.on('disconnect', () => {
term.end();
term.destroy();
events.exited();
const { pid } = term;
term.kill();
events.exited(0, pid);
});
}
static login(socket) {
const term = spawn(
'/usr/bin/env',
['node', '-r', '@std/esm', './lib/buffer.mjs'],
xterm
);
public static login(socket: SocketIO.Socket): Promise<string> {
const term = spawn('/usr/bin/env', ['node', './dist/buffer.js'], xterm);
let buf = '';
return new Promise((resolve, reject) => {
term.on('exit', () => {
@ -56,13 +52,12 @@ export default class Term {
socket.emit('data', data);
});
socket
.on('input', input => {
.on('input', (input: string) => {
term.write(input);
buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input;
})
.on('disconnect', () => {
term.end();
term.destroy();
term.kill();
reject();
});
});

131
src/server/wetty.ts

@ -0,0 +1,131 @@
/**
* 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 } from './interfaces';
export default class WeTTy extends EventEmitter {
/**
* Starts WeTTy Server
* @name start
*/
public start(
ssh: SSH = { user: '', host: 'localhost', auth: 'password', port: 22 },
basePath: string = '/wetty/',
serverPort: number = 3000,
command: string = '',
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(basePath, serverPort, 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,
});
}
}

22
tsconfig.json

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist",
"allowJs": true,
"esModuleInterop": false,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"*": [
"node_modules/",
"src/types/*"
]
}
},
"include": [
"./src/**/*.ts"
]
}

134
webpack.config.babel.js

@ -0,0 +1,134 @@
import path from 'path';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import nodeExternals from 'webpack-node-externals';
const template = override =>
Object.assign(
{
mode: process.env.NODE_ENV || 'development',
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.ts', '.json', '.js', '.node'],
},
stats: {
colors: true,
},
},
override
);
const entry = (folder, file) =>
path.join(__dirname, 'src', folder, `${file}.ts`);
const entries = (folder, files) =>
Object.assign(...files.map(file => ({ [file]: entry(folder, file) })));
export default [
template({
entry: entries('server', ['index', 'buffer']),
target: 'node',
devtool: 'source-map',
output: {
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
filename: '[name].js',
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
plugins: ['lodash'],
},
},
},
{
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
},
],
},
plugins: [new webpack.IgnorePlugin(/uws/)],
}),
template({
entry: entries('client', ['index']),
output: {
path: path.resolve(__dirname, 'dist', 'client'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets: {
browsers: ['last 2 versions', 'safari >= 7'],
},
},
],
],
plugins: ['lodash'],
},
},
},
{
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
},
],
},
{
test: /\.(jpg|jpeg|png|gif|mp3|svg|ico)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]',
},
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
devtool: 'source-map',
}),
];

74
webpack.config.js

@ -1,74 +0,0 @@
const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractSass = new ExtractTextPlugin({
filename: '[name].css',
disable: process.env.NODE_ENV === 'development',
});
const loader = new webpack.ProvidePlugin({
fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch',
});
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
plugins: ['lodash'],
presets: [
[
'env',
{
targets: {
browsers: ['last 2 versions', 'safari >= 7'],
},
},
],
],
},
},
{
test: /\.scss$/,
use: extractSass.extract({
use: [
{
loader: 'css-loader',
options: { minimize: true },
},
{
loader: 'sass-loader',
},
],
fallback: 'style-loader',
}),
},
],
},
plugins:
process.env.NODE_ENV !== 'development'
? [
loader,
extractSass,
new UglifyJSPlugin({
parallel: true,
uglifyOptions: {
ecma: 8,
},
}),
]
: [loader, extractSass],
stats: {
colors: true,
},
};

5936
yarn.lock

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