use configuration::client::{ClientCommand, ClientConfiguration}; use crypto::{ CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm, }; use host::Host; use resolver::Resolver; use ssh::{self, SshKeyExchange}; use ssh::OperationalError; use error_stack::{report, Report, ResultExt}; use std::fmt; use std::str::FromStr; use thiserror::Error; use tracing::Instrument; pub async fn hush(base_config: ClientConfiguration) -> error_stack::Result<(), OperationalError> { match base_config.command() { ClientCommand::ListMacAlgorithms => print_options(MacAlgorithm::allowed()), ClientCommand::ListHostKeyAlgorithms => print_options(HostKeyAlgorithm::allowed()), ClientCommand::ListEncryptionAlgorithms => print_options(EncryptionAlgorithm::allowed()), ClientCommand::ListKeyExchangeAlgorithms => print_options(KeyExchangeAlgorithm::allowed()), ClientCommand::ListCompressionAlgorithms => print_options(CompressionAlgorithm::allowed()), ClientCommand::Connect { target } => connect(&base_config, target).await, } } #[derive(Debug)] struct Target { username: String, host: Host, port: u16, } impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}@{}:{}", self.username, self.host, self.port) } } #[derive(Debug, Error)] pub enum TargetParseError { #[error("Invalid port number '{port_string}': {error}")] InvalidPort { port_string: String, error: std::num::ParseIntError, }, #[error("Invalid hostname '{hostname}': {error}")] InvalidHostname { hostname: String, error: host::HostParseError, }, } impl FromStr for Target { type Err = Report; fn from_str(s: &str) -> Result { let (username, host_and_port) = match s.split_once('@') { None => (whoami::username(), s), Some((username, rest)) => (username.to_string(), rest), }; let (port, host) = match host_and_port.rsplit_once(':') { None => (22, host_and_port), Some((host, portstr)) => match u16::from_str(portstr) { Ok(port) => (port, host), Err(error) => { return Err(report!(TargetParseError::InvalidPort { port_string: portstr.to_string(), error, }) .attach_printable(format!("from target string {:?}", s))) } }, }; let host = Host::from_str(host) .map_err(|error| { report!(TargetParseError::InvalidHostname { hostname: host.to_string(), error, }) }) .attach_printable(format!("from target string {:?}", s))?; Ok(Target { username, host, port, }) } } #[allow(unreachable_code, unused_variables)] async fn connect( base_config: &ClientConfiguration, target: &str, ) -> error_stack::Result<(), OperationalError> { let mut resolver: Resolver = Resolver::new(&base_config.dns_config) .await .change_context(OperationalError::Resolver)?; // let mut resolver = Resolver::new(&base_config.resolver) // .await // .change_context(OperationalError::DnsConfig)?; let target = Target::from_str(target) .change_context(OperationalError::UnableToParseHostAddress) .attach_printable_lazy(|| format!("target address '{}'", target))?; tracing::trace!(%target, "determined SSH target"); let mut stream = target .host .connect(&mut resolver, 22) .await .change_context(OperationalError::Connection)?; let server_addr_str = stream .peer_addr() .map(|x| x.to_string()) .unwrap_or_else(|_| "".to_string()); let client_addr_str = stream .local_addr() .map(|x| x.to_string()) .unwrap_or_else(|_| "".to_string()); let stream_span = tracing::debug_span!( "client connection", server = %server_addr_str, client = %client_addr_str, ); tracing::trace!(%target, "connected to target server"); let stream_error_info = || server_addr_str.clone(); let their_preamble = ssh::Preamble::read(&mut stream) .instrument(stream_span.clone()) .await .change_context(OperationalError::Connection)?; tracing::trace!("received their preamble"); ssh::Preamble::default() .write(&mut stream) .instrument(stream_span) .await .change_context(OperationalError::Connection)?; tracing::trace!("wrote our preamble"); if !their_preamble.preamble.is_empty() { for line in their_preamble.preamble.lines() { tracing::info!("server: {}", line); } } tracing::info!( software = ?their_preamble.software_name, version = ?their_preamble.software_version, commentary = ?their_preamble.commentary, "received server preamble" ); let stream = ssh::SshChannel::new(stream).expect("can build new SSH channel"); let their_initial = stream .read() .await .attach_printable_lazy(stream_error_info) .change_context(OperationalError::KeyExchange)? .ok_or_else(|| report!(OperationalError::KeyExchange)) .attach_printable_lazy(stream_error_info) .attach_printable_lazy(|| "No initial key exchange message found")?; let their_kex: SshKeyExchange = SshKeyExchange::try_from(their_initial) .attach_printable_lazy(stream_error_info) .change_context(OperationalError::KeyExchange)?; println!("their_key: {:?}", their_kex); // let mut rng = rand::thread_rng(); // // let packet = stream // .read() // .await // .map_err(|error| OperationalError::Read { // context: "Initial key exchange read", // error, // })? // .expect("has initial packet"); // let message_type = SshMessageID::from(packet.buffer[0]); // tracing::debug!(size=packet.buffer.len(), %message_type, "Initial buffer received."); // let keyx = SshKeyExchange::try_from(packet)?; // println!("{:?}", keyx); // // let client_config = config.settings_for(""); // let my_kex = SshKeyExchange::new(&mut rng, client_config)?; // stream // .write(my_kex.into()) // .await // .map_err(|error| OperationalError::Write { // context: "initial negotiation message", // error, // })?; Ok(()) } fn print_options(items: &[T]) -> error_stack::Result<(), OperationalError> { for item in items.iter() { println!("{}", item); } Ok(()) }