~klve/poptea

177abd7ef68c64eb229ec42b125914981e895c63 — klve bunc ntnn 5 months ago 229378c
Handling all errors inside the library
5 files changed, 75 insertions(+), 38 deletions(-)

M src/bin/poptea-cli.rs
M src/infra/fs.rs
M src/infra/mod.rs
R src/infra/{client.rs => tls.rs}
M src/lib.rs
M src/bin/poptea-cli.rs => src/bin/poptea-cli.rs +6 -4
@@ 7,9 7,7 @@ use std::{

fn main() {
    let url = std::env::args().nth(1).expect("please provide gemini url");
    let fs = Arc::new(
        Mutex::new(poptea::FileSystem::new(".poptea".into()))
    );
    let fs = Arc::new(Mutex::new(poptea::FileSystem::new(".poptea".into()).expect("failed to init file storage")));

    let client = poptea::TlsClient::new(fs.clone());
    let res = client.get(&url).expect("failed to make a request");


@@ 18,5 16,9 @@ fn main() {
        .write_all(&res.body.unwrap_or_else(|| b"response has no body".to_vec()))
        .expect("failed to write to stdout");

    fs.lock().unwrap().flush_trust_store();
    fs
        .lock()
        .expect("filesystem mutex deadlock")
        .flush_trust_store()
        .expect("failed to persist known hosts");
}

M src/infra/fs.rs => src/infra/fs.rs +20 -16
@@ 1,9 1,9 @@
use crate::{PopResult, TrustStore, VerifyStatus};
use std::collections::HashMap;
use std::fs::{create_dir, OpenOptions};
use std::io::{self, BufRead};
use std::io::prelude::*;
use std::io::LineWriter;
use std::collections::HashMap;
use std::io::{self, BufRead};
use crate::{PopError, PopResult, TrustStore, VerifyStatus};

pub struct FileSystem {
    trust_store: HashMap<String, String>,


@@ 11,46 11,50 @@ pub struct FileSystem {
}

impl FileSystem {
    pub fn new(pop_dir: String) -> Self {
    pub fn new(pop_dir: String) -> PopResult<Self> {
        let mut trust_store = HashMap::new();
        Self::load_trust_store(&pop_dir, &mut trust_store);
        Self::load_trust_store(&pop_dir, &mut trust_store)?;

        Self { 
        Ok(Self {
            trust_store,
            pop_dir,
        }
        })
    }

    fn load_trust_store(pop_dir: &str, store: &mut HashMap<String, String>) {
    fn load_trust_store(pop_dir: &str, store: &mut HashMap<String, String>) -> PopResult<()>{
        let trust_path = format!("{}/known_hosts", pop_dir);
        let file = OpenOptions::new()
            .write(true)
            .read(true)
            .open(trust_path).unwrap();
            .open(trust_path)
            .map_err(|e| PopError::Local(e.to_string()))?;

        for line in io::BufReader::new(file).lines() {
            if let Ok(kh) = line {
                let (host, fingerprint) = kh
                    .split_once(" ").unwrap();
                let (host, fingerprint) = kh.split_once(" ").ok_or_else(|| PopError::Local("failed parse fingerprint line".into()))?;

                store.insert(host.to_string(), fingerprint.to_string());
            }
        }

        Ok(())
    }

    pub fn flush_trust_store(&self) {
    pub fn flush_trust_store(&self) -> PopResult<()> {
        let trust_path = format!("{}/known_hosts", self.pop_dir);
        let file = OpenOptions::new()
            .write(true)
            .read(true)
            .open(trust_path).unwrap();
            .open(trust_path)
            .map_err(|e| PopError::Local(e.to_string()))?;
        let mut file = LineWriter::new(file);

        for (h, f) in &self.trust_store {
            file.write_all(format!("{} {}\n", h, f).as_bytes()).unwrap();
        };
            file.write_all(format!("{} {}\n", h, f).as_bytes()).map_err(|e| PopError::Local(e.to_string()))?;
        }

        file.flush().unwrap();
        file.flush().map_err(|e| PopError::Local(e.to_string()))?;
        Ok(())
    }
}


M src/infra/mod.rs => src/infra/mod.rs +2 -2
@@ 1,5 1,5 @@
mod client;
mod fs;
mod tls;

pub use client::TlsClient;
pub use fs::FileSystem;
pub use tls::TlsClient;

R src/infra/client.rs => src/infra/tls.rs +37 -16
@@ 1,18 1,18 @@
use std::fs::File;
use url::Url;

use data_encoding::BASE32HEX_NOPAD;
use io::{Read, Write};
use sha3::{Digest, Sha3_256};
use std::fs::File;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::{convert::Into, io, io::BufRead};
use url::Url;
use x509_parser::prelude::*;

use crate::{GemResponse, GemStatus, GeminiClient, PopResult, TrustStore, VerifyStatus};
use crate::{GemResponse, GemStatus, GeminiClient, PopError, PopResult, TrustStore, VerifyStatus};

fn fingerprint(cert: &rustls::Certificate) -> PopResult<(String, String)> {
    let (_, pk) = X509Certificate::from_der(cert.as_ref()).unwrap();
    let (_, pk) = X509Certificate::from_der(cert.as_ref())
        .map_err(|err| PopError::Remote(err.to_string()))?;
    let sub = pk.subject().to_string();
    let sub_pk = pk.public_key().subject_public_key.as_ref();



@@ 38,10 38,16 @@ impl rustls::client::ServerCertVerifier for TofuVerification {
        _now: std::time::SystemTime,
    ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
        let path = "cert.der";
        let mut file = File::create(path).unwrap();
        file.write_all(cert.as_ref()).unwrap();
        let (addr, fingerprint) = fingerprint(cert).unwrap();
        let store = self.store.lock().unwrap().verify(&addr, fingerprint);
        let mut file = File::create(path).map_err(|e| rustls::Error::General(e.to_string()))?;
        file.write_all(cert.as_ref())
            .map_err(|e| rustls::Error::General(e.to_string()))?;
        let (addr, fingerprint) =
            fingerprint(cert).map_err(|e| rustls::Error::General(e.to_string()))?;
        let store = self
            .store
            .lock()
            .map_err(|e| rustls::Error::General(e.to_string()))?
            .verify(&addr, fingerprint);
        match store {
            Ok(VerifyStatus::Trusted) => Ok(rustls::client::ServerCertVerified::assertion()),
            Ok(VerifyStatus::Untrusted) => Err(rustls::Error::General("untrusted".into())),


@@ 66,7 72,7 @@ impl TlsClient {
        let mut plaintext = vec![];
        let host = url
            .host_str()
            .ok_or_else(|| crate::PopError::Local("host is missing".into()))?;
            .ok_or_else(|| PopError::Local("host is missing".into()))?;
        let addr = format!("{}:{:?}", host, url.port().unwrap_or(1965));
        let req = format!("{}\r\n", url);



@@ 81,11 87,22 @@ impl TlsClient {
            }));
        let arc = std::sync::Arc::new(config);

        let mut sess = rustls::ClientConnection::new(arc, host.try_into().unwrap()).unwrap();
        let mut sock = std::net::TcpStream::connect(addr).unwrap();
        let mut sess = rustls::ClientConnection::new(
            arc,
            host.try_into()
                .map_err(|_| PopError::Local("failed to parse host".into()))?,
        )
        .map_err(|e| PopError::Remote(e.to_string()))?;

        let mut sock =
            std::net::TcpStream::connect(addr).map_err(|e| PopError::Local(e.to_string()))?;
        let mut stream = rustls::Stream::new(&mut sess, &mut sock);
        stream.write_all(req.as_bytes()).unwrap();
        stream.read_to_end(&mut plaintext).unwrap();
        stream
            .write_all(req.as_bytes())
            .map_err(|e| PopError::Local(e.to_string()))?;
        stream
            .read_to_end(&mut plaintext)
            .map_err(|e| PopError::Local(e.to_string()))?;

        Ok(plaintext)
    }


@@ 94,13 111,17 @@ impl TlsClient {
impl GeminiClient for TlsClient {
    fn get(&self, url: &str) -> PopResult<GemResponse> {
        let plaintext = self.get_plain(url)?;
        let header = plaintext.lines().next().unwrap().unwrap();
        let header = plaintext
            .lines()
            .next()
            .ok_or_else(|| PopError::Local("header is not present".into()))?
            .map_err(|e| PopError::Remote(e.to_string()))?;
        let body = plaintext[header.len()..].to_vec();

        let (status, meta) = header
            .split_once(" ")
            .map(|(s, m)| (GemStatus::from_str(s), m.into()))
            .ok_or_else(|| crate::PopError::Remote("invalid header".into()))?;
            .ok_or_else(|| PopError::Remote("invalid header".into()))?;

        let body = match &body.len() {
            0 => None,

M src/lib.rs => src/lib.rs +10 -0
@@ 1,3 1,4 @@
use std::fmt;
use std::str::FromStr;

mod infra;


@@ 58,6 59,15 @@ pub enum PopError {
    Remote(String),
}

impl fmt::Display for PopError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PopError::Local(e) => write!(f, "{}", e.to_string()),
            PopError::Remote(e) => write!(f, "{}", e.to_string()),
        }
    }
}

pub type PopResult<T> = Result<T, PopError>;

pub trait TrustStore: Send + Sync {