use crate::err::ClientErr;
use matchit::Params;
use openssl::{asn1::Asn1Time, hash::MessageDigest, x509::X509};
use std::{collections::HashMap, net::SocketAddr};
use url::Url;
#[derive(Clone, Debug)]
pub struct TitanResource {
pub mime: String,
pub size: usize,
pub token: Option<String>,
pub content: Vec<u8>,
}
#[derive(Clone)]
pub struct Client<S = ()> {
pub state: S,
pub url: Url,
pub params: HashMap<String, String>,
pub titan: Option<TitanResource>,
pub raw_cert: Option<X509>,
pub ip: SocketAddr,
}
impl<S: Clone> Client<S> {
pub fn new(
state: S,
url: Url,
cert: Option<X509>,
params: &Params<'_, '_>,
ip: SocketAddr,
titan: Option<TitanResource>,
) -> Result<Self, ClientErr> {
if let Some(cert) = &cert {
if cert
.not_after()
.compare(Asn1Time::days_from_now(0)?.as_ref())?
.is_le()
{
return Err(ClientErr::ExpiredCert);
}
}
Ok(Self {
state,
url,
raw_cert: cert,
params: params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
ip,
titan,
})
}
pub fn parameter(&self, key: &str) -> Option<&str> {
self.params.get(key).map(|x| x.as_str())
}
pub fn query(&self, key: &str) -> Option<String> {
self.url
.query_pairs()
.find(|x| x.0 == key)
.and_then(|x| urlencoding::decode(&x.1).ok().map(|x| x.to_string()))
}
pub fn input(&self) -> Option<String> {
self.url
.query()
.and_then(|x| urlencoding::decode(x).ok())
.map(|x| x.into_owned())
}
pub fn certificate(&self) -> Option<String> {
if let Some(cert) = &self.raw_cert {
match cert.to_pem() {
Ok(bytes) => match std::str::from_utf8(&bytes) {
Ok(s) => return Some(s.to_string()),
Err(e) => trace!("Client cert: invalid utf-8 in pem :: {e}"),
},
Err(e) => trace!("Client cert: failed to encode as pem :: {e}"),
}
}
None
}
pub fn fingerprint(&self) -> Option<String> {
use std::fmt::Write;
if let Some(cert) = &self.raw_cert {
match &cert.digest(MessageDigest::sha256()) {
Ok(digest_bytes) => {
let digest: String = digest_bytes.iter().fold(String::new(), |mut out, x| {
let _ = write!(out, "{:02x}", x);
out
});
return Some(digest);
}
Err(e) => trace!("Client cert: failed to get digest/fingerprint :: {e}"),
}
}
None
}
pub fn name(&self) -> Option<String> {
if let Some(cert) = &self.raw_cert {
if let Some(entry) = cert.subject_name().entries().next() {
match entry.data().as_utf8() {
Ok(name) => return Some(name.to_string()),
Err(e) => trace!("Couldn't parse name into utf8 :: {e}"),
}
}
}
None
}
pub fn verify(&self, other_cert: &str) -> bool {
match X509::from_pem(other_cert.as_bytes()) {
Ok(other_cert) => {
if let Some(cert) = &self.raw_cert {
if let Ok(other_cert) = other_cert.public_key() {
if let Ok(is_verified) = cert.verify(&other_cert) {
return is_verified;
}
}
}
}
Err(e) => trace!("Deserializing certificate string :: {e}"),
}
false
}
pub async fn render(&self, route: impl crate::GemCall<S>) -> String {
let bytes = route.gem_call(self.clone()).await;
let Ok(b) = std::str::from_utf8(&bytes) else {
return String::from("```\nInvalid gemini response.\n```");
};
let Some((header, content)) = b.split_once("\r\n") else {
return String::from("```\nInvalid gemini response.\n```");
};
let Some((status, meta)) = header.split_once(' ') else {
return String::from("```\nInvalid gemini response.\n```");
};
let Ok(status) = status.parse::<u8>() else {
return String::from("```\nInvalid gemini response.\n```");
};
if let Some(pos) = header.find("20 text/gemini") {
if pos == 0 {
return content.to_string();
}
}
let status: trotter::Status = status.into();
format!("```\n{status}\n{meta}\n```")
}
}