Files
hushd/client/src/lib.rs
2025-05-03 17:30:01 -07:00

213 lines
6.8 KiB
Rust

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<TargetParseError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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(|_| "<unknown>".to_string());
let client_addr_str = stream
.local_addr()
.map(|x| x.to_string())
.unwrap_or_else(|_| "<unknown>".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<T: fmt::Display>(items: &[T]) -> error_stack::Result<(), OperationalError> {
for item in items.iter() {
println!("{}", item);
}
Ok(())
}