diff --git a/build.sh b/build.sh index c6d05dc..f449df8 100644 --- a/build.sh +++ b/build.sh @@ -21,7 +21,7 @@ show_usage() { clean() { : 'Clean repo and delete all built files' - rm -rf build + rm -rf build yarn-error.log } build-css() { @@ -39,6 +39,7 @@ build-js() { build-assets() { : 'Move assets not handled by sass and typescript to build dir' + mkdir -p build/assets cp src/assets/*.ico build/assets } @@ -50,7 +51,8 @@ watch() { --kill-others \ --success first \ "build-js --watch" \ - "build-css --watch" "nodemon ." + "build-css --watch" \ + "nodemon ." } build() { diff --git a/conf/config.json5 b/conf/config.json5 new file mode 100644 index 0000000..15ddf3f --- /dev/null +++ b/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', + } + */ +} diff --git a/src/client/wetty.ts b/src/client/wetty.ts index b7949c9..2b47f76 100644 --- a/src/client/wetty.ts +++ b/src/client/wetty.ts @@ -1,14 +1,17 @@ -import _ from 'lodash'; -import { dom, library } from '@fortawesome/fontawesome-svg-core'; -import { faCogs } from '@fortawesome/free-solid-svg-icons'; +import _ from '/../web_modules/lodash.js'; +import { + 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 { disconnect } from './wetty/disconnect'; -import { mobileKeyboard } from './wetty/mobile'; -import { overlay } from './shared/elements'; -import { socket } from './wetty/socket'; -import { verifyPrompt } from './shared/verify'; -import { terminal } from './wetty/term'; +import { FileDownloader } from './wetty/download.js'; +import { disconnect } from './wetty/disconnect.js'; +import { mobileKeyboard } from './wetty/mobile.js'; +import { overlay } from './shared/elements.js'; +import { socket } from './wetty/socket.js'; +import { verifyPrompt } from './shared/verify.js'; +import { terminal } from './wetty/term.js'; // Setup for fontawesome library.add(faCogs); diff --git a/src/client/wetty/disconnect.ts b/src/client/wetty/disconnect.ts index 88547d8..3535fb0 100644 --- a/src/client/wetty/disconnect.ts +++ b/src/client/wetty/disconnect.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _ from '/../web_modules/lodash.js'; import { verifyPrompt } from '../shared/verify'; import { overlay } from '../shared/elements'; diff --git a/src/client/wetty/download.ts b/src/client/wetty/download.ts index 0250486..286c8c0 100644 --- a/src/client/wetty/download.ts +++ b/src/client/wetty/download.ts @@ -1,5 +1,6 @@ -import Toastify from 'toastify-js'; -import fileType from 'file-type'; +// @ts-ignore +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_END = '\u001b[4i'; diff --git a/src/client/wetty/mobile.ts b/src/client/wetty/mobile.ts index 014c791..6fda8a3 100644 --- a/src/client/wetty/mobile.ts +++ b/src/client/wetty/mobile.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _ from '/../web_modules/lodash.js'; export function mobileKeyboard(): void { const [screen] = document.getElementsByClassName('xterm-screen'); diff --git a/src/client/wetty/shared/type.ts b/src/client/wetty/shared/type.ts index 44d68e6..3619019 100644 --- a/src/client/wetty/shared/type.ts +++ b/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 { resizeTerm(): void {} diff --git a/src/client/wetty/socket.ts b/src/client/wetty/socket.ts index 4195c8d..781531c 100644 --- a/src/client/wetty/socket.ts +++ b/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/[^/]+$'); export const trim = (str: string): string => str.replace(/\/*$/, ''); diff --git a/src/client/wetty/term.ts b/src/client/wetty/term.ts index 04f8290..2b5fb41 100644 --- a/src/client/wetty/term.ts +++ b/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 { FitAddon } from 'xterm-addon-fit'; -import { Terminal } from 'xterm'; +import { FitAddon } from '/../web_modules/xterm-addon-fit.js'; +import { Terminal } from '/../web_modules/xterm.js'; import type { Term } from './shared/type'; -import { configureTerm } from './term/confiruragtion'; -import { terminal as termElement } from '../shared/elements'; +import { configureTerm } from './term/confiruragtion.js'; +import { terminal as termElement } from '../shared/elements.js'; export function terminal(socket: typeof Socket): Term | undefined { const term = new Terminal() as Term; diff --git a/src/client/wetty/term/confiruragtion.ts b/src/client/wetty/term/confiruragtion.ts index 22487da..e1d9aa9 100644 --- a/src/client/wetty/term/confiruragtion.ts +++ b/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 { copySelected, copyShortcut } from './confiruragtion/clipboard'; diff --git a/src/client/wetty/term/confiruragtion/load.ts b/src/client/wetty/term/confiruragtion/load.ts index ecfcae6..f5dfe36 100644 --- a/src/client/wetty/term/confiruragtion/load.ts +++ b/src/client/wetty/term/confiruragtion/load.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _ from '/../../../web_modules/lodash.js'; export function loadOptions(): object { const defaultOptions = { fontSize: 14 }; diff --git a/src/main.ts b/src/main.ts index 62b66b3..1f8d497 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,17 +3,15 @@ * @module WeTTy */ import yargs from 'yargs'; -import isUndefined from 'lodash/isUndefined'; -import { logger } from './shared/logger'; -import { - sshDefault, - serverDefault, - forceSSHDefault, - defaultCommand, -} from './shared/defaults'; -import { startServer } from './server'; +import { logger } from './shared/logger.js'; +import { startServer } from './server.js'; +import { loadConfigFile, mergeCliConf } from './shared/config.js'; const opts = yargs + .options('conf', { + type: 'string', + description: 'config file to load config from', + }) .option('ssl-key', { type: 'string', description: 'path to SSL key', @@ -25,78 +23,64 @@ const opts = yargs .option('ssh-host', { description: 'ssh server host', type: 'string', - default: sshDefault.host, }) .option('ssh-port', { description: 'ssh server port', type: 'number', - default: sshDefault.port, }) .option('ssh-user', { description: 'ssh user', type: 'string', - default: sshDefault.user, }) .option('title', { description: 'window title', type: 'string', - default: serverDefault.title, }) .option('ssh-auth', { description: 'defaults to "password", you can use "publickey,password" instead', type: 'string', - default: sshDefault.auth, }) .option('ssh-pass', { description: 'ssh password', type: 'string', - default: sshDefault.pass, }) .option('ssh-key', { demand: false, description: 'path to an optional client private key (connection will be password-less and insecure!)', type: 'string', - default: sshDefault.key, }) .option('force-ssh', { description: 'Connecting through ssh even if running as root', type: 'boolean', - default: forceSSHDefault, }) .option('known-hosts', { description: 'path to known hosts file', type: 'string', - default: sshDefault.knownHosts, }) .option('base', { alias: 'b', description: 'base path to wetty', type: 'string', - default: serverDefault.base, }) .option('port', { alias: 'p', description: 'wetty listen port', type: 'number', - default: serverDefault.port, }) .option('host', { description: 'wetty listen host', - default: serverDefault.host, type: 'string', }) .option('command', { alias: 'c', description: 'command to run in shell', type: 'string', - default: defaultCommand, }) .option('bypass-helmet', { description: 'disable helmet from placing security restrictions', type: 'boolean', - default: serverDefault.bypassHelmet, }) .option('help', { alias: 'h', @@ -104,33 +88,23 @@ const opts = yargs description: 'Print help message', }) .boolean('allow_discovery').argv; + if (!opts.help) { - startServer( - { - 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'], - }, - { - base: opts.base, - host: opts.host, - 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; - }); + (async () => { + const config = await loadConfigFile(opts.conf); + const conf = mergeCliConf(opts, config); + console.log(conf); + startServer( + conf.ssh, + conf.server, + conf.command, + conf.forceSSH, + conf.ssl, + ).catch(err => { + logger.error(err); + process.exitCode = 1; + }); + })(); } else { yargs.showHelp(); process.exitCode = 0; diff --git a/src/server.ts b/src/server.ts index d433c7b..4fcdc8e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,18 +2,18 @@ * Create WeTTY server * @module WeTTy */ -import type { SSH, SSL, Server } from './shared/interfaces'; -import { getCommand } from './server/command'; -import { logger } from './shared/logger'; -import { login } from './server/login'; -import { server } from './server/socketServer'; -import { spawn } from './server/spawn'; +import type { SSH, SSL, Server } from './shared/interfaces.js'; +import { getCommand } from './server/command.js'; +import { logger } from './shared/logger.js'; +import { login } from './server/login.js'; +import { server } from './server/socketServer.js'; +import { spawn } from './server/spawn.js'; import { sshDefault, serverDefault, forceSSHDefault, defaultCommand, -} from './shared/defaults'; +} from './shared/defaults.js'; /** * Starts WeTTy Server diff --git a/src/server/command.ts b/src/server/command.ts index e6302e2..e44b815 100644 --- a/src/server/command.ts +++ b/src/server/command.ts @@ -1,9 +1,9 @@ import url from 'url'; import { Socket } from 'socket.io'; -import { SSH } from '../shared/interfaces'; -import { address } from './command/address'; -import { loginOptions } from './command/login'; -import { sshOptions } from './command/ssh'; +import { SSH } from '../shared/interfaces.js'; +import { address } from './command/address.js'; +import { loginOptions } from './command/login.js'; +import { sshOptions } from './command/ssh.js'; const localhost = (host: string): boolean => process.getuid() === 0 && diff --git a/src/server/command/ssh.ts b/src/server/command/ssh.ts index cf521a1..6e3a2a6 100644 --- a/src/server/command/ssh.ts +++ b/src/server/command/ssh.ts @@ -1,5 +1,5 @@ import isUndefined from 'lodash/isUndefined.js'; -import { logger } from '../../shared/logger'; +import { logger } from '../../shared/logger.js'; export function sshOptions( { diff --git a/src/server/login.ts b/src/server/login.ts index 756cd47..6cc035b 100644 --- a/src/server/login.ts +++ b/src/server/login.ts @@ -1,5 +1,5 @@ import pty from 'node-pty'; -import { xterm } from './shared/xterm'; +import { xterm } from './shared/xterm.js'; export function login(socket: SocketIO.Socket): Promise { // Check request-header for username diff --git a/src/server/socketServer.ts b/src/server/socketServer.ts index a9199dc..372ddac 100644 --- a/src/server/socketServer.ts +++ b/src/server/socketServer.ts @@ -3,13 +3,13 @@ import compression from 'compression'; import helmet from 'helmet'; import winston from 'express-winston'; -import type { SSL, SSLBuffer, Server } from '../shared/interfaces'; -import { favicon, redirect } from './socketServer/middleware'; -import { html } from './socketServer/html'; -import { listen } from './socketServer/socket'; -import { logger } from '../shared/logger'; -import { serveStatic, trim } from './socketServer/assets'; -import { loadSSL } from './socketServer/ssl'; +import type { SSL, SSLBuffer, Server } from '../shared/interfaces.js'; +import { favicon, redirect } from './socketServer/middleware.js'; +import { html } from './socketServer/html.js'; +import { listen } from './socketServer/socket.js'; +import { logger } from '../shared/logger.js'; +import { serveStatic, trim } from './socketServer/assets.js'; +import { loadSSL } from './socketServer/ssl.js'; export async function server( { base, port, host, title, bypassHelmet }: Server, diff --git a/src/server/socketServer/html.ts b/src/server/socketServer/html.ts index feb9c8c..47785ed 100644 --- a/src/server/socketServer/html.ts +++ b/src/server/socketServer/html.ts @@ -1,5 +1,5 @@ import type express from 'express'; -import { isDev } from '../../shared/env'; +import { isDev } from '../../shared/env.js'; const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty']; const cssFiles = ['styles', 'options', 'overlay', 'terminal']; diff --git a/src/server/socketServer/socket.ts b/src/server/socketServer/socket.ts index d35fa94..2798130 100644 --- a/src/server/socketServer/socket.ts +++ b/src/server/socketServer/socket.ts @@ -4,8 +4,8 @@ import http from 'http'; import https from 'https'; import isUndefined from 'lodash/isUndefined.js'; -import { logger } from '../../shared/logger'; -import type { SSLBuffer } from '../../shared/interfaces'; +import { logger } from '../../shared/logger.js'; +import type { SSLBuffer } from '../../shared/interfaces.js'; export const listen = ( app: express.Express, diff --git a/src/server/socketServer/ssl.ts b/src/server/socketServer/ssl.ts index ad132fb..690dd7d 100644 --- a/src/server/socketServer/ssl.ts +++ b/src/server/socketServer/ssl.ts @@ -1,7 +1,7 @@ import fs from 'fs-extra'; import path from 'path'; 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 { if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert)) diff --git a/src/server/spawn.ts b/src/server/spawn.ts index a7c1566..1a1896f 100644 --- a/src/server/spawn.ts +++ b/src/server/spawn.ts @@ -1,7 +1,7 @@ import isUndefined from 'lodash/isUndefined.js'; import pty from 'node-pty'; -import { logger } from '../shared/logger'; -import { xterm } from './shared/xterm'; +import { logger } from '../shared/logger.js'; +import { xterm } from './shared/xterm.js'; export function spawn(socket: SocketIO.Socket, args: string[]): void { const term = pty.spawn('/usr/bin/env', args, xterm); diff --git a/src/shared/config.ts b/src/shared/config.ts new file mode 100644 index 0000000..0db8daf --- /dev/null +++ b/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 { + 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, + source: Record, +): Record => + 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, + 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, + }; +} diff --git a/src/shared/defaults.ts b/src/shared/defaults.ts index 4646b79..7af776b 100644 --- a/src/shared/defaults.ts +++ b/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 = { user: process.env.SSHUSER || '', diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index 5ccabed..3cee8cc 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -25,3 +25,11 @@ export interface Server { base: string; bypassHelmet: boolean; } + +export interface Config { + ssh: SSH; + server: Server; + forceSSH: boolean; + command: string; + ssl?: SSL; +} diff --git a/src/shared/logger.ts b/src/shared/logger.ts index c33b56c..6cf2c62 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -1,6 +1,6 @@ import winston from 'winston'; -import { isDev } from './env'; +import { isDev } from './env.js'; const { combine, timestamp, label, simple, json, colorize } = winston.format; diff --git a/tsconfig.json b/tsconfig.json index fb9a761..12cadb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,35 @@ "removeComments": true, "skipLibCheck": 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": [ "src"