butlerx
4 years ago
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