67 changed files with 2210 additions and 1762 deletions
			
			
		
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -0,0 +1,25 @@ | 
				
			|||||
 | 
					[package] | 
				
			||||
 | 
					name = "wetty" | 
				
			||||
 | 
					version = "2.1.0" | 
				
			||||
 | 
					authors = ["butlerx <butlerx@notthe.cloud>"] | 
				
			||||
 | 
					edition = "2018" | 
				
			||||
 | 
					description = "WeTTY = Web + TTY. Terminal access in browser over http/https" | 
				
			||||
 | 
					repository = "https://github.com/butlerx/wetty" | 
				
			||||
 | 
					license = "MIT" | 
				
			||||
 | 
					readme = "README.md" | 
				
			||||
 | 
					include = ["src/**/*", "Cargo.toml", "configs/**/*.yaml", "client/**/*"] | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					[dependencies] | 
				
			||||
 | 
					futures = "0.3" | 
				
			||||
 | 
					handlebars = "3.5" | 
				
			||||
 | 
					lazy_static = "1.4.0" | 
				
			||||
 | 
					log = "0.4" | 
				
			||||
 | 
					prometheus = { version = "0.11.0", features = ["process"] } | 
				
			||||
 | 
					pty = "0.2" | 
				
			||||
 | 
					serde = { version = "1.0", features = ["derive"] } | 
				
			||||
 | 
					serde_json = "1.0" | 
				
			||||
 | 
					stderrlog = "0.4.3" | 
				
			||||
 | 
					structopt = "0.3.21" | 
				
			||||
 | 
					tokio = { version = "1", features = ["full"] } | 
				
			||||
 | 
					toml = "0.5" | 
				
			||||
 | 
					warp = "0.3" | 
				
			||||
| 
		 Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB  | 
@ -1,16 +0,0 @@ | 
				
			|||||
