213 lines
6.8 KiB
Rust
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(())
|
|
}
|