Browse Source

Merge branch 'master' into master

pull/163/head
Cian Butler 6 years ago
committed by GitHub
parent
commit
fc632d638d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      README.md
  2. 19
      bin/nginx.template
  3. 2
      docker-compose.yml
  4. 2
      docs/README.md
  5. 12
      index.js
  6. 2
      package.json
  7. 40
      src/client/index.ts
  8. 50
      src/client/wetty.scss
  9. 7
      src/server/index.ts
  10. 1
      src/server/interfaces.ts
  11. 34
      src/server/server.ts
  12. 11
      src/server/term.ts

4
README.md

@ -24,11 +24,11 @@ $ yarn build
To install it globally from npm use yarn or npm: To install it globally from npm use yarn or npm:
- yarn, `yarn -g add wetty.js` - yarn, `yarn global add wetty.js`
- npm, `npm i -g wetty.js` - npm, `npm i -g wetty.js`
For auto-login feature you'll need sshpass installed (NOT required for rest of For auto-login feature you'll need sshpass installed (NOT required for rest of
the program". the program).
- `apt-get install sshpass` (debian eg. Ubuntu) - `apt-get install sshpass` (debian eg. Ubuntu)
- `yum install sshpass` (red hat flavours eg. CentOs) - `yum install sshpass` (red hat flavours eg. CentOs)

19
bin/nginx.template

@ -46,6 +46,25 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true; proxy_set_header X-NginX-Proxy true;
# Authenticate user via other services (e.g., oauth2 end-points)
#
# Configuration :
# - Configure a 'auth_request' directive for this server block
# - Capture the authenticated username using 'auth_request_set'
# - Set the 'remote-user' request header accordingly
#
# Example (using lasso as authentication middleware):
#
# Add to server block:
# auth_request /lasso-validate
# auth_request_set $auth_user $upstream_http_x_lasso_user;
#
# Add to /wetty location block
# proxy_set_header remote-user $auth_user;
#
# And configure a '/lasso-validate' location. See this blog for further
# guidance: https://developer.okta.com/blog/2018/08/28/nginx-auth-request
} }
# gzip # gzip

2
docker-compose.yml

@ -6,7 +6,7 @@ services:
image: butlerx/wetty image: butlerx/wetty
container_name: wetty container_name: wetty
tty: true tty: true
working_dir: /app working_dir: /usr/src/app
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:

2
docs/README.md

