telnetz 0.1.0

telnet like commandline tool for connecting to TCP socketsa
use std::env;
use std::error::Error;
use std::net::ToSocketAddrs;

use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio_rustls::TlsConnector;

trait StreamLike: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> StreamLike for T {}
type DynStream = Box<dyn StreamLike>;

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    tls: bool,
    insecure: bool,
}

fn print_usage() {
    eprintln!(
        "Usage: telnetz <host> <port> [--tls] [--insecure]\n\
         \n\
         Options:\n\
           --tls        Use TLS for the connection.\n\
           --insecure   Skip certificate validation (TLS only).\n\
           -h, --help   Show this help.\n\
         \n\
         Commands:\n\
           /quit, /exit Exit the session.\n"
    );
}

fn parse_args() -> Result<Config, Box<dyn Error>> {
    let mut host: Option<String> = None;
    let mut port: Option<u16> = None;
    let mut tls = false;
    let mut insecure = false;

    let mut args = env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--tls" => tls = true,
            "--insecure" => insecure = true,
            "-h" | "--help" => {
                print_usage();
                std::process::exit(0);
            }
            _ => {
                if host.is_none() {
                    host = Some(arg);
                } else if port.is_none() {
                    port = Some(arg.parse()?);
                } else {
                    return Err(format!("Unexpected argument: {arg}").into());
                }
            }
        }
    }

    let host = host.ok_or("Missing host")?;
    let port = port.ok_or("Missing port")?;

    if insecure && !tls {
        return Err("--insecure requires --tls".into());
    }

    Ok(Config {
        host,
        port,
        tls,
        insecure,
    })
}

async fn connect(config: &Config) -> Result<DynStream, Box<dyn Error>> {
    let addr = (config.host.as_str(), config.port)
        .to_socket_addrs()?
        .next()
        .ok_or("Unable to resolve host")?;
    let stream = TcpStream::connect(addr).await?;

    if !config.tls {
        return Ok(Box::new(stream));
    }

    let client_config = if config.insecure {
        rustls::ClientConfig::builder()
            .dangerous()
            .with_custom_certificate_verifier(NoVerifier::new())
            .with_no_client_auth()
    } else {
        let mut root_store = rustls::RootCertStore::empty();
        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
        rustls::ClientConfig::builder()
            .with_root_certificates(root_store)
            .with_no_client_auth()
    };

    let connector = TlsConnector::from(std::sync::Arc::new(client_config));
    let server_name = rustls::pki_types::ServerName::try_from(config.host.clone())
        .map_err(|_| "Invalid DNS name for TLS")?;
    let tls_stream = connector.connect(server_name, stream).await?;

    Ok(Box::new(tls_stream))
}

#[derive(Debug)]
struct NoVerifier(std::sync::Arc<rustls::crypto::CryptoProvider>);

impl NoVerifier {
    fn new() -> std::sync::Arc<Self> {
        std::sync::Arc::new(Self(std::sync::Arc::new(
            rustls::crypto::aws_lc_rs::default_provider(),
        )))
    }
}

impl rustls::client::danger::ServerCertVerifier for NoVerifier {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::pki_types::CertificateDer<'_>,
        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &rustls::pki_types::CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls12_signature(
            message,
            cert,
            dss,
            &self.0.signature_verification_algorithms,
        )
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &rustls::pki_types::CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &self.0.signature_verification_algorithms,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        self.0.signature_verification_algorithms.supported_schemes()
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let config = match parse_args() {
        Ok(config) => config,
        Err(err) => {
            eprintln!("Error: {err}");
            print_usage();
            return Ok(());
        }
    };

    let stream = connect(&config).await?;
    let (read_half, mut write_half) = io::split(stream);

    let mut socket_reader = io::BufReader::new(read_half);
    let mut socket_buf = vec![0u8; 2048];
    let (line_tx, mut line_rx) = mpsc::unbounded_channel::<String>();

    tokio::task::spawn_blocking(move || {
        let mut rl = rustyline::Editor::<(), rustyline::history::DefaultHistory>::new()
            .expect("failed to initialize rustyline");
        loop {
            match rl.readline("") {
                Ok(line) => {
                    if line_tx.send(line).is_err() {
                        break;
                    }
                }
                Err(rustyline::error::ReadlineError::Interrupted) => {
                    continue;
                }
                Err(rustyline::error::ReadlineError::Eof) => {
                    break;
                }
                Err(err) => {
                    eprintln!("Readline error: {err}");
                    break;
                }
            }
        }
    });
    let mut stdout = io::stdout();

    eprintln!(
        "Connected to {}:{} (tls: {}). Type /quit to exit.",
        config.host, config.port, config.tls
    );

    loop {
        tokio::select! {
            result = socket_reader.read(&mut socket_buf) => {
                let n = result?;
                if n == 0 {
                    eprintln!("\nConnection closed by remote.");
                    break;
                }
                stdout.write_all(&socket_buf[..n]).await?;
                stdout.flush().await?;
            }
            line = line_rx.recv() => {
                match line {
                    Some(line) => {
                        if line == "/quit" || line == "/exit" {
                            break;
                        }
                        write_half.write_all(line.as_bytes()).await?;
                        write_half.write_all(b"\r\n").await?;
                        write_half.flush().await?;
                    }
                    None => break,
                }
            }
        }
    }

    write_half.shutdown().await?;
    Ok(())
}