import { createInterface } from 'readline'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
ask('Enter your username'); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
function ask(question: string): Promise<string> { | 
					 | 
				
			||||
  const rlp = createInterface({ | 
					 | 
				
			||||
    input: process.stdin, | 
					 | 
				
			||||
    output: process.stdout, | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
  return new Promise(resolve => { | 
					 | 
				
			||||
    rlp.question(`${question}: `, answer => { | 
					 | 
				
			||||
      rlp.close(); | 
					 | 
				
			||||
      resolve(answer); | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -0,0 +1,114 @@ | 
				
			|||||
 | 
					extern crate toml; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use serde::{Deserialize, Serialize}; | 
				
			||||
 | 
					use std::{ | 
				
			||||
 | 
					    env, fs, | 
				
			||||
 | 
					    net::{IpAddr, Ipv4Addr, SocketAddr}, | 
				
			||||
 | 
					    path::PathBuf, | 
				
			||||
 | 
					    str::FromStr, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					use toml::de::Error; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn get_env<T: FromStr>(key: &str, default: T) -> T { | 
				
			||||
 | 
					    match env::var(key) { | 
				
			||||
 | 
					        Ok(val) => val.parse::<T>().unwrap_or(default), | 
				
			||||
 | 
					        Err(_) => default, | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Clone, Deserialize, Debug, Serialize)] | 
				
			||||
 | 
					pub struct SSH { | 
				
			||||
 | 
					    /// Server to ssh to
 | 
				
			||||
 | 
					    pub host: String, | 
				
			||||
 | 
					    /// default user to use when ssh-ing
 | 
				
			||||
 | 
					    pub user: String, | 
				
			||||
 | 
					    /// shh authentication, method. Defaults to "password", you can use "publickey,password" instead'
 | 
				
			||||
 | 
					    pub auth: String, | 
				
			||||
 | 
					    /// Password to use when sshing
 | 
				
			||||
 | 
					    pub pass: Option<String>, | 
				
			||||
 | 
					    /// path to an optional client private key, connection will be password-less and insecure!
 | 
				
			||||
 | 
					    pub key: Option<String>, | 
				
			||||
 | 
					    /// Port to ssh to
 | 
				
			||||
 | 
					    pub port: i16, | 
				
			||||
 | 
					    /// ssh knownHosts file to use
 | 
				
			||||
 | 
					    pub known_hosts: String, | 
				
			||||
 | 
					    /// alternative ssh configuration file, see "-F" option in ssh(1)
 | 
				
			||||
 | 
					    pub config: Option<String>, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					impl Default for SSH { | 
				
			||||
 | 
					    fn default() -> Self { | 
				
			||||
 | 
					        SSH { | 
				
			||||
 | 
					            user: get_env("SSHUSER", "".to_string()), | 
				
			||||
 | 
					            host: get_env("SSHHOST", "localhost".to_string()), | 
				
			||||
 | 
					            auth: get_env("SSHAUTH", "password".to_string()), | 
				
			||||
 | 
					            port: get_env("SSHPORT", 22), | 
				
			||||
 | 
					            known_hosts: get_env("KNOWNHOSTS", "/dev/null".to_string()), | 
				
			||||
 | 
					            config: None, | 
				
			||||
 | 
					            key: None, | 
				
			||||
 | 
					            pass: None, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Clone, Deserialize, Debug, Serialize)] | 
				
			||||
 | 
					pub struct Server { | 
				
			||||
 | 
					    /// URL base to serve resources from
 | 
				
			||||
 | 
					    pub base: String, | 
				
			||||
 | 
					    /// address to listen on including the port eg: 0.0.0.0:3000
 | 
				
			||||
 | 
					    pub address: SocketAddr, | 
				
			||||
 | 
					    // Page title
 | 
				
			||||
 | 
					    pub title: String, | 
				
			||||
 | 
					    /// allow wetty to be embedded in an iframe
 | 
				
			||||
 | 
					    pub allow_iframe: bool, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					impl Default for Server { | 
				
			||||
 | 
					    fn default() -> Self { | 
				
			||||
 | 
					        Server { | 
				
			||||
 | 
					            base: get_env("BASE", "wetty".to_string()), | 
				
			||||
 | 
					            address: get_env( | 
				
			||||
 | 
					                "ADDRESS", | 
				
			||||
 | 
					                SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3030), | 
				
			||||
 | 
					            ), | 
				
			||||
 | 
					            title: get_env("TITLE", "WeTTy - The Web Terminal Emulator".to_string()), | 
				
			||||
 | 
					            allow_iframe: false, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn default_command() -> String { | 
				
			||||
 | 
					    get_env("COMMAND", "login".to_string()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn force_ssh() -> bool { | 
				
			||||
 | 
					    get_env("FORCESSH", false) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Clone, Deserialize, Debug, Serialize)] | 
				
			||||
 | 
					pub struct Config { | 
				
			||||
 | 
					    #[serde(default)] | 
				
			||||
 | 
					    pub server: Server, | 
				
			||||
 | 
					    #[serde(default)] | 
				
			||||
 | 
					    pub ssh: SSH, | 
				
			||||
 | 
					    #[serde(default = "force_ssh")] | 
				
			||||
 | 
					    /// Force sshing to local machine over login if running as root
 | 
				
			||||
 | 
					    pub force_ssh: bool, | 
				
			||||
 | 
					    #[serde(default = "default_command")] | 
				
			||||
 | 
					    /// Command to run on server. Login will use ssh if connecting to different server
 | 
				
			||||
 | 
					    pub default_command: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl Config { | 
				
			||||
 | 
					    pub fn from_file(filename: &PathBuf) -> Result<Self, Error> { | 
				
			||||
 | 
					        debug!("loading config; path={:?}", filename); | 
				
			||||
 | 
					        let contents = fs::read_to_string(filename).expect("Something went wrong reading the file"); | 
				
			||||
 | 
					        toml::from_str(&*contents) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn print_default() { | 
				
			||||
 | 
					        let conf: Config = toml::from_str("").expect("failed to set default"); | 
				
			||||
 | 
					        println!( | 
				
			||||
 | 
					            "{}", | 
				
			||||
 | 
					            toml::Value::try_from(conf).expect("failed to Serialize conf to toml") | 
				
			||||
 | 
					        ) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,85 @@ | 
				
			|||||
 | 
					#[macro_use] extern crate lazy_static; | 
				
			||||
 | 
					#[macro_use] extern crate log; | 
				
			||||
 | 
					#[macro_use] extern crate prometheus; | 
				
			||||
 | 
					extern crate pty; | 
				
			||||
 | 
					extern crate stderrlog; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					mod config; | 
				
			||||
 | 
					mod middlewares; | 
				
			||||
 | 
					mod routes; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use config::{Server,Config}; | 
				
			||||
 | 
					use handlebars::Handlebars; | 
				
			||||
 | 
					use std::sync::Arc; | 
				
			||||
 | 
					use std::{io::Result, path::PathBuf}; | 
				
			||||
 | 
					use structopt::StructOpt; | 
				
			||||
 | 
					use warp::Filter; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// WeTTy server
 | 
				
			||||
 | 
					#[derive(Debug, StructOpt)] | 
				
			||||
 | 
					struct Args { | 
				
			||||
 | 
					    /// Configuration file config path
 | 
				
			||||
 | 
					    #[structopt(short, long, parse(from_os_str), default_value = "configs/config.toml")] | 
				
			||||
 | 
					    config: PathBuf, | 
				
			||||
 | 
					    /// Silence all output
 | 
				
			||||
 | 
					    #[structopt(short, long)] | 
				
			||||
 | 
					    quiet: bool, | 
				
			||||
 | 
					    /// Increase message verbosity
 | 
				
			||||
 | 
					    #[structopt(short, long, parse(from_occurrences))] | 
				
			||||
 | 
					    verbose: usize, | 
				
			||||
 | 
					    /// Print default config
 | 
				
			||||
 | 
					    #[structopt(short, long)] | 
				
			||||
 | 
					    print: bool, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[tokio::main] | 
				
			||||
 | 
					async fn main() -> Result<()> { | 
				
			||||
 | 
					    let args = Args::from_args(); | 
				
			||||
 | 
					    if args.print { | 
				
			||||
 | 
					        return Ok(Config::print_default()); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    stderrlog::new() | 
				
			||||
 | 
					        .module(module_path!()) | 
				
			||||
 | 
					        .quiet(args.quiet) | 
				
			||||
 | 
					        .verbosity(args.verbose) | 
				
			||||
 | 
					        .timestamp(stderrlog::Timestamp::Second) | 
				
			||||
 | 
					        .init() | 
				
			||||
 | 
					        .unwrap(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let log = warp::log("wetty"); | 
				
			||||
 | 
					    let metrics = warp::log::custom(middlewares::metrics); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let conf = Config::from_file(&args.config)?; | 
				
			||||
 | 
					    info!("config loaded; path={:?}", args.config); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let metrics_route = warp::path!("metrics").and_then(routes::metrics::handler).with(metrics); | 
				
			||||
 | 
					    let health = warp::path!("health").and_then(routes::health::handler).with(metrics); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let base = warp::path(conf.server.base); | 
				
			||||
 | 
					    let socket = base.clone().and(warp::path!("socket.io")) | 
				
			||||
 | 
					        .and(warp::ws()) | 
				
			||||
 | 
					        .and_then(routes::socket::handler) | 
				
			||||
 | 
					        .with(metrics); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let hb = Arc::new(Handlebars::new()); | 
				
			||||
 | 
					    let client = base.clone().or(base.clone().and(warp::path!("ssh" / String ))) | 
				
			||||
 | 
					        .and(||warp::any().map(|| conf.server.clone())) | 
				
			||||
 | 
					        .map(move |_user, config: Server|routes::html::render(hb.clone(), config.title,config.base)) | 
				
			||||
 | 
					        .with(metrics); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let routes = warp::fs::dir("client/build") | 
				
			||||
 | 
					        .or(client) | 
				
			||||
 | 
					        .or(metrics_route) | 
				
			||||
 | 
					        .or(health) | 
				
			||||
 | 
					        .or(socket) | 
				
			||||
 | 
					        .with(log); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    info!( | 
				
			||||
 | 
					        "Server started; address={}", | 
				
			||||
 | 
					        conf.server.address, | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					    warp::serve(routes).run(conf.server.address).await; | 
				
			||||
 | 
					    info!("Server shutting down"); | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
@ -1,114 +0,0 @@ | 
				
			|||||
#!/usr/bin/env node | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Create WeTTY server | 
					 | 
				
			||||
 * @module WeTTy | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * This is the cli Interface for wetty. | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
import yargs from 'yargs'; | 
					 | 
				
			||||
import { logger } from './shared/logger.js'; | 
					 | 
				
			||||
import { start } 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', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssl-cert', { | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
    description: 'path to SSL certificate', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-host', { | 
					 | 
				
			||||
    description: 'ssh server host', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-port', { | 
					 | 
				
			||||
    description: 'ssh server port', | 
					 | 
				
			||||
    type: 'number', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-user', { | 
					 | 
				
			||||
    description: 'ssh user', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('title', { | 
					 | 
				
			||||
    description: 'window title', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-auth', { | 
					 | 
				
			||||
    description: | 
					 | 
				
			||||
      'defaults to "password", you can use "publickey,password" instead', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-pass', { | 
					 | 
				
			||||
    description: 'ssh password', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-key', { | 
					 | 
				
			||||
    demand: false, | 
					 | 
				
			||||
    description: | 
					 | 
				
			||||
      'path to an optional client private key (connection will be password-less and insecure!)', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('ssh-config', { | 
					 | 
				
			||||
    description: 'Specifies an alternative ssh configuration file. For further details see "-F" option in ssh(1)', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('force-ssh', { | 
					 | 
				
			||||
    description: 'Connecting through ssh even if running as root', | 
					 | 
				
			||||
    type: 'boolean', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('known-hosts', { | 
					 | 
				
			||||
    description: 'path to known hosts file', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('base', { | 
					 | 
				
			||||
    alias: 'b', | 
					 | 
				
			||||
    description: 'base path to wetty', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('port', { | 
					 | 
				
			||||
    alias: 'p', | 
					 | 
				
			||||
    description: 'wetty listen port', | 
					 | 
				
			||||
    type: 'number', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('host', { | 
					 | 
				
			||||
    description: 'wetty listen host', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('command', { | 
					 | 
				
			||||
    alias: 'c', | 
					 | 
				
			||||
    description: 'command to run in shell', | 
					 | 
				
			||||
    type: 'string', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('allow-iframe', { | 
					 | 
				
			||||
    description: | 
					 | 
				
			||||
      'Allow wetty to be embedded in an iframe, defaults to allowing same origin', | 
					 | 
				
			||||
    type: 'boolean', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .option('help', { | 
					 | 
				
			||||
    alias: 'h', | 
					 | 
				
			||||
    type: 'boolean', | 
					 | 
				
			||||
    description: 'Print help message', | 
					 | 
				
			||||
  }) | 
					 | 
				
			||||
  .boolean('allow_discovery').argv; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
if (!opts.help) { | 
					 | 
				
			||||
  loadConfigFile(opts.conf) | 
					 | 
				
			||||
    .then(config => mergeCliConf(opts, config)) | 
					 | 
				
			||||
    .then(conf => | 
					 | 
				
			||||
      start(conf.ssh, conf.server, conf.command, conf.forceSSH, conf.ssl), | 
					 | 
				
			||||
    ) | 
					 | 
				
			||||
    .catch((err: Error) => { | 
					 | 
				
			||||
      logger.error(err); | 
					 | 
				
			||||
      process.exitCode = 1; | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
} else { | 
					 | 
				
			||||
  yargs.showHelp(); | 
					 | 
				
			||||
  process.exitCode = 0; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -0,0 +1,48 @@ | 
				
			|||||
 | 
					use prometheus::{HistogramVec, IntCounter, IntCounterVec}; | 
				
			||||
 | 
					use warp::filters::log::Info; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					lazy_static! { | 
				
			||||
 | 
					    static ref INCOMING_REQUESTS: IntCounter = | 
				
			||||
 | 
					        register_int_counter!("incoming_requests", "Incoming Requests") | 
				
			||||
 | 
					            .expect("metric can be created"); | 
				
			||||
 | 
					    static ref RESPONSE_CODE_COLLECTOR: IntCounterVec = register_int_counter_vec!( | 
				
			||||
 | 
					        opts!("response_code", "Response Codes"), | 
				
			||||
 | 
					        &["method", "path", "status", "type"] | 
				
			||||
 | 
					    ) | 
				
			||||
 | 
					    .expect("metric can be created"); | 
				
			||||
 | 
					    static ref RESPONSE_TIME_COLLECTOR: HistogramVec = register_histogram_vec!( | 
				
			||||
 | 
					        histogram_opts!("response_time", "Response Times"), | 
				
			||||
 | 
					        &["method", "path"] | 
				
			||||
 | 
					    ) | 
				
			||||
 | 
					    .expect("metric can be created"); | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn metrics(info: Info) { | 
				
			||||
 | 
					    let (method, path, status_code) = ( | 
				
			||||
 | 
					        info.method().to_string(), | 
				
			||||
 | 
					        info.path(), | 
				
			||||
 | 
					        info.status().as_u16(), | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					    INCOMING_REQUESTS.inc(); | 
				
			||||
 | 
					    RESPONSE_TIME_COLLECTOR | 
				
			||||
 | 
					        .with_label_values(&[&method, &path]) | 
				
			||||
 | 
					        .observe(info.elapsed().as_secs_f64()); | 
				
			||||
 | 
					    match status_code { | 
				
			||||
 | 
					        500..=599 => RESPONSE_CODE_COLLECTOR | 
				
			||||
 | 
					            .with_label_values(&[&method, &path, &status_code.to_string(), "500"]) | 
				
			||||
 | 
					            .inc(), | 
				
			||||
 | 
					        400..=499 => RESPONSE_CODE_COLLECTOR | 
				
			||||
 | 
					            .with_label_values(&[&method, &path, &status_code.to_string(), "400"]) | 
				
			||||
 | 
					            .inc(), | 
				
			||||
 | 
					        300..=399 => RESPONSE_CODE_COLLECTOR | 
				
			||||
 | 
					            .with_label_values(&[&method, &path, &status_code.to_string(), "300"]) | 
				
			||||
 | 
					            .inc(), | 
				
			||||
 | 
					        200..=299 => RESPONSE_CODE_COLLECTOR | 
				
			||||
 | 
					            .with_label_values(&[&method, &path, &status_code.to_string(), "200"]) | 
				
			||||
 | 
					            .inc(), | 
				
			||||
 | 
					        100..=199 => RESPONSE_CODE_COLLECTOR | 
				
			||||
 | 
					            .with_label_values(&[&method, &path, &status_code.to_string(), "100"]) | 
				
			||||
 | 
					            .inc(), | 
				
			||||
 | 
					        _ => (), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			|||||
 | 
					use crate::routes::Responce; | 
				
			||||
 | 
					use warp::{http::StatusCode, Reply}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub async fn handler() -> Responce<impl Reply> { | 
				
			||||
 | 
					    Ok(StatusCode::OK) | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,50 @@ | 
				
			|||||
 | 
					use handlebars::Handlebars; | 
				
			||||
 | 
					use serde_json::json; | 
				
			||||
 | 
					use std::sync::Arc; | 
				
			||||
 | 
					use warp::Reply; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn render(hbs: Arc<Handlebars>, title: String, base: String) -> impl Reply { | 
				
			||||
 | 
					    let value = json!({ | 
				
			||||
 | 
					        "title": title, | 
				
			||||
 | 
					        "base": base, | 
				
			||||
 | 
					        "js": ["wetty"], | 
				
			||||
 | 
					        "css": ["styles", "options", "overlay", "terminal"] | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					    warp::reply::html( | 
				
			||||
 | 
					        hbs.render_template(INDEX_HTML, &value) | 
				
			||||
 | 
					            .unwrap_or_else(|err| err.to_string()), | 
				
			||||
 | 
					    ) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub static INDEX_HTML: &str = r#"<!DOCTYPE html> | 
				
			||||
 | 
					<html lang="en"> | 
				
			||||
 | 
					  <head> | 
				
			||||
 | 
					    <meta charset="utf8"> | 
				
			||||
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge"> | 
				
			||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | 
				
			||||
 | 
					    <link rel="icon" type="image/x-icon" href="{{base}}/favicon.ico"> | 
				
			||||
 | 
					    <title>{{title}}</title> | 
				
			||||
 | 
					    {{#each css}} | 
				
			||||
 | 
					    <link rel="stylesheet" href="{{base}}/assets/css/{{this}}.css" /> | 
				
			||||
 | 
					    {{/each}} | 
				
			||||
 | 
					  </head> | 
				
			||||
 | 
					  <body> | 
				
			||||
 | 
					    <div id="overlay"> | 
				
			||||
 | 
					      <div class="error"> | 
				
			||||
 | 
					        <div id="msg"></div> | 
				
			||||
 | 
					        <input type="button" onclick="location.reload();" value="reconnect" /> | 
				
			||||
 | 
					      </div> | 
				
			||||
 | 
					    </div> | 
				
			||||
 | 
					    <div id="options"> | 
				
			||||
 | 
					      <a class="toggler" | 
				
			||||
 | 
					         href="\#" | 
				
			||||
 | 
					         alt="Toggle options" | 
				
			||||
 | 
					       ><i class="fas fa-cogs"></i></a> | 
				
			||||
 | 
					      <textarea class="editor"></textarea> | 
				
			||||
 | 
					    </div> | 
				
			||||
 | 
					    <div id="terminal"></div> | 
				
			||||
 | 
					    {{#each js}} | 
				
			||||
 | 
					    <script type="module" src="{{base}}/client/{{this}}.js"></script> | 
				
			||||
 | 
					    {{/each}} | 
				
			||||
 | 
					  </body> | 
				
			||||
 | 
					</html>"#; | 
				
			||||
@ -0,0 +1,20 @@ | 
				
			|||||
 | 
					use crate::routes::Responce; | 
				
			||||
 | 
					use prometheus::{self, Encoder}; | 
				
			||||
 | 
					use warp::Reply; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub async fn handler() -> Responce<impl Reply> { | 
				
			||||
 | 
					    let encoder = prometheus::TextEncoder::new(); | 
				
			||||
 | 
					    let mut buffer = Vec::new(); | 
				
			||||
 | 
					    if let Err(e) = encoder.encode(&prometheus::gather(), &mut buffer) { | 
				
			||||
 | 
					        error!("could not encode prometheus metrics; error={}", e); | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					    let res = match String::from_utf8(buffer.clone()) { | 
				
			||||
 | 
					        Ok(v) => v, | 
				
			||||
 | 
					        Err(e) => { | 
				
			||||
 | 
					            error!("prometheus metrics could not be from_utf8'd; error={}", e); | 
				
			||||
 | 
					            String::default() | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					    buffer.clear(); | 
				
			||||
 | 
					    Ok(res) | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,8 @@ | 
				
			|||||
 | 
					use warp::Rejection; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub mod health; | 
				
			||||
 | 
					pub mod html; | 
				
			||||
 | 
					pub mod metrics; | 
				
			||||
 | 
					pub mod socket; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					type Responce<T> = std::result::Result<T, Rejection>; | 
				
			||||
@ -0,0 +1,45 @@ | 
				
			|||||
 | 
					use crate::routes::Responce; | 
				
			||||
 | 
					use futures::{SinkExt, StreamExt}; | 
				
			||||
 | 
					use prometheus::IntGauge; | 
				
			||||
 | 
					use serde::{Deserialize, Serialize}; | 
				
			||||
 | 
					use warp::{ | 
				
			||||
 | 
					    ws::{Message, WebSocket, Ws}, | 
				
			||||
 | 
					    Reply, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					lazy_static! { | 
				
			||||
 | 
					    static ref CONNECTED_CLIENTS: IntGauge = register_int_gauge!("connected_clients", "Connected Clients").expect("metric can be created"); | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub async fn handler(ws: Ws) -> Responce<impl Reply> { | 
				
			||||
 | 
					    Ok(ws.on_upgrade(move |socket| handle_connection(socket, ))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					async fn handle_connection(ws: WebSocket) { | 
				
			||||
 | 
					    let (mut sender, mut rcv) = ws.split(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    info!("Connection Opened;"); | 
				
			||||
 | 
					    CONNECTED_CLIENTS.inc(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    while let Some(event) = rcv.next().await { | 
				
			||||
 | 
					        match event { | 
				
			||||
 | 
					            Ok(msg) => { | 
				
			||||
 | 
					                debug!( | 
				
			||||
 | 
					                    "Message recieved from websocket; msg={:?}", | 
				
			||||
 | 
					                    msg,                ); | 
				
			||||
 | 
					                if let Ok(txt) = msg.to_str() { | 
				
			||||
 | 
					                    //handle message
 | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            Err(err) => { | 
				
			||||
 | 
					                error!( | 
				
			||||
 | 
					                    "websocket error; error={}", | 
				
			||||
 | 
					                    err | 
				
			||||
 | 
					                ); | 
				
			||||
 | 
					                break; | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    info!("Connection Dropped;"); | 
				
			||||
 | 
					    CONNECTED_CLIENTS.dec(); | 
				
			||||
 | 
					} | 
				
			||||
@ -1,73 +0,0 @@ | 
				
			|||||
/** | 
					 | 
				
			||||
 * Create WeTTY server | 
					 | 
				
			||||
 * @module WeTTy | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
import type SocketIO from 'socket.io'; | 
					 | 
				
			||||
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.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Starts WeTTy Server | 
					 | 
				
			||||
 * @name startServer | 
					 | 
				
			||||
 * @returns Promise that resolves SocketIO server | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
export async function start( | 
					 | 
				
			||||
  ssh: SSH = sshDefault, | 
					 | 
				
			||||
  serverConf: Server = serverDefault, | 
					 | 
				
			||||
  command: string = defaultCommand, | 
					 | 
				
			||||
  forcessh: boolean = forceSSHDefault, | 
					 | 
				
			||||
  ssl?: SSL, | 
					 | 
				
			||||
): Promise<SocketIO.Server> { | 
					 | 
				
			||||
  if (ssh.key) { | 
					 | 
				
			||||
    logger.warn(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 | 
					 | 
				
			||||
! Password-less auth enabled using private key from ${ssh.key}. | 
					 | 
				
			||||
! This is dangerous, anything that reaches the wetty server | 
					 | 
				
			||||
! will be able to run remote operations without authentication. | 
					 | 
				
			||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
 | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
 | 
					 | 
				
			||||
  const io = await server(serverConf, ssl); | 
					 | 
				
			||||
  /** | 
					 | 
				
			||||
   * Wetty server connected too | 
					 | 
				
			||||
   * @fires WeTTy#connnection | 
					 | 
				
			||||
   */ | 
					 | 
				
			||||
  io.on('connection', async (socket: SocketIO.Socket) => { | 
					 | 
				
			||||
    /** | 
					 | 
				
			||||
     * @event wetty#connection | 
					 | 
				
			||||
     * @name connection | 
					 | 
				
			||||
     */ | 
					 | 
				
			||||
    logger.info('Connection accepted.'); | 
					 | 
				
			||||
    const [args, sshUser] = getCommand(socket, ssh, command, forcessh); | 
					 | 
				
			||||
    logger.debug('Command Generated', { | 
					 | 
				
			||||
      user: sshUser, | 
					 | 
				
			||||
      cmd: args.join(' '), | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
    if (sshUser) { | 
					 | 
				
			||||
      spawn(socket, args); | 
					 | 
				
			||||
    } else { | 
					 | 
				
			||||
      try { | 
					 | 
				
			||||
        const username = await login(socket); | 
					 | 
				
			||||
        args[1] = `${username.trim()}@${args[1]}`; | 
					 | 
				
			||||
        logger.debug('Spawning term', { | 
					 | 
				
			||||
          username: username.trim(), | 
					 | 
				
			||||
          cmd: args.join(' ').trim(), | 
					 | 
				
			||||
        }); | 
					 | 
				
			||||
        spawn(socket, args); | 
					 | 
				
			||||
      } catch (error) { | 
					 | 
				
			||||
        logger.info('Disconnect signal sent', { err: error }); | 
					 | 
				
			||||
      } | 
					 | 
				
			||||
    } | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
  return io; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,50 +0,0 @@ | 
				
			|||||
import url from 'url'; | 
					 | 
				
			||||
import type { Socket } from 'socket.io'; | 
					 | 
				
			||||
import type { SSH } from '../shared/interfaces'; | 
					 | 
				
			||||
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 && | 
					 | 
				
			||||
  (host === 'localhost' || host === '0.0.0.0' || host === '127.0.0.1'); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const urlArgs = ( | 
					 | 
				
			||||
  referer: string, | 
					 | 
				
			||||
  def: { [s: string]: string }, | 
					 | 
				
			||||
): { [s: string]: string } => | 
					 | 
				
			||||
  Object.assign(def, url.parse(referer, true).query); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export function getCommand( | 
					 | 
				
			||||
  { | 
					 | 
				
			||||
    request: { headers }, | 
					 | 
				
			||||
    client: { | 
					 | 
				
			||||
      conn: { remoteAddress }, | 
					 | 
				
			||||
    }, | 
					 | 
				
			||||
  }: Socket, | 
					 | 
				
			||||
  { user, host, port, auth, pass, key, knownHosts, config }: SSH, | 
					 | 
				
			||||
  command: string, | 
					 | 
				
			||||
  forcessh: boolean, | 
					 | 
				
			||||
): [string[], boolean] { | 
					 | 
				
			||||
  const sshAddress = address(headers, user, host); | 
					 | 
				
			||||
  const localLogin = !forcessh && localhost(host); | 
					 | 
				
			||||
  return localLogin | 
					 | 
				
			||||
    ? [loginOptions(command, remoteAddress), localLogin] | 
					 | 
				
			||||
    : [ | 
					 | 
				
			||||
        sshOptions( | 
					 | 
				
			||||
          { | 
					 | 
				
			||||
            ...urlArgs(headers.referer, { | 
					 | 
				
			||||
              port: `${port}`, | 
					 | 
				
			||||
              pass: pass || '', | 
					 | 
				
			||||
              command, | 
					 | 
				
			||||
              auth, | 
					 | 
				
			||||
              knownHosts, | 
					 | 
				
			||||
              config: config || '', | 
					 | 
				
			||||
            }), | 
					 | 
				
			||||
            host: sshAddress, | 
					 | 
				
			||||
          }, | 
					 | 
				
			||||
          key, | 
					 | 
				
			||||
        ), | 
					 | 
				
			||||
        user !== '' || user.includes('@') || sshAddress.includes('@'), | 
					 | 
				
			||||
      ]; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,14 +0,0 @@ | 
				
			|||||
export function address( | 
					 | 
				
			||||
  headers: Record<string, string>, | 
					 | 
				
			||||
  user: string, | 
					 | 
				
			||||
  host: string, | 
					 | 
				
			||||
): string { | 
					 | 
				
			||||
  // Check request-header for username
 | 
					 | 
				
			||||
  const remoteUser = headers['remote-user']; | 
					 | 
				
			||||
  if (remoteUser) { | 
					 | 
				
			||||
    return `${remoteUser}@${host}`; | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
  const match = headers.referer.match('.+/ssh/([^/]+)$'); | 
					 | 
				
			||||
  const fallback = user ? `${user}@${host}` : host; | 
					 | 
				
			||||
  return match ? `${match[1].split('?')[0]}@${host}` : fallback; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,12 +0,0 @@ | 
				
			|||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const getRemoteAddress = (remoteAddress: string): string => | 
					 | 
				
			||||
  isUndefined(remoteAddress.split(':')[3]) | 
					 | 
				
			||||
    ? 'localhost' | 
					 | 
				
			||||
    : remoteAddress.split(':')[3]; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export function loginOptions(command: string, remoteAddress: string): string[] { | 
					 | 
				
			||||
  return command === 'login' | 
					 | 
				
			||||
    ? [command, '-h', getRemoteAddress(remoteAddress)] | 
					 | 
				
			||||
    : [command]; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,52 +0,0 @@ | 
				
			|||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
import { logger } from '../../shared/logger.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export function sshOptions( | 
					 | 
				
			||||
  { | 
					 | 
				
			||||
    pass, | 
					 | 
				
			||||
    path, | 
					 | 
				
			||||
    command, | 
					 | 
				
			||||
    host, | 
					 | 
				
			||||
    port, | 
					 | 
				
			||||
    auth, | 
					 | 
				
			||||
    knownHosts, | 
					 | 
				
			||||
    config, | 
					 | 
				
			||||
  }: Record<string, string>, | 
					 | 
				
			||||
  key?: string, | 
					 | 
				
			||||
): string[] { | 
					 | 
				
			||||
  const cmd = parseCommand(command, path); | 
					 | 
				
			||||
  const hostChecking = knownHosts !== '/dev/null' ? 'yes' : 'no'; | 
					 | 
				
			||||
  logger.info(`Authentication Type: ${auth}`); | 
					 | 
				
			||||
  let sshRemoteOptsBase = ['ssh', host, '-t']; | 
					 | 
				
			||||
  if (config !== '') { | 
					 | 
				
			||||
    sshRemoteOptsBase = sshRemoteOptsBase.concat(['-F', config]); | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
  sshRemoteOptsBase = sshRemoteOptsBase.concat([ | 
					 | 
				
			||||
    '-p', | 
					 | 
				
			||||
    port, | 
					 | 
				
			||||
    '-o', | 
					 | 
				
			||||
    `PreferredAuthentications=${auth}`, | 
					 | 
				
			||||
    '-o', | 
					 | 
				
			||||
    `UserKnownHostsFile=${knownHosts}`, | 
					 | 
				
			||||
    '-o', | 
					 | 
				
			||||
    `StrictHostKeyChecking=${hostChecking}`, | 
					 | 
				
			||||
  ]); | 
					 | 
				
			||||
  if (!isUndefined(key)) { | 
					 | 
				
			||||
    return sshRemoteOptsBase.concat(['-i', key, cmd]); | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
  if (pass !== '') { | 
					 | 
				
			||||
    return ['sshpass', '-p', pass].concat(sshRemoteOptsBase, [cmd]); | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
  if (auth === 'none') { | 
					 | 
				
			||||
    sshRemoteOptsBase.splice(sshRemoteOptsBase.indexOf('-o'), 2); | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
 | 
					 | 
				
			||||
  return cmd === '' ? sshRemoteOptsBase : sshRemoteOptsBase.concat([cmd]); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
function parseCommand(command: string, path?: string): string { | 
					 | 
				
			||||
  if (command === 'login' && isUndefined(path)) return ''; | 
					 | 
				
			||||
  return !isUndefined(path) | 
					 | 
				
			||||
    ? `$SHELL -c "cd ${path};${command === 'login' ? '$SHELL' : command}"` | 
					 | 
				
			||||
    : command; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,35 +0,0 @@ | 
				
			|||||
import type SocketIO from 'socket.io'; | 
					 | 
				
			||||
import pty from 'node-pty'; | 
					 | 
				
			||||
import { dirname, resolve as resolvePath } from 'path'; | 
					 | 
				
			||||
import { fileURLToPath } from 'url'; | 
					 | 
				
			||||
import { xterm } from './shared/xterm.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const executable = resolvePath( | 
					 | 
				
			||||
  dirname(fileURLToPath(import.meta.url)), | 
					 | 
				
			||||
  '..', | 
					 | 
				
			||||
  'buffer.js', | 
					 | 
				
			||||
); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export function login(socket: SocketIO.Socket): Promise<string> { | 
					 | 
				
			||||
  // Request carries no username information
 | 
					 | 
				
			||||
  // Create terminal and ask user for username
 | 
					 | 
				
			||||
  const term = pty.spawn('/usr/bin/env', ['node', executable], xterm); | 
					 | 
				
			||||
  let buf = ''; | 
					 | 
				
			||||
  return new Promise((resolve, reject) => { | 
					 | 
				
			||||
    term.on('exit', () => { | 
					 | 
				
			||||
      resolve(buf); | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
    term.on('data', (data: string) => { | 
					 | 
				
			||||
      socket.emit('data', data); | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
    socket | 
					 | 
				
			||||
      .on('input', (input: string) => { | 
					 | 
				
			||||
        term.write(input); | 
					 | 
				
			||||
        buf = /\177/.exec(input) ? buf.slice(0, -1) : buf + input; | 
					 | 
				
			||||
      }) | 
					 | 
				
			||||
      .on('disconnect', () => { | 
					 | 
				
			||||
        term.kill(); | 
					 | 
				
			||||
        reject(); | 
					 | 
				
			||||
      }); | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,15 +0,0 @@ | 
				
			|||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
import type { IPtyForkOptions } from 'node-pty'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const xterm: IPtyForkOptions = { | 
					 | 
				
			||||
  name: 'xterm-256color', | 
					 | 
				
			||||
  cols: 80, | 
					 | 
				
			||||
  rows: 30, | 
					 | 
				
			||||
  cwd: process.cwd(), | 
					 | 
				
			||||
  env: Object.assign( | 
					 | 
				
			||||
    {}, | 
					 | 
				
			||||
    ...Object.keys(process.env) | 
					 | 
				
			||||
      .filter((key: string) => !isUndefined(process.env[key])) | 
					 | 
				
			||||
      .map((key: string) => ({ [key]: process.env[key] })), | 
					 | 
				
			||||
  ), | 
					 | 
				
			||||
}; | 
					 | 
				
			||||
@ -1,44 +0,0 @@ | 
				
			|||||
import type SocketIO from 'socket.io'; | 
					 | 
				
			||||
import express from 'express'; | 
					 | 
				
			||||
import compression from 'compression'; | 
					 | 
				
			||||
import winston from 'express-winston'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
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 { policies } from './socketServer/security.js'; | 
					 | 
				
			||||
import { loadSSL } from './socketServer/ssl.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export async function server( | 
					 | 
				
			||||
  { base, port, host, title, allowIframe }: Server, | 
					 | 
				
			||||
  ssl?: SSL, | 
					 | 
				
			||||
): Promise<SocketIO.Server> { | 
					 | 
				
			||||
  const basePath = trim(base); | 
					 | 
				
			||||
  logger.info('Starting server', { | 
					 | 
				
			||||
    ssl, | 
					 | 
				
			||||
    port, | 
					 | 
				
			||||
    base, | 
					 | 
				
			||||
    title, | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
  const app = express(); | 
					 | 
				
			||||
  const client = html(basePath, title); | 
					 | 
				
			||||
  app | 
					 | 
				
			||||
    .use(`${basePath}/web_modules`, serveStatic('web_modules')) | 
					 | 
				
			||||
    .use(`${basePath}/assets`, serveStatic('assets')) | 
					 | 
				
			||||
    .use(`${basePath}/client`, serveStatic('client')) | 
					 | 
				
			||||
    .use(winston.logger(logger)) | 
					 | 
				
			||||
    .use(compression()) | 
					 | 
				
			||||
    .use(favicon(basePath)) | 
					 | 
				
			||||
    .use(redirect) | 
					 | 
				
			||||
    .use(policies(allowIframe)) | 
					 | 
				
			||||
    .get(basePath, client) | 
					 | 
				
			||||
    .get(`${basePath}/ssh/:user`, client); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
  const sslBuffer: SSLBuffer = await loadSSL(ssl); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
  return listen(app, host, port, basePath, sslBuffer); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,5 +0,0 @@ | 
				
			|||||
import express from 'express'; | 
					 | 
				
			||||
import { assetsPath } from './shared/path.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const trim = (str: string): string => str.replace(/\/*$/, ''); | 
					 | 
				
			||||
export const serveStatic = (path: string) => express.static(assetsPath(path)); | 
					 | 
				
			||||
@ -1,55 +0,0 @@ | 
				
			|||||
import type { Request, Response, RequestHandler } from 'express'; | 
					 | 
				
			||||
import { isDev } from '../../shared/env.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const jsFiles = isDev ? ['dev', 'wetty'] : ['wetty']; | 
					 | 
				
			||||
const cssFiles = ['styles', 'options', 'overlay', 'terminal']; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const render = ( | 
					 | 
				
			||||
  title: string, | 
					 | 
				
			||||
  favicon: string, | 
					 | 
				
			||||
  css: string[], | 
					 | 
				
			||||
  js: string[], | 
					 | 
				
			||||
): string => `<!doctype html>
 | 
					 | 
				
			||||
<html lang="en"> | 
					 | 
				
			||||
  <head> | 
					 | 
				
			||||
    <meta charset="utf8"> | 
					 | 
				
			||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge"> | 
					 | 
				
			||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | 
					 | 
				
			||||
    <link rel="icon" type="image/x-icon" href="${favicon}"> | 
					 | 
				
			||||
    <title>${title}</title> | 
					 | 
				
			||||
    ${css.map(file => `<link rel="stylesheet" href="${file}" />`).join('\n')} | 
					 | 
				
			||||
  </head> | 
					 | 
				
			||||
  <body> | 
					 | 
				
			||||
    <div id="overlay"> | 
					 | 
				
			||||
      <div class="error"> | 
					 | 
				
			||||
        <div id="msg"></div> | 
					 | 
				
			||||
        <input type="button" onclick="location.reload();" value="reconnect" /> | 
					 | 
				
			||||
      </div> | 
					 | 
				
			||||
    </div> | 
					 | 
				
			||||
    <div id="options"> | 
					 | 
				
			||||
      <a class="toggler" | 
					 | 
				
			||||
         href="#" | 
					 | 
				
			||||
         alt="Toggle options" | 
					 | 
				
			||||
       ><i class="fas fa-cogs"></i></a> | 
					 | 
				
			||||
      <textarea class="editor"></textarea> | 
					 | 
				
			||||
    </div> | 
					 | 
				
			||||
    <div id="terminal"></div> | 
					 | 
				
			||||
    ${js | 
					 | 
				
			||||
      .map(file => `<script type="module" src="${file}"></script>`) | 
					 | 
				
			||||
      .join('\n')} | 
					 | 
				
			||||
  </body> | 
					 | 
				
			||||
</html>`;
 | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const html = (base: string, title: string): RequestHandler => ( | 
					 | 
				
			||||
  _req: Request, | 
					 | 
				
			||||
  res: Response, | 
					 | 
				
			||||
): void => { | 
					 | 
				
			||||
  res.send( | 
					 | 
				
			||||
    render( | 
					 | 
				
			||||
      title, | 
					 | 
				
			||||
      `${base}/favicon.ico`, | 
					 | 
				
			||||
      cssFiles.map(css => `${base}/assets/css/${css}.css`), | 
					 | 
				
			||||
      jsFiles.map(js => `${base}/client/${js}.js`), | 
					 | 
				
			||||
    ), | 
					 | 
				
			||||
  ); | 
					 | 
				
			||||
}; | 
					 | 
				
			||||
@ -1,99 +0,0 @@ | 
				
			|||||
import type { Request, Response, NextFunction, RequestHandler } from 'express'; | 
					 | 
				
			||||
import etag from 'etag'; | 
					 | 
				
			||||
import fresh from 'fresh'; | 
					 | 
				
			||||
import parseUrl from 'parseurl'; | 
					 | 
				
			||||
import fs from 'fs'; | 
					 | 
				
			||||
import { assetsPath } from './shared/path.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const ONE_YEAR_MS = 60 * 60 * 24 * 365 * 1000; // 1 year
 | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Determine if the cached representation is fresh. | 
					 | 
				
			||||
 * @param req - server request | 
					 | 
				
			||||
 * @param res - server response | 
					 | 
				
			||||
 * @returns if the cache is fresh or not | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
const isFresh = (req: Request, res: Response): boolean => | 
					 | 
				
			||||
  fresh(req.headers, { | 
					 | 
				
			||||
    etag: res.getHeader('ETag'), | 
					 | 
				
			||||
    'last-modified': res.getHeader('Last-Modified'), | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * redirect requests with trailing / to remove it | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * @param req - server request | 
					 | 
				
			||||
 * @param res - server response | 
					 | 
				
			||||
 * @param next - next middleware to call on finish | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
export function redirect( | 
					 | 
				
			||||
  req: Request, | 
					 | 
				
			||||
  res: Response, | 
					 | 
				
			||||
  next: NextFunction, | 
					 | 
				
			||||
): void { | 
					 | 
				
			||||
  if (req.path.substr(-1) === '/' && req.path.length > 1) | 
					 | 
				
			||||
    res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length)); | 
					 | 
				
			||||
  else next(); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Serves the favicon located by the given `path`. | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * @param basePath - server base path | 
					 | 
				
			||||
 * @returns middleware | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
export function favicon(basePath: string): RequestHandler { | 
					 | 
				
			||||
  const path = assetsPath('assets', 'favicon.ico'); | 
					 | 
				
			||||
  return (req: Request, res: Response, next: NextFunction): void => { | 
					 | 
				
			||||
    if (getPathName(req) !== `${basePath}/favicon.ico`) { | 
					 | 
				
			||||
      next(); | 
					 | 
				
			||||
      return; | 
					 | 
				
			||||
    } | 
					 | 
				
			||||
 | 
					 | 
				
			||||
    if (req.method !== 'GET' && req.method !== 'HEAD') { | 
					 | 
				
			||||
      res.statusCode = req.method === 'OPTIONS' ? 200 : 405; | 
					 | 
				
			||||
      res.setHeader('Allow', 'GET, HEAD, OPTIONS'); | 
					 | 
				
			||||
      res.setHeader('Content-Length', '0'); | 
					 | 
				
			||||
      res.end(); | 
					 | 
				
			||||
      return; | 
					 | 
				
			||||
    } | 
					 | 
				
			||||
 | 
					 | 
				
			||||
    fs.readFile(path, (err: Error | null, buf: Buffer) => { | 
					 | 
				
			||||
      if (err) return next(err); | 
					 | 
				
			||||
      Object.entries({ | 
					 | 
				
			||||
        'Cache-Control': `public, max-age=${Math.floor(ONE_YEAR_MS / 1000)}`, | 
					 | 
				
			||||
        ETag: etag(buf), | 
					 | 
				
			||||
      }).forEach(([key, value]) => { | 
					 | 
				
			||||
        res.setHeader(key, value); | 
					 | 
				
			||||
      }); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
      // Validate freshness
 | 
					 | 
				
			||||
      if (isFresh(req, res)) { | 
					 | 
				
			||||
        res.statusCode = 304; | 
					 | 
				
			||||
        return res.end(); | 
					 | 
				
			||||
      } | 
					 | 
				
			||||
 | 
					 | 
				
			||||
      // Send icon
 | 
					 | 
				
			||||
      res.statusCode = 200; | 
					 | 
				
			||||
      res.setHeader('Content-Length', buf.length); | 
					 | 
				
			||||
      res.setHeader('Content-Type', 'image/x-icon'); | 
					 | 
				
			||||
      return res.end(buf); | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
  }; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Get the request pathname. | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * @param requests | 
					 | 
				
			||||
 * @returns path name or undefined | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
 | 
					 | 
				
			||||
function getPathName(req: Request): string | undefined { | 
					 | 
				
			||||
  try { | 
					 | 
				
			||||
    const url = parseUrl(req); | 
					 | 
				
			||||
    return url?.pathname ? url.pathname : undefined; | 
					 | 
				
			||||
  } catch (e) { | 
					 | 
				
			||||
    return undefined; | 
					 | 
				
			||||
  } | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,25 +0,0 @@ | 
				
			|||||
import helmet from 'helmet'; | 
					 | 
				
			||||
import type { Request, Response } from 'express'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const policies = (allowIframe: boolean) => ( | 
					 | 
				
			||||
  req: Request, | 
					 | 
				
			||||
  res: Response, | 
					 | 
				
			||||
  next: (err?: unknown) => void, | 
					 | 
				
			||||
) => { | 
					 | 
				
			||||
  helmet({ | 
					 | 
				
			||||
    frameguard: allowIframe ? false : { action: 'sameorigin' }, | 
					 | 
				
			||||
    referrerPolicy: { policy: ['no-referrer-when-downgrade'] }, | 
					 | 
				
			||||
    contentSecurityPolicy: { | 
					 | 
				
			||||
      directives: { | 
					 | 
				
			||||
        defaultSrc: ["'self'"], | 
					 | 
				
			||||
        scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], | 
					 | 
				
			||||
        styleSrc: ["'self'", "'unsafe-inline'"], | 
					 | 
				
			||||
        fontSrc: ["'self'", 'data:'], | 
					 | 
				
			||||
        connectSrc: [ | 
					 | 
				
			||||
          "'self'", | 
					 | 
				
			||||
          (req.protocol === 'http' ? 'ws://' : 'wss://') + req.get('host'), | 
					 | 
				
			||||
        ], | 
					 | 
				
			||||
      }, | 
					 | 
				
			||||
    }, | 
					 | 
				
			||||
  })(req, res, next); | 
					 | 
				
			||||
}; | 
					 | 
				
			||||
@ -1,12 +0,0 @@ | 
				
			|||||
import findUp from 'find-up'; | 
					 | 
				
			||||
import { resolve, dirname } from 'path'; | 
					 | 
				
			||||
import { fileURLToPath } from 'url'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const filePath = dirname( | 
					 | 
				
			||||
  findUp.sync('package.json', { | 
					 | 
				
			||||
    cwd: dirname(fileURLToPath(import.meta.url)), | 
					 | 
				
			||||
  }) || process.cwd(), | 
					 | 
				
			||||
); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const assetsPath = (...args: string[]) => | 
					 | 
				
			||||
  resolve(filePath, 'build', ...args); | 
					 | 
				
			||||
@ -1,36 +0,0 @@ | 
				
			|||||
import type express from 'express'; | 
					 | 
				
			||||
import socket from 'socket.io'; | 
					 | 
				
			||||
import http from 'http'; | 
					 | 
				
			||||
import https from 'https'; | 
					 | 
				
			||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
import { logger } from '../../shared/logger.js'; | 
					 | 
				
			||||
import type { SSLBuffer } from '../../shared/interfaces.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const listen = ( | 
					 | 
				
			||||
  app: express.Express, | 
					 | 
				
			||||
  host: string, | 
					 | 
				
			||||
  port: number, | 
					 | 
				
			||||
  path: string, | 
					 | 
				
			||||
  { key, cert }: SSLBuffer, | 
					 | 
				
			||||
): SocketIO.Server => | 
					 | 
				
			||||
  socket( | 
					 | 
				
			||||
    !isUndefined(key) && !isUndefined(cert) | 
					 | 
				
			||||
      ? https.createServer({ key, cert }, app).listen(port, host, () => { | 
					 | 
				
			||||
          logger.info('Server started', { | 
					 | 
				
			||||
            port, | 
					 | 
				
			||||
            connection: 'https', | 
					 | 
				
			||||
          }); | 
					 | 
				
			||||
        }) | 
					 | 
				
			||||
      : http.createServer(app).listen(port, host, () => { | 
					 | 
				
			||||
          logger.info('Server started', { | 
					 | 
				
			||||
            port, | 
					 | 
				
			||||
            connection: 'http', | 
					 | 
				
			||||
          }); | 
					 | 
				
			||||
        }), | 
					 | 
				
			||||
    { | 
					 | 
				
			||||
      path: `${path}/socket.io`, | 
					 | 
				
			||||
      pingInterval: 3000, | 
					 | 
				
			||||
      pingTimeout: 7000, | 
					 | 
				
			||||
    }, | 
					 | 
				
			||||
  ); | 
					 | 
				
			||||
@ -1,14 +0,0 @@ | 
				
			|||||
import fs from 'fs-extra'; | 
					 | 
				
			||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
import { resolve } from 'path'; | 
					 | 
				
			||||
import type { SSL, SSLBuffer } from '../../shared/interfaces'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export async function loadSSL(ssl?: SSL): Promise<SSLBuffer> { | 
					 | 
				
			||||
  if (isUndefined(ssl) || isUndefined(ssl.key) || isUndefined(ssl.cert)) | 
					 | 
				
			||||
    return {}; | 
					 | 
				
			||||
  const [key, cert]: Buffer[] = await Promise.all([ | 
					 | 
				
			||||
    fs.readFile(resolve(ssl.key)), | 
					 | 
				
			||||
    fs.readFile(resolve(ssl.cert)), | 
					 | 
				
			||||
  ]); | 
					 | 
				
			||||
  return { key, cert }; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,38 +0,0 @@ | 
				
			|||||
import type SocketIO from 'socket.io'; | 
					 | 
				
			||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
import pty from 'node-pty'; | 
					 | 
				
			||||
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); | 
					 | 
				
			||||
  const { pid } = term; | 
					 | 
				
			||||
  const address = args[0] === 'ssh' ? args[1] : 'localhost'; | 
					 | 
				
			||||
  logger.info('Process Started on behalf of user', { | 
					 | 
				
			||||
    pid, | 
					 | 
				
			||||
    address, | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
  socket.emit('login'); | 
					 | 
				
			||||
  term.on('exit', (code: number) => { | 
					 | 
				
			||||
    logger.info('Process exited', { code, pid }); | 
					 | 
				
			||||
    socket.emit('logout'); | 
					 | 
				
			||||
    socket | 
					 | 
				
			||||
      .removeAllListeners('disconnect') | 
					 | 
				
			||||
      .removeAllListeners('resize') | 
					 | 
				
			||||
      .removeAllListeners('input'); | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
  term.on('data', (data: string) => { | 
					 | 
				
			||||
    socket.emit('data', data); | 
					 | 
				
			||||
  }); | 
					 | 
				
			||||
  socket | 
					 | 
				
			||||
    .on('resize', ({ cols, rows }) => { | 
					 | 
				
			||||
      term.resize(cols, rows); | 
					 | 
				
			||||
    }) | 
					 | 
				
			||||
    .on('input', input => { | 
					 | 
				
			||||
      if (!isUndefined(term)) term.write(input); | 
					 | 
				
			||||
    }) | 
					 | 
				
			||||
    .on('disconnect', () => { | 
					 | 
				
			||||
      term.kill(); | 
					 | 
				
			||||
      logger.info('Process exited', { code: 0, pid }); | 
					 | 
				
			||||
    }); | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,134 +0,0 @@ | 
				
			|||||
import fs from 'fs-extra'; | 
					 | 
				
			||||
import path from 'path'; | 
					 | 
				
			||||
import JSON5 from 'json5'; | 
					 | 
				
			||||
import isUndefined from 'lodash/isUndefined.js'; | 
					 | 
				
			||||
import type { Arguments } from 'yargs'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
import type { Config, SSH, Server, SSL } from './interfaces'; | 
					 | 
				
			||||
import { | 
					 | 
				
			||||
  sshDefault, | 
					 | 
				
			||||
  serverDefault, | 
					 | 
				
			||||
  forceSSHDefault, | 
					 | 
				
			||||
  defaultCommand, | 
					 | 
				
			||||
} from './defaults.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
type confValue = | 
					 | 
				
			||||
  | boolean | 
					 | 
				
			||||
  | string | 
					 | 
				
			||||
  | number | 
					 | 
				
			||||
  | undefined | 
					 | 
				
			||||
  | unknown | 
					 | 
				
			||||
  | SSH | 
					 | 
				
			||||
  | Server | 
					 | 
				
			||||
  | SSL; | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Cast given value to boolean | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * @param value - variable to cast | 
					 | 
				
			||||
 * @returns variable cast to boolean | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
function ensureBoolean(value: confValue): 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: SSH | Server, | 
					 | 
				
			||||
  source: Record<string, confValue>, | 
					 | 
				
			||||
): SSH | Server => | 
					 | 
				
			||||
  Object.fromEntries( | 
					 | 
				
			||||
    Object.entries(source).map(([key, value]) => [ | 
					 | 
				
			||||
      key, | 
					 | 
				
			||||
      isUndefined(source[key]) ? target[key] : value, | 
					 | 
				
			||||
    ]), | 
					 | 
				
			||||
  ) as SSH | Server; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
/** | 
					 | 
				
			||||
 * Merge cli arguemens with config object | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 * @param opts - Object containing cli args | 
					 | 
				
			||||
 * @param config - Config object | 
					 | 
				
			||||
 * @returns merged configuration | 
					 | 
				
			||||
 * | 
					 | 
				
			||||
 */ | 
					 | 
				
			||||
export function mergeCliConf(opts: Arguments, config: Config): Config { | 
					 | 
				
			||||
  const ssl = { | 
					 | 
				
			||||
    key: opts['ssl-key'], | 
					 | 
				
			||||
    cert: opts['ssl-cert'], | 
					 | 
				
			||||
    ...config.ssl, | 
					 | 
				
			||||
  } as 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'], | 
					 | 
				
			||||
      config: opts['ssh-config'], | 
					 | 
				
			||||
      knownHosts: opts['known-hosts'], | 
					 | 
				
			||||
    }) as SSH, | 
					 | 
				
			||||
    server: objectAssign(config.server, { | 
					 | 
				
			||||
      base: opts.base, | 
					 | 
				
			||||
      host: opts.host, | 
					 | 
				
			||||
      port: opts.port, | 
					 | 
				
			||||
      title: opts.title, | 
					 | 
				
			||||
      allowIframe: opts['allow-iframe'], | 
					 | 
				
			||||
    }) as Server, | 
					 | 
				
			||||
    command: isUndefined(opts.command) ? config.command : `${opts.command}`, | 
					 | 
				
			||||
    forceSSH: isUndefined(opts['force-ssh']) | 
					 | 
				
			||||
      ? config.forceSSH | 
					 | 
				
			||||
      : ensureBoolean(opts['force-ssh']), | 
					 | 
				
			||||
    ssl: isUndefined(ssl.key) || isUndefined(ssl.cert) ? undefined : ssl, | 
					 | 
				
			||||
  }; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,23 +0,0 @@ | 
				
			|||||
import type { SSH, Server } from './interfaces'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const sshDefault: SSH = { | 
					 | 
				
			||||
  user: process.env.SSHUSER || '', | 
					 | 
				
			||||
  host: process.env.SSHHOST || 'localhost', | 
					 | 
				
			||||
  auth: process.env.SSHAUTH || 'password', | 
					 | 
				
			||||
  pass: process.env.SSHPASS || undefined, | 
					 | 
				
			||||
  key: process.env.SSHKEY || undefined, | 
					 | 
				
			||||
  port: parseInt(process.env.SSHPORT || '22', 10), | 
					 | 
				
			||||
  knownHosts: process.env.KNOWNHOSTS || '/dev/null', | 
					 | 
				
			||||
  config: process.env.SSHCONFIG || undefined, | 
					 | 
				
			||||
}; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const serverDefault: Server = { | 
					 | 
				
			||||
  base: process.env.BASE || '/wetty/', | 
					 | 
				
			||||
  port: parseInt(process.env.PORT || '3000', 10), | 
					 | 
				
			||||
  host: '0.0.0.0', | 
					 | 
				
			||||
  title: process.env.TITLE || 'WeTTy - The Web Terminal Emulator', | 
					 | 
				
			||||
  allowIframe: false, | 
					 | 
				
			||||
}; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const forceSSHDefault = process.env.FORCESSH === 'true' || false; | 
					 | 
				
			||||
export const defaultCommand = process.env.COMMAND || 'login'; | 
					 | 
				
			||||
@ -1 +0,0 @@ | 
				
			|||||
export const isDev = process.env.NODE_ENV === 'development'; | 
					 | 
				
			||||
@ -1,38 +0,0 @@ | 
				
			|||||
export interface SSH { | 
					 | 
				
			||||
  [s: string]: string | number | boolean | undefined; | 
					 | 
				
			||||
  user: string; | 
					 | 
				
			||||
  host: string; | 
					 | 
				
			||||
  auth: string; | 
					 | 
				
			||||
  port: number; | 
					 | 
				
			||||
  knownHosts: string; | 
					 | 
				
			||||
  pass?: string; | 
					 | 
				
			||||
  key?: string; | 
					 | 
				
			||||
  config?: string; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export interface SSL { | 
					 | 
				
			||||
  key: string; | 
					 | 
				
			||||
  cert: string; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export interface SSLBuffer { | 
					 | 
				
			||||
  key?: Buffer; | 
					 | 
				
			||||
  cert?: Buffer; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export interface Server { | 
					 | 
				
			||||
  [s: string]: string | number | boolean; | 
					 | 
				
			||||
  port: number; | 
					 | 
				
			||||
  host: string; | 
					 | 
				
			||||
  title: string; | 
					 | 
				
			||||
  base: string; | 
					 | 
				
			||||
  allowIframe: boolean; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export interface Config { | 
					 | 
				
			||||
  ssh: SSH; | 
					 | 
				
			||||
  server: Server; | 
					 | 
				
			||||
  forceSSH: boolean; | 
					 | 
				
			||||
  command: string; | 
					 | 
				
			||||
  ssl?: SSL; | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,24 +0,0 @@ | 
				
			|||||
import winston from 'winston'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
import { isDev } from './env.js'; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const { combine, timestamp, label, simple, json, colorize } = winston.format; | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const dev = combine( | 
					 | 
				
			||||
  colorize(), | 
					 | 
				
			||||
  label({ label: 'Wetty' }), | 
					 | 
				
			||||
  timestamp(), | 
					 | 
				
			||||
  simple(), | 
					 | 
				
			||||
); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
const prod = combine(label({ label: 'Wetty' }), timestamp(), json()); | 
					 | 
				
			||||
 | 
					 | 
				
			||||
export const logger = winston.createLogger({ | 
					 | 
				
			||||
  format: isDev ? dev : prod, | 
					 | 
				
			||||
  transports: [ | 
					 | 
				
			||||
    new winston.transports.Console({ | 
					 | 
				
			||||
      level: isDev ? 'debug' : 'info', | 
					 | 
				
			||||
      handleExceptions: true, | 
					 | 
				
			||||
    }), | 
					 | 
				
			||||
  ], | 
					 | 
				
			||||
}); | 
					 | 
				
			||||
@ -1,6 +0,0 @@ | 
				
			|||||
{ | 
					 | 
				
			||||
  "extends": "./tsconfig.json", | 
					 | 
				
			||||
  "include": [ | 
					 | 
				
			||||
    "src/client" | 
					 | 
				
			||||
  ] | 
					 | 
				
			||||
} | 
					 | 
				
			||||
@ -1,14 +0,0 @@ | 
				
			|||||
{ | 
					 | 
				
			||||
  "extends": "./tsconfig.json", | 
					 | 
				
			||||
  "compilerOptions": { | 
					 | 
				
			||||
    "incremental": true, | 
					 | 
				
			||||
    "outDir": "./build", | 
					 | 
				
			||||
    "sourceMap": true | 
					 | 
				
			||||
  }, | 
					 | 
				
			||||
  "include": [ | 
					 | 
				
			||||
    "src" | 
					 | 
				
			||||
  ], | 
					 | 
				
			||||
  "exclude": [ | 
					 | 
				
			||||
    "src/client" | 
					 | 
				
			||||
  ] | 
					 | 
				
			||||
} | 
					 | 
				
			||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					
					Loading…
					
					
				
		Reference in new issue