From e4bdde8aae1295862056829181386ed1c5f9f8e2 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Tue, 27 Jul 2021 13:58:16 -0500 Subject: [PATCH] Update with proper logging; fix bad host parsing --- Cargo.lock | 126 +++++++++++++++++++++++++++----------- Cargo.toml | 5 +- src/data.rs | 9 ++- src/lib.rs | 43 ++++++++++--- src/opt.rs | 54 ++++++++++++---- src/subcommand/execute.rs | 8 +-- src/subcommand/launch.rs | 30 ++++++--- src/subcommand/listen.rs | 49 +++++++++++---- 8 files changed, 239 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d80d4cc..a10ef13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -124,6 +133,7 @@ dependencies = [ "bytes", "derive_more", "directories", + "flexi_logger", "fork", "futures", "hex", @@ -133,14 +143,30 @@ dependencies = [ "serde", "serde_cbor", "serde_json", - "stderrlog", "structopt", + "strum", "tokio", "tokio-stream", "tokio-util", "whoami", ] +[[package]] +name = "flexi_logger" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba2265890613939b533fa11c3728651531419ac549ccf527896201581f23991" +dependencies = [ + "atty", + "chrono", + "glob", + "lazy_static", + "log", + "regex", + "thiserror", + "yansi", +] + [[package]] name = "fork" version = "0.1.18" @@ -255,6 +281,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "half" version = "1.7.1" @@ -533,6 +565,23 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "ryu" version = "1.0.5" @@ -607,19 +656,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" -[[package]] -name = "stderrlog" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38" -dependencies = [ - "atty", - "chrono", - "log", - "termcolor", - "thread_local", -] - [[package]] name = "strsim" version = "0.8.0" @@ -650,6 +686,27 @@ dependencies = [ "syn", ] +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.4.1" @@ -668,30 +725,32 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "textwrap" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "winapi-util", + "unicode-width", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "thiserror" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ - "unicode-width", + "thiserror-impl", ] [[package]] -name = "thread_local" -version = "1.0.1" +name = "thiserror-impl" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ - "lazy_static", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -886,21 +945,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" + [[package]] name = "zeroize" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index b8873c9..fea2f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ codegen-units = 1 bytes = "1.0.1" derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error"] } directories = "3.0.2" -fork = "0.1.18" futures = "0.3.16" hex = "0.4.3" log = "0.4.14" @@ -22,12 +21,14 @@ orion = "0.16.0" serde = { version = "1.0.126", features = ["derive"] } serde_cbor = "0.11.1" serde_json = "1.0.64" +strum = { version = "0.21.0", features = ["derive"] } tokio = { version = "1.9.0", features = ["full"] } tokio-stream = "0.1.7" tokio-util = { version = "0.6.7", features = ["codec"] } # Binary-specific dependencies +flexi_logger = "0.18.0" +fork = "0.1.18" lazy_static = "1.4.0" -stderrlog = "0.5.1" structopt = "0.3.22" whoami = "1.1.2" diff --git a/src/data.rs b/src/data.rs index 484ec5f..4303f46 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,15 +1,17 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use structopt::StructOpt; +use strum::AsRefStr; /// Represents an operation to be performed on the remote machine -#[derive(Clone, Debug, PartialEq, Eq, StructOpt, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, StructOpt, Serialize, Deserialize)] #[serde( rename_all = "snake_case", deny_unknown_fields, tag = "type", content = "payload" )] +#[strum(serialize_all = "snake_case")] pub enum Operation { /// Reads a file from the specified path on the remote machine #[structopt(visible_aliases = &["cat"])] @@ -136,13 +138,14 @@ pub enum Operation { } /// Represents an response to an operation performed on the remote machine -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, Serialize, Deserialize)] #[serde( rename_all = "snake_case", deny_unknown_fields, tag = "status", content = "payload" )] +#[strum(serialize_all = "snake_case")] pub enum Response { /// Represents a successfully-handled operation Ok(ResponsePayload), @@ -155,7 +158,7 @@ pub enum Response { } /// Represents the payload of a successful response -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, Serialize, Deserialize)] #[serde( rename_all = "snake_case", deny_unknown_fields, diff --git a/src/lib.rs b/src/lib.rs index 715e532..1f5a584 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod opt; mod subcommand; mod utils; +use log::error; pub use opt::Opt; use std::path::PathBuf; @@ -21,17 +22,41 @@ lazy_static::lazy_static! { pub fn run() { let opt = Opt::load(); init_logging(&opt.common); - if let Err(x) = opt.subcommand.run() { - eprintln!("{}", x); + if let Err(x) = opt.subcommand.run(opt.common) { + error!("{}", x); } } pub fn init_logging(opt: &opt::CommonOpt) { - stderrlog::new() - .module("distant") - .quiet(opt.quiet) - .verbosity(opt.verbose as usize) - .timestamp(stderrlog::Timestamp::Off) - .init() - .unwrap(); + use flexi_logger::{FileSpec, LevelFilter, LogSpecification, Logger}; + let module = "distant"; + + // Disable logging for everything but our binary, which is based on verbosity + let mut builder = LogSpecification::builder(); + builder.default(LevelFilter::Off).module( + module, + match opt.verbose { + 0 => LevelFilter::Warn, + 1 => LevelFilter::Info, + 2 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }, + ); + + // If quiet, we suppress all output + if opt.quiet { + builder.module(module, LevelFilter::Off); + } + + // Create our logger, but don't initialize yet + let logger = Logger::with(builder.build()); + + // If provided, log to file instead of stderr + let logger = if let Some(path) = opt.log_file.as_ref() { + logger.log_to_file(FileSpec::try_from(path).expect("Failed to create log file spec")) + } else { + logger + }; + + logger.start().expect("Failed to initialize logger"); } diff --git a/src/opt.rs b/src/opt.rs index 2f47f58..7177f9a 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -3,7 +3,7 @@ use derive_more::{Display, Error, From}; use lazy_static::lazy_static; use std::{ env, - net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}, + net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::PathBuf, str::FromStr, }; @@ -41,6 +41,10 @@ pub struct CommonOpt { /// Quiet mode #[structopt(short, long, global = true)] pub quiet: bool, + + /// Log output to disk instead of stderr + #[structopt(long, global = true)] + pub log_file: Option, } #[derive(Debug, StructOpt)] @@ -56,12 +60,12 @@ pub enum Subcommand { impl Subcommand { /// Runs the subcommand, returning the result - pub fn run(self) -> Result<(), Box> { + pub fn run(self, opt: CommonOpt) -> Result<(), Box> { match self { Self::ClearSession => subcommand::clear_session::run()?, - Self::Execute(cmd) => subcommand::execute::run(cmd)?, - Self::Launch(cmd) => subcommand::launch::run(cmd)?, - Self::Listen(cmd) => subcommand::listen::run(cmd)?, + Self::Execute(cmd) => subcommand::execute::run(cmd, opt)?, + Self::Launch(cmd) => subcommand::launch::run(cmd, opt)?, + Self::Listen(cmd) => subcommand::listen::run(cmd, opt)?, } Ok(()) @@ -127,24 +131,28 @@ pub enum BindAddress { #[derive(Clone, Debug, Display, From, Error, PartialEq, Eq)] pub enum ConvertToIpAddrError { - ClientIpParseError(AddrParseError), - MissingClientIp, + AddrParseError(AddrParseError), + #[display(fmt = "SSH_CONNECTION missing 3rd argument (host ip)")] + MissingSshAddr, VarError(env::VarError), } impl BindAddress { - /// Converts address into valid IP - pub fn to_ip_addr(&self) -> Result { + /// Converts address into valid IP; in the case of "any", will leverage the + /// `use_ipv6` flag to determine if binding should use ipv4 or ipv6 + pub fn to_ip_addr(&self, use_ipv6: bool) -> Result { match self { Self::Ssh => { let ssh_connection = env::var("SSH_CONNECTION")?; let ip_str = ssh_connection .split(' ') + .skip(2) .next() - .ok_or(ConvertToIpAddrError::MissingClientIp)?; + .ok_or(ConvertToIpAddrError::MissingSshAddr)?; let ip = ip_str.parse::()?; Ok(ip) } + Self::Any if use_ipv6 => Ok(IpAddr::V6(Ipv6Addr::UNSPECIFIED)), Self::Any => Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), Self::Ip(addr) => Ok(*addr), } @@ -159,7 +167,7 @@ impl FromStr for BindAddress { "ssh" => Ok(Self::Ssh), "any" => Ok(Self::Any), "localhost" => Ok(Self::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST))), - x => x.parse(), + x => Ok(Self::Ip(x.parse::()?)), } } } @@ -193,6 +201,19 @@ pub struct LaunchSubcommand { #[structopt(long, value_name = "ssh|any|IP", default_value = "ssh")] pub bind_server: BindAddress, + /// If specified, will write server logs to a file instead of discarding them + #[structopt(long)] + pub server_log_file: Option, + + /// If specified, will set the server's log level (0 is warning and above, 1 is info, 2 is + /// debug, and 3 or higher is trace) + #[structopt(long, default_value = "0")] + pub server_log_level: u8, + + /// If specified, will bind server to the ipv6 interface if host is "any" instead of ipv4 + #[structopt(short = "6", long)] + pub use_ipv6: bool, + /// Username to use when sshing into remote machine #[structopt(short, long, default_value = &USERNAME)] pub username: String, @@ -211,7 +232,12 @@ pub struct LaunchSubcommand { } /// Represents some range of ports -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Display, PartialEq, Eq)] +#[display( + fmt = "{}{}", + start, + "end.as_ref().map(|end| format!(\"[:{}]\", end)).unwrap_or_default()" +)] pub struct PortRange { pub start: u16, pub end: Option, @@ -293,6 +319,10 @@ pub struct ListenSubcommand { #[structopt(short, long, value_name = "ssh|any|IP", default_value = "localhost")] pub host: BindAddress, + /// If specified, will bind to the ipv6 interface if host is "any" instead of ipv4 + #[structopt(short = "6", long)] + pub use_ipv6: bool, + /// Set the port(s) that the server will attempt to bind to /// /// This can be in the form of PORT1 or PORT1:PORTN to provide a range of ports. diff --git a/src/subcommand/execute.rs b/src/subcommand/execute.rs index 3b36f62..464e0c1 100644 --- a/src/subcommand/execute.rs +++ b/src/subcommand/execute.rs @@ -1,7 +1,7 @@ use crate::{ data::Response, net::{Transport, TransportError}, - opt::ExecuteSubcommand, + opt::{CommonOpt, ExecuteSubcommand}, utils::{Session, SessionError}, }; use derive_more::{Display, Error, From}; @@ -14,13 +14,13 @@ pub enum Error { TransportError(TransportError), } -pub fn run(cmd: ExecuteSubcommand) -> Result<(), Error> { +pub fn run(cmd: ExecuteSubcommand, opt: CommonOpt) -> Result<(), Error> { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(async { run_async(cmd).await }) + rt.block_on(async { run_async(cmd, opt).await }) } -async fn run_async(cmd: ExecuteSubcommand) -> Result<(), Error> { +async fn run_async(cmd: ExecuteSubcommand, _opt: CommonOpt) -> Result<(), Error> { let session = Session::load().await?; let mut transport = Transport::connect(session).await?; diff --git a/src/subcommand/launch.rs b/src/subcommand/launch.rs index e446113..bcf10b9 100644 --- a/src/subcommand/launch.rs +++ b/src/subcommand/launch.rs @@ -1,6 +1,10 @@ -use crate::{opt::LaunchSubcommand, utils::Session}; +use crate::{ + opt::{CommonOpt, LaunchSubcommand}, + utils::Session, +}; use derive_more::{Display, Error, From}; use hex::FromHexError; +use log::*; use orion::{aead::SecretKey, errors::UnknownCryptoError}; use std::string::FromUtf8Error; use tokio::{io, process::Command}; @@ -16,15 +20,25 @@ pub enum Error { Utf8Error(FromUtf8Error), } -pub fn run(cmd: LaunchSubcommand) -> Result<(), Error> { +pub fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result<(), Error> { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(async { run_async(cmd).await }) + rt.block_on(async { run_async(cmd, opt).await }) } -async fn run_async(cmd: LaunchSubcommand) -> Result<(), Error> { +async fn run_async(cmd: LaunchSubcommand, _opt: CommonOpt) -> Result<(), Error> { let remote_command = format!( - "{} listen --daemon --host {}", - cmd.remote_program, cmd.bind_server + "{} listen --daemon --host {} {} {} {}", + cmd.remote_program, + cmd.bind_server, + if cmd.use_ipv6 { "-6" } else { "" }, + cmd.server_log_file + .as_ref() + .map(|path| format!("--log-file {:?}", path)) + .unwrap_or_default(), + match cmd.server_log_level { + 0 => String::new(), + n => format!("-{}", "v".repeat(n as usize)), + }, ); let ssh_command = format!( "{} -o StrictHostKeyChecking=no ssh://{}@{}:{} {} {}", @@ -35,7 +49,7 @@ async fn run_async(cmd: LaunchSubcommand) -> Result<(), Error> { cmd.identity_file .map(|f| format!("-i {}", f.as_path().display())) .unwrap_or_default(), - remote_command, + remote_command.trim(), ); let out = Command::new("sh") .arg("-c") @@ -84,7 +98,7 @@ async fn run_async(cmd: LaunchSubcommand) -> Result<(), Error> { session.save().await?; if cmd.print_startup_data { - println!("DISTANT DATA {} {}", port, session.to_hex_key()); + info!("DISTANT DATA {} {}", port, session.to_hex_key()); } Ok(()) diff --git a/src/subcommand/listen.rs b/src/subcommand/listen.rs index 5952245..d259569 100644 --- a/src/subcommand/listen.rs +++ b/src/subcommand/listen.rs @@ -1,10 +1,11 @@ use crate::{ - data::{Operation, Response, ResponsePayload}, - net::{Transport, TransportError}, - opt::{ConvertToIpAddrError, ListenSubcommand}, + data::{Operation, Response}, + net::Transport, + opt::{CommonOpt, ConvertToIpAddrError, ListenSubcommand}, }; use derive_more::{Display, Error, From}; use fork::{daemon, Fork}; +use log::*; use orion::aead::SecretKey; use std::{string::FromUtf8Error, sync::Arc}; use tokio::{io, net::TcpListener}; @@ -19,16 +20,16 @@ pub enum Error { Utf8Error(FromUtf8Error), } -pub fn run(cmd: ListenSubcommand) -> Result { +pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result { if cmd.daemon { // NOTE: We keep the stdin, stdout, stderr open so we can print out the pid with the parent match daemon(false, true) { Ok(Fork::Child) => { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(async { run_async(cmd, true).await })?; + rt.block_on(async { run_async(cmd, opt, true).await })?; } Ok(Fork::Parent(pid)) => { - eprintln!("[distant detached, pid = {}]", pid); + info!("[distant detached, pid = {}]", pid); if let Err(_) = fork::close_fd() { return Err(Error::ForkError); } @@ -37,17 +38,22 @@ pub fn run(cmd: ListenSubcommand) -> Result { } } else { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(async { run_async(cmd, false).await })?; + rt.block_on(async { run_async(cmd, opt, false).await })?; } Ok(()) } -async fn run_async(cmd: ListenSubcommand, is_forked: bool) -> Result { - let addr = cmd.host.to_ip_addr()?; +async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> Result { + let addr = cmd.host.to_ip_addr(cmd.use_ipv6)?; let socket_addrs = cmd.port.make_socket_addrs(addr); + + debug!("Binding to {} in range {}", addr, cmd.port); let listener = TcpListener::bind(socket_addrs.as_slice()).await?; + let port = listener.local_addr()?.port(); + debug!("Bound to port: {}", port); + let key = Arc::new(SecretKey::default()); // Print information about port, key, etc. unless told not to @@ -67,6 +73,19 @@ async fn run_async(cmd: ListenSubcommand, is_forked: bool) -> Result { // Wait for a client connection let (client, _) = listener.accept().await?; + // Grab the client's remote address for later logging purposes + let addr_string = match client.peer_addr() { + Ok(addr) => { + let addr_string = addr.to_string(); + info!("New client from {}", addr_string); + addr_string + } + Err(x) => { + error!("Unable to examine client's peer address: {}", x); + "???".to_string() + } + }; + // Build a transport around the client let mut transport = Transport::new(client, Arc::clone(&key)); @@ -74,18 +93,24 @@ async fn run_async(cmd: ListenSubcommand, is_forked: bool) -> Result { tokio::spawn(async move { loop { match transport.receive::().await { - Ok(_request) => { + Ok(request) => { + trace!( + "[{}] Received request of type {}", + addr_string.as_str(), + request.as_ref() + ); + let response = Response::Error { msg: String::from("Unimplemented"), }; if let Err(x) = transport.send(response).await { - eprintln!("ERROR: {:?}", x); + error!("{}", x); break; } } Err(x) => { - eprintln!("ERROR: {:?}", x); + error!("{}", x); break; } }