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 {