Files
hushd/src/client.rs
2024-10-21 09:40:11 -07:00

175 lines
5.6 KiB
Rust

use crate::config::{ClientCommand, ClientConfiguration};
use crate::crypto::{
CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm,
};
use crate::network::host::Host;
use crate::ssh;
use crate::OperationalError;
use error_stack::{report, Report, ResultExt};
use std::fmt::Display;
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,
}
}
struct Target {
username: String,
host: Host,
port: u16,
}
#[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: crate::network::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,
})
}
}
async fn connect(
base_config: &ClientConfiguration,
target: &str,
) -> error_stack::Result<(), OperationalError> {
let resolver = base_config.resolver();
let target = Target::from_str(target)
.change_context(OperationalError::UnableToParseHostAddress)
.attach_printable_lazy(|| format!("target address '{}'", target))?;
let mut stream = target
.host
.connect(resolver, 22)
.await
.change_context(OperationalError::Connection)?;
let stream_span = tracing::debug_span!(
"client connection",
server = %stream.peer_addr().map(|x| x.to_string()).unwrap_or_else(|_| "<unknown>".to_string()),
client = %stream.local_addr().map(|x| x.to_string()).unwrap_or_else(|_| "<unknown>".to_string()),
);
let their_preamble = ssh::Preamble::read(&mut stream)
.instrument(stream_span.clone())
.await
.change_context(OperationalError::Connection)?;
let our_preamble = ssh::Preamble::default()
.write(&mut stream)
.instrument(stream_span)
.await
.change_context(OperationalError::Connection)?;
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 mut stream = ssh::SshChannel::new(stream);
let their_initial = stream
.read()
.await
.attach_printable_lazy(stream_error_info)
.change_context(OperationalError::KeyExchange)?
.ok_or_else();
// 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: Display>(items: &[T]) -> error_stack::Result<(), OperationalError> {
for item in items.iter() {
println!("{}", item);
}
Ok(())
}