diff --git a/Cargo.lock b/Cargo.lock index bf9d683..2008450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,12 +165,14 @@ dependencies = [ "anyhow", "bytes", "crossterm", + "home", "log", "open", "procfs", "thiserror", "tokio", "tokio-stream", + "toml", ] [[package]] @@ -188,6 +190,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" +dependencies = [ + "winapi", +] + [[package]] name = "iana-time-zone" version = "0.1.50" @@ -422,6 +433,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" + [[package]] name = "signal-hook" version = "0.3.14" @@ -552,6 +569,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 0fa113b..271de68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,13 @@ edition = "2021" anyhow = "1.0" bytes = "1" crossterm = { version = "0.25", features = ["event-stream"] } +home = "0.5.4" log = { version = "0.4", features = ["std"] } open = "3" thiserror = "1.0" tokio = { version = "1", features = ["full"] } tokio-stream = "0.1" +toml = "0.5" [target.'cfg(target_os="linux")'.dependencies] procfs = "0.14.1" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..3b5a1a4 --- /dev/null +++ b/config.toml @@ -0,0 +1,9 @@ +# This is an example config file +auto = true + +[servers."coder.doty-dev"] +auto = true + +[servers."coder.doty-dev".ports] +10350 = "Tilt UI" +8080 = true diff --git a/src/client/config.rs b/src/client/config.rs new file mode 100644 index 0000000..a6d5a6a --- /dev/null +++ b/src/client/config.rs @@ -0,0 +1,156 @@ +use anyhow::{bail, Result}; +use std::collections::HashMap; +use toml::Value; + +#[derive(Debug, Clone)] +pub struct PortConfig { + pub enabled: bool, + pub description: Option, +} + +#[derive(Debug, Clone)] +pub struct ServerConfig { + auto: bool, + ports: HashMap, +} + +impl ServerConfig { + pub fn get(&self, port: u16) -> PortConfig { + match self.ports.get(&port) { + None => PortConfig { enabled: self.auto, description: None }, + Some(c) => c.clone(), + } + } +} + +#[derive(Debug)] +pub struct Config { + auto: bool, + servers: HashMap, +} + +impl Config { + pub fn get(&self, remote: &str) -> ServerConfig { + match self.servers.get(remote) { + Some(cfg) => cfg.clone(), + None => ServerConfig { auto: self.auto, ports: HashMap::new() }, + } + } +} + +pub fn load_config() -> Result { + use std::io::ErrorKind; + + let mut home = match home::home_dir() { + Some(h) => h, + None => return Ok(default()), + }; + home.push(".fwd"); + + let contents = match std::fs::read_to_string(home) { + Ok(contents) => contents, + Err(e) => match e.kind() { + ErrorKind::NotFound => return Ok(default()), + _ => return Err(e.into()), + }, + }; + + Ok(parse_config(&contents.parse::()?)?) +} + +fn default() -> Config { + Config { auto: true, servers: HashMap::new() } +} + +fn parse_config(value: &Value) -> Result { + match value { + Value::Table(table) => Ok({ + let auto = match table.get("auto") { + None => true, + Some(Value::Boolean(v)) => *v, + Some(v) => bail!("expected a true or false, got {:?}", v), + }; + Config { + auto, + servers: get_servers(&table, auto)?, + } + }), + _ => bail!("top level must be a table"), + } +} + +fn get_servers( + table: &toml::value::Table, + auto: bool, +) -> Result> { + match table.get("servers") { + None => Ok(HashMap::new()), + Some(Value::Table(table)) => Ok({ + let mut servers = HashMap::new(); + for (k, v) in table { + servers.insert(k.clone(), get_server(v, auto)?); + } + servers + }), + v => bail!("expected a table in the servers key, got {:?}", v), + } +} + +fn get_server(value: &Value, auto: bool) -> Result { + match value { + Value::Table(table) => Ok(ServerConfig { + auto: match table.get("auto") { + None => auto, // Default to global default + Some(Value::Boolean(v)) => *v, + Some(v) => bail!("expected true or false, got {:?}", v), + }, + ports: get_ports(table)?, + }), + value => bail!("expected a table, got {:?}", value), + } +} + +fn get_ports(table: &toml::value::Table) -> Result> { + match table.get("ports") { + None => Ok(HashMap::new()), + Some(Value::Table(table)) => Ok({ + let mut ports = HashMap::new(); + for (k,v) in table { + let port:u16 = k.parse()?; + let config = match v { + Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None}, + Value::Table(table) => PortConfig{ + enabled: match table.get("enabled") { + Some(Value::Boolean(enabled)) => *enabled, + _ => bail!("not implemented"), + }, + description: match table.get("description") { + Some(Value::String(desc)) => Some(desc.clone()), + Some(v) => bail!("expect a string description, got {:?}", v), + None => None, + }, + }, + _ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v), + }; + ports.insert(port, config); + } + ports + }), + Some(Value::Array(array)) => Ok({ + let mut ports = HashMap::new(); + for v in array { + ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None}); + } + ports + }), + Some(v) => bail!("ports must be either a table of ' = ...' or an array of ports, got {:?}", v), + } +} + +fn get_port_number(v: &Value) -> Result { + let port: u16 = match v { + Value::Integer(i) => (*i).try_into()?, + v => bail!("port must be a small number, got {:?}", v), + }; + Ok(port) +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 9a54b55..908c9c3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -12,6 +12,7 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::process; use tokio::sync::mpsc; +mod config; mod ui; /// Wait for the server to be ready; we know the server is there and @@ -374,7 +375,15 @@ pub async fn run_client(remote: &str) { _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); - let mut ui = ui::UI::new(event_receiver); + let config = match config::load_config() { + Ok(config) => config.get(remote), + Err(e) => { + eprintln!("Error loading configuration: {:?}", e); + return; + } + }; + + let mut ui = ui::UI::new(event_receiver, config); // Start the reconnect loop. tokio::select! { diff --git a/src/client/ui.rs b/src/client/ui.rs index 5c5d650..a777354 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -1,4 +1,4 @@ -use super::client_listen; +use super::{client_listen, config::ServerConfig}; use crate::message::PortDesc; use anyhow::Result; use crossterm::{ @@ -65,12 +65,16 @@ struct Listener { } impl Listener { - pub fn from_desc(desc: PortDesc) -> Listener { - Listener { - enabled: false, - stop: None, - desc: Some(desc), + pub fn from_desc( + socks_port: Option, + desc: PortDesc, + enabled: bool, + ) -> Listener { + let mut listener = Listener { enabled, stop: None, desc: Some(desc) }; + if enabled { + listener.start(socks_port); } + listener } pub fn enabled(&self) -> bool { @@ -102,6 +106,7 @@ impl Listener { if let (Some(desc), Some(socks_port), None) = (&self.desc, socks_port, &self.stop) { + info!("Starting port {port} to {socks_port}", port = desc.port); let (l, stop) = oneshot::channel(); let port = desc.port; tokio::spawn(async move { @@ -127,16 +132,17 @@ pub struct UI { events: mpsc::Receiver, ports: HashMap, socks_port: Option, + lines: VecDeque, + config: ServerConfig, + selection: usize, running: bool, show_logs: bool, - selection: usize, - lines: VecDeque, alternate_screen: bool, raw_mode: bool, } impl UI { - pub fn new(events: mpsc::Receiver) -> UI { + pub fn new(events: mpsc::Receiver, config: ServerConfig) -> UI { UI { events, ports: HashMap::new(), @@ -145,6 +151,7 @@ impl UI { show_logs: false, selection: 0, lines: VecDeque::with_capacity(1024), + config, alternate_screen: false, raw_mode: false, } @@ -419,9 +426,16 @@ impl UI { { listener.connect(self.socks_port, port_desc); } else { + let config = self.config.get(port_desc.port); + info!("Port config {port_desc:?} -> {config:?}"); + self.ports.insert( port_desc.port, - Listener::from_desc(port_desc), + Listener::from_desc( + self.socks_port, + port_desc, + config.enabled, + ), ); } }