Browse Source

Add support for loading from config file

pull/270/head
butlerx 4 years ago
parent
commit
3375c5c4d0
No known key found for this signature in database GPG Key ID: B37CA765BAA89170
  1. 6
      build.sh
  2. 27
      conf/config.json5
  3. 23
      src/client/wetty.ts
  4. 2
      src/client/wetty/disconnect.ts
  5. 5
      src/client/wetty/download.ts
  6. 2
      src/client/wetty/mobile.ts
  7. 2
      src/client/wetty/shared/type.ts
  8. 2
      src/client/wetty/socket.ts
  9. 10
      src/client/wetty/term.ts
  10. 2
      src/client/wetty/term/confiruragtion.ts
  11. 2
      src/client/wetty/term/confiruragtion/load.ts
  12. 72
      src/main.ts
  13. 14
      src/server.ts
  14. 8
      src/server/command.ts
  15. 2
      src/server/command/ssh.ts
  16. 2
      src/server/login.ts
  17. 14
      src/server/socketServer.ts
  18. 2
      src/server/socketServer/html.ts
  19. 4
      src/server/socketServer/socket.ts
  20. 2
      src/server/socketServer/ssl.ts
  21. 4
      src/server/spawn.ts
  22. 126
      src/shared/config.ts
  23. 2
      src/shared/defaults.ts
  24. 8
      src/shared/interfaces.ts
  25. 2
      src/shared/logger.ts
  26. 30
      tsconfig.json

6
build.sh