@ -21,4 +21,4 @@ wetty.start(/* server settings, see Options */).then(() => {
## API ## API
For WeTTy options and event details please refer to the [api docs](./api.md) For WeTTy options and event details please refer to the [api docs](./API.md)

12
index.js

@ -41,6 +41,12 @@ if (require.main === module) {
type: 'string', type: 'string',
default: process.env.SSHUSER || '', default: process.env.SSHUSER || '',
}, },
title: {
demand: false,
description: 'window title',
type: 'string',
default: process.env.TITLE || 'WeTTy - The Web Terminal Emulator',
},
sshauth: { sshauth: {
demand: false, demand: false,
description: description:
@ -88,6 +94,12 @@ if (require.main === module) {
type: 'string', type: 'string',
default: process.env.COMMAND || 'login', default: process.env.COMMAND || 'login',
}, },
bypasshelmet: {
demand: false,
description: 'disable helmet from placing security restrictions',
type: 'boolean',
default: false,
},
help: { help: {
demand: false, demand: false,
alias: 'h', alias: 'h',

2
package.json

@ -94,7 +94,7 @@
"eslint-plugin-typescript": "^1.0.0-rc.1", "eslint-plugin-typescript": "^1.0.0-rc.1",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^6.1.1", "lint-staged": "~8.2.0",
"mini-css-extract-plugin": "^0.5.0", "mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"nodemon": "^1.14.10", "nodemon": "^1.14.10",

40
src/client/index.ts

@ -15,8 +15,46 @@ const socket = io(window.location.origin, {
socket.on('connect', () => { socket.on('connect', () => {
const term = new Terminal(); const term = new Terminal();
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
term.setOption('fontSize', 14); const defaultOptions = { fontSize: 14 };
let options: any;
try {
if (localStorage.options === undefined) {
options = defaultOptions;
} else {
options = JSON.parse(localStorage.options);
}
} catch {
options = defaultOptions;
}
Object.keys(options).forEach(key => {
const value = options[key];
term.setOption(key, value);
});
const code = JSON.stringify(options, null, 2);
const editor = document.querySelector('#options .editor');
editor.value = code;
editor.addEventListener('keyup', e => {
try {
const updated = JSON.parse(editor.value);
const updatedCode = JSON.stringify(updated, null, 2);
editor.value = updatedCode;
editor.classList.remove('error');
localStorage.options = updatedCode;
Object.keys(updated).forEach(key => {
const value = updated[key];
term.setOption(key, value);
});
resize();
} catch {
// skip
editor.classList.add('error');
}
});
document.getElementById('overlay').style.display = 'none'; document.getElementById('overlay').style.display = 'none';
document.querySelector('#options .toggler').addEventListener('click', e => {
document.getElementById('options').classList.toggle('opened');
e.preventDefault();
});
window.addEventListener('beforeunload', handler, false); window.addEventListener('beforeunload', handler, false);
/* /*
term.scrollPort_.screen_.setAttribute('contenteditable', 'false'); term.scrollPort_.screen_.setAttribute('contenteditable', 'false');

50
src/client/wetty.scss

@ -3,6 +3,7 @@
$black: #000; $black: #000;
$grey: rgba(0, 0, 0, 0.75); $grey: rgba(0, 0, 0, 0.75);
$white: #fff; $white: #fff;
$lgrey: #ccc;
html, html,
body { body {
@ -44,4 +45,53 @@ body {
position: relative; position: relative;
width: 100%; width: 100%;
} }
#options {
position: absolute;
top: 1em;
right: 1em;
z-index: 20;
height: 16px;
width: 16px;
a.toggler {
display: inline-block;
position: absolute;
right: 1em;
top: 0em;
font-size: 16px;
color: $lgrey;
z-index: 20;
:hover {
color: $white;
}
}
.editor {
background-color: rgba(0, 0, 0, 0.85);
padding: 0.5em;
border-radius: 0.3em;
border-color: rgba(255, 255, 255, 0.25);
display: none;
position: relative;
height: 100%;
width: 100%;
top: 1em;
right: 2em;
color: #eee;
font-size: 24px;
}
.editor.error {
color: red;
}
}
#options.opened {
height: 50%;
width: 50%;
.editor {
display: flex;
}
}
} }

7
src/server/index.ts

@ -15,7 +15,9 @@ export interface Options {
base: string; base: string;
host: string; host: string;
port: number; port: number;
title: string;
command?: string; command?: string;
bypasshelmet?: boolean;
} }
interface CLI extends Options { interface CLI extends Options {
@ -33,9 +35,11 @@ export default class Server {
base, base,
host, host,
port, port,
title,
command, command,
sslkey, sslkey,
sslcert, sslcert,
bypasshelmet,
}: Options): Promise<void> { }: Options): Promise<void> {
wetty wetty
.on('exit', ({ code, msg }: { code: number; msg: string }) => { .on('exit', ({ code, msg }: { code: number; msg: string }) => {
@ -54,10 +58,11 @@ export default class Server {
host: sshhost, host: sshhost,
auth: sshauth, auth: sshauth,
port: sshport, port: sshport,
title,
pass: sshpass, pass: sshpass,
key: sshkey, key: sshkey,
}, },
{ base, host, port }, { base, host, port, title, bypasshelmet },
command, command,
{ key: sslkey, cert: sslcert } { key: sslkey, cert: sslcert }
); );

1
src/server/interfaces.ts

@ -21,4 +21,5 @@ export interface Server {
port: number; port: number;
host: string; host: string;
base: string; base: string;
bypasshelmet: boolean;
} }

34
src/server/server.ts

@ -17,27 +17,29 @@ const distDir = path.join(__dirname, 'client');
const trim = (str: string): string => str.replace(/\/*$/, ''); const trim = (str: string): string => str.replace(/\/*$/, '');
export default function createServer( export default function createServer(
{ base, port, host }: Server, { base, port, host, title, bypasshelmet }: Server,
{ key, cert }: SSLBuffer { key, cert }: SSLBuffer
): SocketIO.Server { ): SocketIO.Server {
const basePath = trim(base); const basePath = trim(base);
events.emit( events.emit(
'debug', 'debug',
`key: ${key}, cert: ${cert}, port: ${port}, base: ${base}` `key: ${key}, cert: ${cert}, port: ${port}, base: ${base}, title: ${title}`
); );
const html = ( const html = (
req: express.Request, req: express.Request,
res: express.Response res: express.Response
): express.Response => ): express.Response => {
const resourcePath = /^\/ssh\//.test(req.url) ? '../' : '';
res.send(`<!doctype html> res.send(`<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>WeTTy - The Web Terminal Emulator</title> <title>${title}</title>
<link rel="stylesheet" href="${basePath}/public/index.css" /> <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> </head>
<body> <body>
<div id="overlay"> <div id="overlay">
@ -46,29 +48,29 @@ export default function createServer(
<input type="button" onclick="location.reload();" value="reconnect" /> <input type="button" onclick="location.reload();" value="reconnect" />
</div> </div>
</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> <div id="terminal"></div>
<script src="${basePath}/public/index.js"></script> <script src="${resourcePath}public/index.js"></script>
</body> </body>
</html>`); </html>`);
}
const app = express(); const app = express();
app app
.use(morgan('combined', { stream: logger.stream })) .use(morgan('combined', { stream: logger.stream }))
.use(helmet({ frameguard: false })) .use(helmet({ frameguard: !bypasshelmet }))
.use(compression()) .use(compression())
.use(favicon(path.join(distDir, 'favicon.ico'))) .use(favicon(path.join(distDir, 'favicon.ico')))
.use(`${basePath}/public`, express.static(distDir)) .use(`${basePath}/public`, express.static(distDir))
.use((req, res, next) => { .use((req, res, next) => {
if ( if (req.url === basePath) res.redirect(301, req.url + '/');
req.url.substr(-1) === '/' &&
req.url.length > 1 &&
!/\?[^]*\//.test(req.url)
)
res.redirect(301, req.url.slice(0, -1));
else next(); else next();
}) }).get(basePath, html).get(`${basePath}/ssh/:user`, html);
.get(basePath, html)
.get(`${basePath}/ssh/:user`, html);
return socket( return socket(
!isUndefined(key) && !isUndefined(cert) !isUndefined(key) && !isUndefined(cert)

11
src/server/term.ts

@ -42,6 +42,17 @@ export default class Term {
} }
public static login(socket: SocketIO.Socket): Promise<string> { public static login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username
let remoteUser = socket.request.headers['remote-user'];
if (remoteUser) {
return new Promise((resolve,reject) => {
resolve(remoteUser);
});
}
// Request carries no username information
// Create terminal and ask user for username
const term = spawn( const term = spawn(
'/usr/bin/env', '/usr/bin/env',
['node', `${__dirname}/buffer.js`], ['node', `${__dirname}/buffer.js`],

Loading…
Cancel
Save