use async_trait::async_trait; use tokio_gemini::{ certs::{ dane, file::sscv::KnownHostsFile, fingerprint::CertFingerprint, SelfsignedCertVerifier, }, dns::DnsClient, Client, LibError, }; // // cargo add tokio_gemini -F file-sscv,dane // cargo add tokio -F macros,rt-multi-thread,io-util,fs // const USAGE: &str = "-k\t\tinsecure mode (trust all certs) -d \tuse custom DNS for resolving & DANE -h\t\tshow help"; #[tokio::main] async fn main() -> Result<(), LibError> { let config = parse_args(); let client = build_client(&config).await?; let mut resp = client.request(config.url).await?; { let status_code = resp.status().status_code(); let status_num: u8 = status_code.into(); eprintln!("{} {:?}", status_num, status_code); } if resp.status().reply_type() == tokio_gemini::ReplyType::Success { let mime = resp.mime()?; eprintln!("Mime: {}", mime); if mime.type_() == mime::TEXT { println!("{}", resp.text().await?); } else { eprintln!("Downloading into content.bin"); let mut f = tokio::fs::File::create("content.bin").await?; tokio::io::copy(&mut resp.stream(), &mut f).await?; } } else { eprintln!("Message: {}", resp.message()); } Ok(()) } fn parse_args() -> Config { let mut config = Config { insecure: false, dns: None, url: "gemini://geminiprotocol.net/".to_owned(), }; let mut expected_dns = false; for arg in std::env::args().skip(1) { match arg.as_str() { dns if expected_dns => { config.dns = Some(dns.to_owned()); expected_dns = false; } "-k" => config.insecure = true, "-d" => expected_dns = true, "-h" => { eprintln!("{}", USAGE); std::process::exit(0); } url => { eprintln!("URL: {}", url); config.url = url.to_owned(); break; } } } if expected_dns { eprintln!("{}", USAGE); std::process::exit(0); } config } async fn build_client(config: &Config) -> Result { let dns = if let Some(addr) = &config.dns { Some(DnsClient::init(addr).await?) } else { None }; let known_hosts = KnownHostsFile::parse_file("known_hosts").await?; let verifier = CertVerifier { known_hosts, dns: dns.clone(), }; let client = tokio_gemini::Client::builder(); let client = if config.insecure { client.dangerous_with_no_verifier() } else { client.with_selfsigned_cert_verifier(verifier) }; let client = client.maybe_with_dns_client(dns); Ok(client.build()) } struct Config { insecure: bool, dns: Option, url: String, } struct CertVerifier { known_hosts: KnownHostsFile, dns: Option, } #[async_trait] impl SelfsignedCertVerifier for CertVerifier { async fn verify( &self, cert: &tokio_gemini::certs::CertificateDer<'_>, host: &str, port: u16, ) -> Result { if let Some(known) = self.known_hosts.get_known_cert(host) { // if found in known_hosts, just compare certs Ok(known.fingerprint.hash_and_compare(cert)) } else { // otherwise, generate a hash and add to known_hosts let hash = if let Some(dns) = &self.dns { // if DNS client is configured, try verifying the cert // via DANE instead of blindly trusting match dane::dane(&dns, cert, host, port).await { Ok(hash) => hash, // use the fingerprint matched with TLSA record Err(LibError::HostLookupError) => { // no TLSA record found -- server admin haven't set it up eprintln!( "TLSA not configured for tcp:{}:{}, trusting on first use", host, port, ); // just generate a hash for this cert CertFingerprint::new_sha256(cert) } Err(e) => { // cert not matched, DNS server rejected request, etc. eprintln!("DANE verification failed: {:?}", e); return Err(e); } } } else { eprintln!("DANE disabled"); // just generate a hash for this cert CertFingerprint::new_sha256(cert) }; let fingerprint = hash.base64(); let fptype = hash.fingerprint_type_str(); eprintln!( "Warning: adding trusted cert for {} with FP {}", host, &fingerprint, ); // adding the cert hash to trusted // can be done simplier: // self.known_hosts.add_trusted_cert(host, hash).await.unwrap... self.known_hosts.add_cert_to_hashmap(host, hash); self.known_hosts .add_cert_to_file(host, &fingerprint, fptype) .await .unwrap_or_else(|e| { eprintln!("Cert saved in-memory, unable to write to file: {:?}", e); }); Ok(true) } } }