@ -21,7 +21,7 @@ show_usage() {
clean() { clean() {
: 'Clean repo and delete all built files' : 'Clean repo and delete all built files'
rm -rf build rm -rf build yarn-error.log
} }
build-css() { build-css() {
@ -39,6 +39,7 @@ build-js() {
build-assets() { build-assets() {
: 'Move assets not handled by sass and typescript to build dir' : 'Move assets not handled by sass and typescript to build dir'
mkdir -p build/assets
cp src/assets/*.ico build/assets cp src/assets/*.ico build/assets
} }
@ -50,7 +51,8 @@ watch() {
--kill-others \ --kill-others \
--success first \ --success first \
"build-js --watch" \ "build-js --watch" \
"build-css --watch" "nodemon ." "build-css --watch" \
"nodemon ."
} }
build() { build() {

27
conf/config.json5

@ -0,0 +1,27 @@
{
ssh: {
// user: 'username', // default user to use when ssh-ing
host: 'localhost', // Server to ssh to
auth: 'password', // shh authentication, method. Defaults to "password", you can use "publickey,password" instead'
// pass: "password", // Password to use when sshing
// key: "", // path to an optional client private key, connection will be password-less and insecure!
port: 22, // Port to ssh to
knownHosts: '/dev/null', // ssh knownHosts file to use
},
server: {
base: '/wetty/', // URL base to serve resources from
port: 3000, // Port to listen on
host: '0.0.0.0', // address to listen on
title: 'WeTTy - The Web Terminal Emulator', // Page title
bypassHelmet: false, // Disable Helmet security checks
},
forceSSH: false, // Force sshing to local machine over login if running as root
command: 'login', // Command to run on server. Login will use ssh if connecting to different server
/*
ssl:{
key: 'ssl.key',
cert: 'ssl.cert',
}
*/
}

23
src/client/wetty.ts

@ -1,14 +1,17 @@
import _ from 'lodash'; import _ from '/../web_modules/lodash.js';
import { dom, library } from '@fortawesome/fontawesome-svg-core'; import {
import { faCogs } from '@fortawesome/free-solid-svg-icons'; dom,
library,
} from '/../web_modules/@fortawesome/fontawesome-svg-core.js';
import { faCogs } from '/../web_modules/@fortawesome/free-solid-svg-icons.js';
import { FileDownloader } from './wetty/download'; import { FileDownloader } from './wetty/download.js';
import { disconnect } from './wetty/disconnect'; import { disconnect } from './wetty/disconnect.js';
import { mobileKeyboard } from './wetty/mobile'; import { mobileKeyboard } from './wetty/mobile.js';
import { overlay } from './shared/elements'; import { overlay } from './shared/elements.js';
import { socket } from './wetty/socket'; import { socket } from './wetty/socket.js';
import { verifyPrompt } from './shared/verify'; import { verifyPrompt } from './shared/verify.js';
import { terminal } from './wetty/term'; import { terminal } from './wetty/term.js';
// Setup for fontawesome // Setup for fontawesome
library.add(faCogs); library.add(faCogs);

2
src/client/wetty/disconnect.ts

@ -1,4 +1,4 @@
import _ from 'lodash'; import _ from '/../web_modules/lodash.js';
import { verifyPrompt } from '../shared/verify'; import { verifyPrompt } from '../shared/verify';
import { overlay } from '../shared/elements'; import { overlay } from '../shared/elements';

5
src/client/wetty/download.ts

@ -1,5 +1,6 @@
import Toastify from 'toastify-js'; // @ts-ignore
import fileType from 'file-type'; import Toastify from '/../web_modules/toastify-js.js';
import fileType from '/../web_modules/file-type.js';
const DEFAULT_FILE_BEGIN = '\u001b[5i'; const DEFAULT_FILE_BEGIN = '\u001b[5i';
const DEFAULT_FILE_END = '\u001b[4i'; const DEFAULT_FILE_END = '\u001b[4i';

2
src/client/wetty/mobile.ts

@ -1,4 +1,4 @@
import _ from 'lodash'; import _ from '/../web_modules/lodash.js';
export function mobileKeyboard(): void { export function mobileKeyboard(): void {
const [screen] = document.getElementsByClassName('xterm-screen'); const [screen] = document.getElementsByClassName('xterm-screen');

2
src/client/wetty/shared/type.ts

@ -1,4 +1,4 @@
import { Terminal } from 'xterm'; import { Terminal } from '/../../web_modules/xterm.js';
export class Term extends Terminal { export class Term extends Terminal {
resizeTerm(): void {} resizeTerm(): void {}

2
src/client/wetty/socket.ts

@ -1,4 +1,4 @@
import io from 'socket.io-client'; import io from '/../../web_modules/socket.io-client.js';
const userRegex = new RegExp('ssh/[^/]+$'); const userRegex = new RegExp('ssh/[^/]+$');
export const trim = (str: string): string => str.replace(/\/*$/, ''); export const trim = (str: string): string => str.replace(/\/*$/, '');

10
src/client/wetty/term.ts

@ -1,11 +1,11 @@
import _ from 'lodash'; import _ from '/../web_modules/lodash.js';
import type { Socket } from 'socket.io-client'; import type { Socket } from 'socket.io-client';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from '/../web_modules/xterm-addon-fit.js';
import { Terminal } from 'xterm'; import { Terminal } from '/../web_modules/xterm.js';
import type { Term } from './shared/type'; import type { Term } from './shared/type';
import { configureTerm } from './term/confiruragtion'; import { configureTerm } from './term/confiruragtion.js';
import { terminal as termElement } from '../shared/elements'; import { terminal as termElement } from '../shared/elements.js';
export function terminal(socket: typeof Socket): Term | undefined { export function terminal(socket: typeof Socket): Term | undefined {
const term = new Terminal() as Term; const term = new Terminal() as Term;

2
src/client/wetty/term/confiruragtion.ts

@ -1,4 +1,4 @@
import _ from 'lodash'; import _ from '/../../web_modules/lodash.js';
import type { Term } from '../shared/type'; import type { Term } from '../shared/type';
import { copySelected, copyShortcut } from './confiruragtion/clipboard'; import { copySelected, copyShortcut } from './confiruragtion/clipboard';

2
src/client/wetty/term/confiruragtion/load.ts

@ -1,4 +1,4 @@
import _ from 'lodash'; import _ from '/../../../web_modules/lodash.js';
export function loadOptions(): object { export function loadOptions(): object {
const defaultOptions = { fontSize: 14 }; const defaultOptions = { fontSize: 14 };

72
src/main.ts

@ -3,17 +3,15 @@
* @module WeTTy * @module WeTTy
*/ */
import yargs from 'yargs'; import yargs from 'yargs';
import isUndefined from 'lodash/isUndefined'; import { logger } from './shared/logger.js';
import { logger } from './shared/logger'; import { startServer } from './server.js';
import { import { loadConfigFile, mergeCliConf } from './shared/config.js';
sshDefault,
serverDefault,
forceSSHDefault,
defaultCommand,
} from './shared/defaults';
import { startServer } from './server';
const opts = yargs const opts = yargs
.options('conf', {
type: 'string',
description: 'config file to load config from',
})
.option('ssl-key', { .option('ssl-key', {
type: 'string', type: 'string',
description: 'path to SSL key', description: 'path to SSL key',
@ -25,78 +23,64 @@ const opts = yargs
.option('ssh-host', { .option('ssh-host', {
description: 'ssh server host', description: 'ssh server host',
type: 'string', type: 'string',
default: sshDefault.host,
}) })
.option('ssh-port', { .option('ssh-port', {
description: 'ssh server port', description: 'ssh server port',
type: 'number', type: 'number',
default: sshDefault.port,
}) })
.option('ssh-user', { .option('ssh-user', {
description: 'ssh user', description: 'ssh user',
type: 'string', type: 'string',
default: sshDefault.user,
}) })
.option('title', { .option('title', {
description: 'window title', description: 'window title',
type: 'string', type: 'string',
default: serverDefault.title,
}) })
.option('ssh-auth', { .option('ssh-auth', {
description: description:
'defaults to "password", you can use "publickey,password" instead', 'defaults to "password", you can use "publickey,password" instead',
type: 'string', type: 'string',
default: sshDefault.auth,
}) })
.option('ssh-pass', { .option('ssh-pass', {
description: 'ssh password', description: 'ssh password',
type: 'string', type: 'string',
default: sshDefault.pass,
}) })
.option('ssh-key', { .option('ssh-key', {
demand: false, demand: false,
description: description:
'path to an optional client private key (connection will be password-less and insecure!)', 'path to an optional client private key (connection will be password-less and insecure!)',
type: 'string', type: 'string',
default: sshDefault.key,
}) })
.option('force-ssh', { .option('force-ssh', {
description: 'Connecting through ssh even if running as root', description: 'Connecting through ssh even if running as root',
type: 'boolean', type: 'boolean',
default: forceSSHDefault,
}) })
.option('known-hosts', { .option('known-hosts', {
description: 'path to known hosts file', description: 'path to known hosts file',
type: 'string', type: 'string',
default: sshDefault.knownHosts,
}) })
.option('base', { .option('base', {
alias: 'b', alias: 'b',
description: 'base path to wetty', description: 'base path to wetty',
type: 'string', type: 'string',
default: serverDefault.base,
}) })
.option('port', { .option('port', {
alias: 'p', alias: 'p',
description: 'wetty listen port', description: 'wetty listen port',
type: 'number', type: 'number',
default: serverDefault.port,
}) })
.option('host', { .option('host', {
description: 'wetty listen host', description: 'wetty listen host',
default: serverDefault.host,
type: 'string', type: 'string',
}) })
.option('command', { .option('command', {
alias: 'c', alias: 'c',
description: 'command to run in shell', description: 'command to run in shell',
type: 'string', type: 'string',
default: defaultCommand,
}) })
.option('bypass-helmet', { .option('bypass-helmet', {
description: 'disable helmet from placing security restrictions', description: 'disable helmet from placing security restrictions',
type: 'boolean', type: 'boolean',
default: serverDefault.bypassHelmet,
}) })
.option('help', { .option('help', {
alias: 'h', alias: 'h',
@ -104,33 +88,23 @@ const opts = yargs
description: 'Print help message', description: 'Print help message',
}) })
.boolean('allow_discovery').argv; .boolean('allow_discovery').argv;
if (!opts.help) { if (!opts.help) {
startServer( (async () => {
{ const config = await loadConfigFile(opts.conf);
user: opts['ssh-user'], const conf = mergeCliConf(opts, config);
host: opts['ssh-host'], console.log(conf);
auth: opts['ssh-auth'], startServer(
port: opts['ssh-port'], conf.ssh,
pass: opts['ssh-pass'], conf.server,
key: opts['ssh-key'], conf.command,
knownHosts: opts['known-hosts'], conf.forceSSH,
}, conf.ssl,
{ ).catch(err => {
base: opts.base, logger.error(err);
host: opts.host, process.exitCode = 1;
port: opts.port, });
title: opts.title, })();
bypassHelmet: opts['bypass-helmet'],
},
opts.command,
opts['force-ssh'],
isUndefined(opts['ssl-key']) || isUndefined(opts['ssl-cert'])
? undefined
: { key: opts['ssl-key'], cert: opts['ssl-cert'] },
).catch((err: Error) => {
logger.error(err);
process.exitCode = 1;
});
} else { } else {
yargs.showHelp(); yargs.showHelp();
process.exitCode = 0; process.exitCode = 0;

14
src/server.ts

@ -2,18 +2,18 @@
* Create WeTTY server * Create WeTTY server
* @module WeTTy * @module WeTTy
*/ */
import type { SSH, SSL, Server } from './shared/interfaces'; import type { SSH, SSL, Server } from './shared/interfaces.js';
import { getCommand } from './server/command'; import { getCommand } from './server/command.js';
import { logger } from './shared/logger'; import { logger } from './shared/logger.js';
import { login } from './server/login'; import { login } from './server/login.js';
import { server } from './server/socketServer'; import { server } from './server/socketServer.js';
import { spawn } from './server/spawn'; import { spawn } from './server/spawn.js';
import { import {
sshDefault, sshDefault,
serverDefault, serverDefault,
forceSSHDefault, forceSSHDefault,
defaultCommand, defaultCommand,
} from './shared/defaults'; } from './shared/defaults.js';
/** /**
* Starts WeTTy Server * Starts WeTTy Server

8
src/server/command.ts

@ -1,9 +1,9 @@
import url from 'url'; import url from 'url';
import { Socket } from 'socket.io'; import { Socket } from 'socket.io';
import { SSH } from '../shared/interfaces'; import { SSH } from '../shared/interfaces.js';
import { address } from './command/address'; import { address } from './command/address.js';
import { loginOptions } from './command/login'; import { loginOptions } from './command/login.js';
import { sshOptions } from './command/ssh'; import { sshOptions } from './command/ssh.js';
const localhost = (host: string): boolean => const localhost = (host: string): boolean =>
process.getuid() === 0 && process.getuid() === 0 &&

2
src/server/command/ssh.ts

@ -1,5 +1,5 @@
import isUndefined from 'lodash/isUndefined.js'; import isUndefined from 'lodash/isUndefined.js';
import { logger } from '../../shared/logger'; import { logger } from '../../shared/logger.js';
export function sshOptions( export function sshOptions(
{ {

2
src/server/login.ts

@ -1,5 +1,5 @@
import pty from 'node-pty'; import pty from 'node-pty';
import { xterm } from './shared/xterm'; import { xterm } from './shared/xterm.js';
export function login(socket: SocketIO.Socket): Promise<string> { export function login(socket: SocketIO.Socket): Promise<string> {
// Check request-header for username // Check request-header for username

14
src/server/socketServer.ts

@ -3,13 +3,13 @@ import compression from 'compression';
import helmet from 'helmet'; import helmet from 'helmet';
import winston from 'express-winston'; import winston from 'express-winston';
import type { SSL, SSLBuffer, Server } from '../shared/interfaces'; import type { SSL, SSLBuffer, Server } from '../shared/interfaces.js';
import { favicon, redirect } from './socketServer/middleware'; import { favicon, redirect } from './socketServer/middleware.js';
import { html } from './socketServer/html'; import { html } from './socketServer/html.js';
import { listen } from './socketServer/socket'; import { listen } from './socketServer/socket.js';
import { logger } from '../shared/logger'; import { logger } from '../shared/logger.js';
import { serveStatic, trim } from './socketServer/assets'; import { serveStatic, trim } from './socketServer/assets.js';
import { loadSSL } from './socketServer/ssl'; import { loadSSL } from './socketServer/ssl.js';
export async function server( export async function server(
{ base, port, host, title, bypassHelmet }: Server, { base, port, host, title, bypassHelmet }: Server,

2
src/server/socketServer/html.ts

@ -1,5 +1,5 @@
import type express from 'express'; import type express from 'express';
import { isDev } from '../../shared/env'; import { isDev } from '../../shared/env.js';
const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty']; const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty'];
const cssFiles = ['styles', 'options', 'overlay', 'terminal']; const cssFiles = ['styles', 'options', 'overlay', 'terminal'];

4
src/server/socketServer/socket.ts

@ -4,8 +4,8 @@ import http from 'http';
import https from 'https'; import https from 'https';
import isUndefined from 'lodash/isUndefined.js'; import isUndefined from 'lodash/isUndefined.js';
import { logger } from '../../shared/logger'; import { logger } from '../../shared/logger.js';
import type { SSLBuffer } from '../../shared/interfaces'; import type { SSLBuffer } from '../../shared/interfaces.js';
export const listen = ( export const listen = (
app: express.Express, app: express.Express,

2
src/server/socketServer/ssl.ts

@ -1,7 +1,7 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import isUndefined from 'lodash/isUndefined.js'; import isUndefined from 'lodash/isUndefined.js';
import type { SSL, SSLBuffer } from '../shared/interfaces'; import type { SSL, SSLBuffer } from '../../shared/interfaces';
export async function loadSSL(ssl?: SSL): Promise<SSLBuffer> { export async function loadSSL(ssl?: SSL): Promise<SSLBuffer> {
if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert)) if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert))

4
src/server/spawn.ts

@ -1,7 +1,7 @@
import isUndefined from 'lodash/isUndefined.js'; import isUndefined from 'lodash/isUndefined.js';
import pty from 'node-pty'; import pty from 'node-pty';
import { logger } from '../shared/logger'; import { logger } from '../shared/logger.js';
import { xterm } from './shared/xterm'; import { xterm } from './shared/xterm.js';
export function spawn(socket: SocketIO.Socket, args: string[]): void { export function spawn(socket: SocketIO.Socket, args: string[]): void {
const term = pty.spawn('/usr/bin/env', args, xterm); const term = pty.spawn('/usr/bin/env', args, xterm);

126
src/shared/config.ts

@ -0,0 +1,126 @@
import fs from 'fs-extra';
import path from 'path';
import JSON5 from 'json5';
import isUndefined from 'lodash/isUndefined.js';
import type { Config, SSH, Server } from './interfaces';
import {
sshDefault,
serverDefault,
forceSSHDefault,
defaultCommand,
} from './defaults.js';
/**
* Cast given value to boolean
*
* @param value - variable to cast
* @returns variable cast to boolean
*/
function ensureBoolean(value: any): boolean {
switch (value) {
case true:
case 'true':
case 1:
case '1':
case 'on':
case 'yes':
return true;
default:
return false;
}
}
/**
* Load JSON5 config from file and merge with default args
* If no path is provided the default config is returned
*
* @param filepath - path to config to load
* @returns variable cast to boolean
*/
export async function loadConfigFile(filepath?: string): Promise<Config> {
if (isUndefined(filepath)) {
return {
ssh: sshDefault,
server: serverDefault,
command: defaultCommand,
forceSSH: forceSSHDefault,
};
}
const content = await fs.readFile(path.resolve(filepath));
const parsed = JSON5.parse(content.toString()) as Config;
return {
ssh: isUndefined(parsed.ssh)
? sshDefault
: Object.assign(sshDefault, parsed.ssh),
server: isUndefined(parsed.server)
? serverDefault
: Object.assign(serverDefault, parsed.server),
command: isUndefined(parsed.command) ? defaultCommand : `${parsed.command}`,
forceSSH: isUndefined(parsed.forceSSH)
? forceSSHDefault
: ensureBoolean(parsed.forceSSH),
ssl: parsed.ssl,
};
}
/**
* Merge 2 objects removing undefined fields
*
* @param target - base object
* @param source - object to get new values from
* @returns merged object
*
*/
const objectAssign = (
target: Record<string, any>,
source: Record<string, any>,
): Record<string, any> =>
Object.fromEntries(
Object.entries(source).map(([key, value]) => [
key,
isUndefined(source[key]) ? target[key] : value,
]),
);
/**
* Merge cli arguemens with config object
*
* @param opts - Object containing cli args
* @param config - Config object
* @returns merged configuration
*
*/
export function mergeCliConf(
opts: Record<string, any>,
config: Config,
): Config {
const ssl = {
key: opts['ssl-key'],
cert: opts['ssl-cert'],
...config.ssl,
};
return {
ssh: objectAssign(config.ssh, {
user: opts['ssh-user'],
host: opts['ssh-host'],
auth: opts['ssh-auth'],
port: opts['ssh-port'],
pass: opts['ssh-pass'],
key: opts['ssh-key'],
knownHosts: opts['known-hosts'],
}) as SSH,
server: objectAssign(config.server, {
base: opts.base,
host: opts.host,
port: opts.port,
title: opts.title,
bypassHelmet: opts['bypass-helmet'],
}) as Server,
command: isUndefined(opts.command) ? config.command : opts.command,
forceSSH: isUndefined(opts['force-ssh'])
? config.forceSSH
: opts['force-ssh'],
ssl: isUndefined(ssl.key) || isUndefined(ssl.cert) ? undefined : ssl,
};
}

2
src/shared/defaults.ts

@ -1,4 +1,4 @@
import type { SSH, Server } from '../shared/interfaces'; import type { SSH, Server } from "./interfaces";
export const sshDefault: SSH = { export const sshDefault: SSH = {
user: process.env.SSHUSER || '', user: process.env.SSHUSER || '',

8
src/shared/interfaces.ts

@ -25,3 +25,11 @@ export interface Server {
base: string; base: string;
bypassHelmet: boolean; bypassHelmet: boolean;
} }
export interface Config {
ssh: SSH;
server: Server;
forceSSH: boolean;
command: string;
ssl?: SSL;
}

2
src/shared/logger.ts

@ -1,6 +1,6 @@
import winston from 'winston'; import winston from 'winston';
import { isDev } from './env'; import { isDev } from './env.js';
const { combine, timestamp, label, simple, json, colorize } = winston.format; const { combine, timestamp, label, simple, json, colorize } = winston.format;

30
tsconfig.json

@ -19,7 +19,35 @@
"removeComments": true, "removeComments": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
"baseUrl": ".",
"paths": {
"/*/web_modules/lodash.js": [
"node_modules/@types/lodash/index.d.ts"
],
"/*/web_modules/xterm.js": [
"node_modules/xterm/typings/xterm.d.ts",
"node_modules/xterm"
],
"/*/web_modules/xterm-addon-fit.js": [
"node_modules/xterm-addon-fit"
],
"/*/web_modules/toastify-js.js": [
"node_modules/toastify-js"
],
"/*/web_modules/file-type.js": [
"node_modules/file-type"
],
"/*/web_modules/socket.io-client.js": [
"node_modules/@types/socket.io-client/index.d.ts"
],
"/*/web_modules/@fortawesome/fontawesome-svg-core.js": [
"node_modules/@fortawesome/fontawesome-svg-core"
],
"/*/web_modules/@fortawesome/free-solid-svg-icons.js": [
"node_modules/@fortawesome/free-solid-svg-icons"
]
}
}, },
"include": [ "include": [
"src" "src"

Loading…
Cancel
Save