184 lines
5.4 KiB
Rust
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)
|
|
}
|
|
}
|
|
}
|