bssh 2.4.3

Parallel SSH command execution tool for cluster management
Documentation
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};

pub trait ToSocketAddrsWithHostname {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>>;
    fn hostname(&self) -> String;
    fn host_port(&self) -> io::Result<(String, u16)> {
        parse_host_port(&self.hostname())
    }
}

impl ToSocketAddrsWithHostname for String {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        self.as_str().to_socket_addrs().map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        self.clone()
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        parse_host_port(self)
    }
}

impl ToSocketAddrsWithHostname for &str {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        self.to_string()
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        parse_host_port(self)
    }
}

impl ToSocketAddrsWithHostname for (&str, u16) {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        self.0.to_string()
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.0.to_string(), self.1))
    }
}

impl ToSocketAddrsWithHostname for (String, u16) {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        self.0.clone()
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.0.clone(), self.1))
    }
}

impl ToSocketAddrsWithHostname for (IpAddr, u16) {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        format!("{}", self.0)
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.0.to_string(), self.1))
    }
}

impl ToSocketAddrsWithHostname for (Ipv4Addr, u16) {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        format!("{}", self.0)
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.0.to_string(), self.1))
    }
}

impl ToSocketAddrsWithHostname for (Ipv6Addr, u16) {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        ToSocketAddrs::to_socket_addrs(self).map(|iter| iter.collect())
    }
    fn hostname(&self) -> String {
        format!("{}", self.0)
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.0.to_string(), self.1))
    }
}

impl ToSocketAddrsWithHostname for SocketAddr {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        Ok(vec![*self])
    }
    fn hostname(&self) -> String {
        format!("{}", self.ip())
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.ip().to_string(), self.port()))
    }
}

impl ToSocketAddrsWithHostname for SocketAddrV4 {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        Ok(vec![SocketAddr::V4(*self)])
    }
    fn hostname(&self) -> String {
        format!("{}", self.ip())
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.ip().to_string(), self.port()))
    }
}

impl ToSocketAddrsWithHostname for SocketAddrV6 {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        Ok(vec![SocketAddr::V6(*self)])
    }
    fn hostname(&self) -> String {
        format!("{}", self.ip())
    }
    fn host_port(&self) -> io::Result<(String, u16)> {
        Ok((self.ip().to_string(), self.port()))
    }
}

impl ToSocketAddrsWithHostname for &[SocketAddr] {
    fn to_socket_addrs(&self) -> io::Result<Vec<SocketAddr>> {
        Ok(self.to_vec())
    }

    fn hostname(&self) -> String {
        self.first()
            .map(|addr| addr.ip().to_string())
            .unwrap_or_default()
    }

    fn host_port(&self) -> io::Result<(String, u16)> {
        self.first()
            .map(|addr| (addr.ip().to_string(), addr.port()))
            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing socket address"))
    }
}

fn parse_host_port(target: &str) -> io::Result<(String, u16)> {
    let (host, port) = if let Some(rest) = target.strip_prefix('[') {
        let (host, rest) = rest.split_once(']').ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidInput,
                "missing closing bracket in IPv6 host",
            )
        })?;
        let port = rest
            .strip_prefix(':')
            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing port separator"))?;
        (host, port)
    } else {
        target
            .rsplit_once(':')
            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing port separator"))?
    };

    if host.is_empty() {
        return Err(io::Error::new(io::ErrorKind::InvalidInput, "missing host"));
    }

    let port = port
        .parse::<u16>()
        .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
    Ok((host.to_string(), port))
}

#[cfg(test)]
mod tests {
    use super::{ToSocketAddrsWithHostname, parse_host_port};
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

    #[test]
    fn socket_addr_slice_hostname_uses_first_address_only() {
        let addrs = [
            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 22),
            SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 22),
        ];

        assert_eq!(addrs.as_slice().hostname(), "127.0.0.1");
    }

    #[test]
    fn empty_socket_addr_slice_hostname_is_empty() {
        let addrs: [SocketAddr; 0] = [];

        assert_eq!(addrs.as_slice().hostname(), "");
    }

    #[test]
    fn host_port_parses_domain_without_resolution() {
        assert_eq!(
            "server-only.internal:5432".host_port().unwrap(),
            ("server-only.internal".to_string(), 5432)
        );
    }

    #[test]
    fn host_port_parses_bracketed_ipv6_literal() {
        assert_eq!(
            parse_host_port("[2001:db8::1]:443").unwrap(),
            ("2001:db8::1".to_string(), 443)
        );
    }
}