tokio-gemini/examples/main.rs

184 lines
5.4 KiB
Rust

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 <DNS server>\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<Client, LibError> {
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<String>,
url: String,
}
struct CertVerifier {
known_hosts: KnownHostsFile,
dns: Option<DnsClient>,
}
#[async_trait]
impl SelfsignedCertVerifier for CertVerifier {
async fn verify(
&self,
cert: &tokio_gemini::certs::CertificateDer<'_>,
host: &str,
port: u16,
) -> Result<bool, tokio_gemini::LibError> {
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)
}
}
}