diff --git a/.gitignore b/.gitignore index 65a366f..c05fedb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /Cargo.lock tags tags.lock +tags.temp + diff --git a/Cargo.toml b/Cargo.toml index 46e28e5..75649b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,8 @@ x509-parser = "0.15.0" version = "0.21.1" features = [ "dangerous_configuration" ] +[dependencies.serde] +version = "1.0" +features = ["derive"] +optional = true + diff --git a/src/certificate_store.rs b/src/certificate_store.rs new file mode 100644 index 0000000..c258680 --- /dev/null +++ b/src/certificate_store.rs @@ -0,0 +1,36 @@ +use std::collections::{BTreeMap, HashMap}; + +/// An item which stores known certificates +pub trait CertificateStore: Send + Sync { + fn get_certificate(&self, host: &str) -> Option; + fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option; + fn contains_certificate(&self, host: &str) -> bool; +} + +impl CertificateStore for HashMap { + fn get_certificate(&self, host: &str) -> Option { + self.get(host).cloned() + } + + fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option { + self.insert(host.to_string(), fingerprint.to_string()) + } + + fn contains_certificate(&self, host: &str) -> bool { + self.contains_key(host) + } +} + +impl CertificateStore for BTreeMap { + fn get_certificate(&self, host: &str) -> Option { + self.get(host).cloned() + } + + fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option { + self.insert(host.to_string(), fingerprint.to_string()) + } + + fn contains_certificate(&self, host: &str) -> bool { + self.contains_key(host) + } +} diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index cf78c92..0000000 --- a/src/client.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod store; -pub mod verifier; - diff --git a/src/client/store.rs b/src/client/store.rs deleted file mode 100644 index 8b55a63..0000000 --- a/src/client/store.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub trait CertificateStore { - fn get(&self, host: &str) -> Option; - fn insert(&mut self, host: &str, fingerprint: &str); -} - diff --git a/src/client/verifier.rs b/src/client/verifier.rs deleted file mode 100644 index d341622..0000000 --- a/src/client/verifier.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::fingerprint::Fingerprint; -use rustls::{client::{ServerCertVerified, ServerCertVerifier}, Certificate}; -use super::store::CertificateStore; - -pub struct Verifier<'a, T: CertificateStore> { - store: &'a T, -} - -impl<'a, T: CertificateStore + Sync> ServerCertVerifier for Verifier<'a, T> { - fn verify_server_cert( - &self, - end_entity: &Certificate, - _intermediates: &[Certificate], - server_name: &rustls::ServerName, - _scts: &mut dyn Iterator, - _ocsp_response: &[u8], - _now: std::time::SystemTime, - ) -> Result { - let fp = end_entity.fingerprint().map_err(|e| rustls::Error::General(e.to_string()))?; - let name = match server_name { - rustls::ServerName::DnsName(n) => n.as_ref().to_string(), - rustls::ServerName::IpAddress(ip) => ip.to_string(), - _ => todo!() - }; - if let Some(fingerprint) = match server_name { - rustls::ServerName::DnsName(n) => self.store.get(n.as_ref()), - rustls::ServerName::IpAddress(ip) => self.store.get(&ip.to_string()), - _ => todo!(), - } { - if fingerprint == fp.1 && name == fp.0 { - return Ok(ServerCertVerified::assertion()); - } - } else { - // todo: need a way to update `self.store`. Probably will require - // an Arc> for interior mutability - } - return Err(rustls::Error::General("Unrecognized certificate".to_string())); - } -} diff --git a/src/fingerprint.rs b/src/fingerprint.rs deleted file mode 100644 index b5e6dce..0000000 --- a/src/fingerprint.rs +++ /dev/null @@ -1,66 +0,0 @@ -use digest::Digest; -use rustls::Certificate; -use sha2::Sha256; -use std::fmt::{Write, self}; -use x509_parser::prelude::*; - -pub trait Fingerprint { - type Error; - - fn fingerprint(&self) -> Result<(String, String), Self::Error>; -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Error { - Fmt, - X509(X509Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Fmt => write!(f, "FingerPrint: format error"), - Self::X509(e) => write!(f, "FingerPrint: {e}"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Fmt => None, - Self::X509(e) => Some(e), - } - } -} - -impl From for Error { - fn from(_value: fmt::Error) -> Self { - Self::Fmt - } -} - -impl From> for Error { - fn from(value: x509_parser::nom::Err) -> Self { - Self::X509(value.into()) - } -} - -impl Fingerprint for Certificate { - type Error = Error; - - fn fingerprint(&self) -> Result<(String, String), Self::Error> { - let (_, pk) = X509Certificate::from_der(self.as_ref())?; - let subject = pk.subject().to_string(); - let key = pk.public_key().subject_public_key.as_ref(); - let mut hasher = Sha256::new(); - hasher.update(key); - let res = hasher.finalize(); - let mut s = String::with_capacity(res.len()); - for c in res { - write!(s, "{c:02x}")?; - } - Ok((subject[3..].to_string(), s)) - } -} - diff --git a/src/fingerprint/error.rs b/src/fingerprint/error.rs new file mode 100644 index 0000000..8149a75 --- /dev/null +++ b/src/fingerprint/error.rs @@ -0,0 +1,40 @@ +use {std::fmt, x509_parser::prelude::X509Error}; + +#[derive(Debug)] +/// Errors which can occur when fingerprinting a certificate +pub enum Error { + Fmt, + InvalidForDate, + X509(X509Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Fmt => write!(f, "FingerPrint: format error"), + Self::InvalidForDate => write!(f, "FingerPrint: invalid for date"), + Self::X509(e) => write!(f, "FingerPrint: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::X509(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(_value: fmt::Error) -> Self { + Self::Fmt + } +} + +impl From> for Error { + fn from(value: x509_parser::nom::Err) -> Self { + Self::X509(value.into()) + } +} diff --git a/src/fingerprint/mod.rs b/src/fingerprint/mod.rs new file mode 100644 index 0000000..14bd46f --- /dev/null +++ b/src/fingerprint/mod.rs @@ -0,0 +1,54 @@ +use { + digest::Digest, + rustls::Certificate, + sha2::Sha256, + std::{fmt::Write, io::Read}, + x509_parser::prelude::*, +}; + +mod error; +pub use error::Error; + +/// Creates an sha256 fingerprint for a certificate +pub trait GetFingerprint { + type Error; + + fn fingerprint(&self) -> Result; +} + +pub struct Fingerprint { + pub names: Vec, + pub fingerprint: String, +} + +impl GetFingerprint for Certificate { + type Error = Error; + + fn fingerprint(&self) -> Result { + let (_, pk) = X509Certificate::from_der(self.as_ref())?; + let subject = pk.subject(); + let mut names = vec![]; + subject.iter_common_name().for_each(|n| { + let mut val = n.attr_value().data; + let mut name = String::new(); + if let Ok(_) = val.read_to_string(&mut name) { + names.push(name); + } + }); + if !pk.validity().is_valid() { + return Err(Error::InvalidForDate); + } + let key = pk.public_key().subject_public_key.as_ref(); + let mut hasher = Sha256::new(); + hasher.update(key); + let res = hasher.finalize(); + let mut s = String::with_capacity(res.len()); + for c in res { + write!(s, "{c:02x}")?; + } + Ok(Fingerprint { + names, + fingerprint: s, + }) + } +} diff --git a/src/host/error.rs b/src/host/error.rs new file mode 100644 index 0000000..ad38fce --- /dev/null +++ b/src/host/error.rs @@ -0,0 +1,19 @@ +use std::fmt; + +#[derive(Debug, PartialEq)] +/// Errors which can occur when parsing a host from a string +pub enum Error { + MissingSeparator, + EmptyDomain, + EmptyTld, + EmptySubdomain, + IllegalWhitespace, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} diff --git a/src/host.rs b/src/host/mod.rs similarity index 74% rename from src/host.rs rename to src/host/mod.rs index 45e27f3..a0a4c36 100644 --- a/src/host.rs +++ b/src/host/mod.rs @@ -12,8 +12,15 @@ //! assert_eq!(host.tld.as_str(), "com"); //! ``` use std::{fmt, str::FromStr}; +mod error; +pub use error::Error; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Represents the fully qualified domain name for this host pub struct Host { pub subdomain: Option, pub domain: String, @@ -30,40 +37,23 @@ impl fmt::Display for Host { } } -#[derive(Clone, Debug, PartialEq)] -pub enum ParseHostError { - MissingSeparator, - EmptyDomain, - EmptyTld, - EmptySubdomain, - IllegalWhitespace, -} - -impl fmt::Display for ParseHostError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for ParseHostError {} - impl FromStr for Host { - type Err = ParseHostError; + type Err = Error; fn from_str(s: &str) -> Result { if s.contains(char::is_whitespace) { - return Err(ParseHostError::IllegalWhitespace); + return Err(Error::IllegalWhitespace); } if let Some((domain, tld)) = s.rsplit_once('.') { if domain.is_empty() { - Err(ParseHostError::EmptyDomain) + Err(Error::EmptyDomain) } else if tld.is_empty() { - Err(ParseHostError::EmptyTld) + Err(Error::EmptyTld) } else if let Some((subdomain, domain)) = domain.rsplit_once('.') { if subdomain.is_empty() { - Err(ParseHostError::EmptySubdomain) + Err(Error::EmptySubdomain) } else if domain.is_empty() { - Err(ParseHostError::EmptyDomain) + Err(Error::EmptyDomain) } else { Ok(Host { subdomain: Some(subdomain.to_string()), @@ -79,7 +69,7 @@ impl FromStr for Host { }) } } else { - Err(ParseHostError::MissingSeparator) + Err(Error::MissingSeparator) } } } @@ -92,37 +82,31 @@ mod tests { fn parse_missing_separator() { assert_eq!( "exampledotcom".parse::(), - Err(ParseHostError::MissingSeparator) + Err(Error::MissingSeparator) ); } #[test] fn parse_empty_tld() { - assert_eq!("example.".parse::(), Err(ParseHostError::EmptyTld)); + assert_eq!("example.".parse::(), Err(Error::EmptyTld)); } #[test] fn parse_empty_domain() { - assert_eq!(".com".parse::(), Err(ParseHostError::EmptyDomain)); - assert_eq!( - "example..com".parse::(), - Err(ParseHostError::EmptyDomain) - ); + assert_eq!(".com".parse::(), Err(Error::EmptyDomain)); + assert_eq!("example..com".parse::(), Err(Error::EmptyDomain)); } #[test] fn parse_empty_subdomain() { - assert_eq!( - ".example.com".parse::(), - Err(ParseHostError::EmptySubdomain) - ); + assert_eq!(".example.com".parse::(), Err(Error::EmptySubdomain)); } #[test] fn parse_illegal_whitespace() { assert_eq!( "exam\tple.com".parse::(), - Err(ParseHostError::IllegalWhitespace) + Err(Error::IllegalWhitespace) ); } diff --git a/src/lib.rs b/src/lib.rs index cc24ec2..6a4b595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ #![warn(clippy::all, clippy::pedantic)] -pub mod client; -pub mod fingerprint; -pub mod host; -pub mod request; -pub mod response; -pub mod server; -pub mod status; +mod certificate_store; +mod fingerprint; +mod host; +pub mod prelude; +mod receiver; +mod request; +mod response; +mod sender; +mod status; diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..8f57e98 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,13 @@ +pub use super::{ + certificate_store::CertificateStore, + fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, + host::{Error as ParseHostError, Host}, + //receiver, + request::{Error as ParseRequestError, Request}, + response::{Error as ParseResponseError, Response}, + sender::{Error as SenderError, Sender, Verifier}, + status::{ + AuthenticationFailure, Error as ParseStatusError, PermanentFailure, Redirect, Status, + TemporaryFailure, + }, +}; diff --git a/src/receiver/mod.rs b/src/receiver/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/receiver/mod.rs @@ -0,0 +1 @@ + diff --git a/src/request/error.rs b/src/request/error.rs new file mode 100644 index 0000000..9620f09 --- /dev/null +++ b/src/request/error.rs @@ -0,0 +1,36 @@ +use {crate::prelude::ParseHostError, std::fmt}; + +#[derive(Debug, PartialEq)] +/// Errors which can occur when parsing a request +pub enum Error { + MissingSeparator, + EmptyUser, + EmptyHost, + EmptyMessage, + Malformed, + ParseHostError(ParseHostError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseHostError(e) => write!(f, "{e}"), + _ => write!(f, "{self:?}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ParseHostError(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(value: ParseHostError) -> Self { + Self::ParseHostError(value) + } +} diff --git a/src/request.rs b/src/request/mod.rs similarity index 59% rename from src/request.rs rename to src/request/mod.rs index 606464f..27e0a57 100644 --- a/src/request.rs +++ b/src/request/mod.rs @@ -1,11 +1,22 @@ -use crate::host::{Host, ParseHostError}; +use crate::prelude::Host; use std::{fmt, str::FromStr}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +mod error; +pub use error::Error; + #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// The full request as sent by the `Sender` and received by the `Receiver` pub struct Request { - user: String, - host: Host, - message: String, + /// The username of the sender + pub user: String, + /// The fully qualified domain name of the sending server + pub host: Host, + /// The message body + pub message: String, } impl fmt::Display for Request { @@ -18,60 +29,26 @@ impl fmt::Display for Request { } } -#[derive(Clone, Debug, PartialEq)] -pub enum ParseRequestError { - MissingSeparator, - EmptyUser, - EmptyHost, - EmptyMessage, - Malformed, - ParseHostError(ParseHostError), -} - -impl fmt::Display for ParseRequestError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ParseHostError(e) => write!(f, "{e}"), - _ => write!(f, "{self:?}"), - } - } -} - -impl std::error::Error for ParseRequestError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::ParseHostError(e) => Some(e), - _ => None, - } - } -} - -impl From for ParseRequestError { - fn from(value: ParseHostError) -> Self { - Self::ParseHostError(value) - } -} - impl FromStr for Request { - type Err = ParseRequestError; + type Err = Error; fn from_str(s: &str) -> Result { if let Some((user, message)) = s.split_once(' ') { let Some(message) = message.strip_suffix("\r\n").map(ToString::to_string) else { - return Err(ParseRequestError::Malformed); + return Err(Error::Malformed); }; if message.is_empty() { - return Err(ParseRequestError::EmptyMessage); + return Err(Error::EmptyMessage); } if let Some((user, host)) = user.rsplit_once('@') { if host.is_empty() { - return Err(ParseRequestError::EmptyHost); + return Err(Error::EmptyHost); } else if user == "misfin://" { - return Err(ParseRequestError::EmptyUser); + return Err(Error::EmptyUser); } let host = host.parse()?; let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else { - return Err(ParseRequestError::Malformed); + return Err(Error::Malformed); }; Ok(Request { user, @@ -79,10 +56,10 @@ impl FromStr for Request { message, }) } else { - Err(ParseRequestError::MissingSeparator) + Err(Error::MissingSeparator) } } else { - Err(ParseRequestError::MissingSeparator) + Err(Error::MissingSeparator) } } } @@ -90,6 +67,7 @@ impl FromStr for Request { #[cfg(test)] mod tests { use super::*; + use crate::prelude::ParseHostError; const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n"; @@ -118,33 +96,33 @@ mod tests { #[test] fn parse_missing_sep() { let req = "misfin://john@example.comHelloWorld!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::MissingSeparator)); + assert_eq!(req, Err(Error::MissingSeparator)); } #[test] fn parse_malformed() { let req = "misfin://john@example.com Hello World!\n".parse::(); - assert_eq!(req, Err(ParseRequestError::Malformed)); + assert_eq!(req, Err(Error::Malformed)); let req = "mail://john@example.com Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::Malformed)); + assert_eq!(req, Err(Error::Malformed)); } #[test] fn parse_empty_user() { let req = "misfin://@example.com Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyUser)); + assert_eq!(req, Err(Error::EmptyUser)); } #[test] fn parse_empty_host() { let req = "misfin://john@ Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyHost)); + assert_eq!(req, Err(Error::EmptyHost)); } #[test] fn parse_empty_msg() { let req = "misfin://john@example.com \r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyMessage)); + assert_eq!(req, Err(Error::EmptyMessage)); } #[test] @@ -152,9 +130,7 @@ mod tests { let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::(); assert_eq!( req, - Err(ParseRequestError::ParseHostError( - ParseHostError::IllegalWhitespace - )) + Err(Error::ParseHostError(ParseHostError::IllegalWhitespace)) ); } } diff --git a/src/response.rs b/src/response.rs deleted file mode 100644 index 77821f4..0000000 --- a/src/response.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{fmt, num::ParseIntError, str::FromStr}; - -use crate::status::{ParseStatusError, Status}; - -#[derive(Clone, Debug, PartialEq)] -pub struct Response { - pub status: Status, - pub meta: String, -} - -impl fmt::Display for Response { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}\r\n", u8::from(self.status.clone()), self.meta) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ParseResponseError { - TooLong, - ParseInt(ParseIntError), - StatusError, - Malformed, -} - -impl fmt::Display for ParseResponseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TooLong => write!(f, "ParseResponseError: too long"), - Self::ParseInt(e) => write!(f, "ParseResponseError: {e}"), - Self::StatusError => write!(f, "ParseResponseError: Invalid Status"), - Self::Malformed => write!(f, "ParseResponseError: Malformed"), - } - } -} - -impl std::error::Error for ParseResponseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::ParseInt(e) => Some(e), - _ => None, - } - } -} - -impl From for ParseResponseError { - fn from(value: ParseIntError) -> Self { - Self::ParseInt(value) - } -} - -impl From for ParseResponseError { - fn from(_value: ParseStatusError) -> Self { - Self::StatusError - } -} - -impl FromStr for Response { - type Err = ParseResponseError; - - fn from_str(s: &str) -> Result { - if s.len() > 2048 { - return Err(ParseResponseError::TooLong); - } - if !s.ends_with("\r\n") { - return Err(ParseResponseError::Malformed); - } - let Some((status, meta)) = s.split_once(' ') else { - return Err(ParseResponseError::Malformed); - }; - let status: u8 = status.parse()?; - let status: Status = status.try_into()?; - Ok(Self { - status, - meta: meta.trim_end().to_string(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_success() { - let response: Response = "20 message delivered\r\n".parse().unwrap(); - assert_eq!(response.status, Status::Success); - assert_eq!(response.meta.as_str(), "message delivered"); - } - - #[test] - fn parse_badend() { - let response = "20 message delivered\n".parse::(); - assert_eq!(response, Err(ParseResponseError::Malformed)); - } - - #[test] - fn parse_badint() { - let response = "twenty message deliverred\r\n".parse::(); - match response { - Err(ParseResponseError::ParseInt(_)) => {} - _ => panic!(), - } - } -} diff --git a/src/response/error.rs b/src/response/error.rs new file mode 100644 index 0000000..5783cfa --- /dev/null +++ b/src/response/error.rs @@ -0,0 +1,51 @@ +use { + crate::prelude::ParseStatusError, + std::{fmt, num::ParseIntError}, +}; + +#[derive(Debug, PartialEq)] +/// Errors which can occur when parsing the response sent by the receving server +/// back to the sender +pub enum Error { + /// The message was too long. The Misfin spec allows a maximum length of + /// 2048 bytes for the message body. + TooLong, + /// An error occurred parsing the status code as a number. + ParseInt(ParseIntError), + /// The server sent an invalid or unrecognized status code + StatusError, + /// The response was malformed + Malformed, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TooLong => write!(f, "ParseResponseError: too long"), + Self::ParseInt(e) => write!(f, "ParseResponseError: {e}"), + Self::StatusError => write!(f, "ParseResponseError: Invalid Status"), + Self::Malformed => write!(f, "ParseResponseError: Malformed"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ParseInt(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(value: ParseIntError) -> Self { + Self::ParseInt(value) + } +} + +impl From for Error { + fn from(_value: ParseStatusError) -> Self { + Self::StatusError + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..fc5cef7 --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,71 @@ +use crate::prelude::Status; +use std::{fmt, str::FromStr}; + +mod error; +pub use error::Error; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Sent from the receiving server back to the sending server +pub struct Response { + pub status: Status, + pub meta: String, +} + +impl fmt::Display for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}\r\n", u8::from(self.status.clone()), self.meta) + } +} + +impl FromStr for Response { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.len() > 2048 { + return Err(Error::TooLong); + } + if !s.ends_with("\r\n") { + return Err(Error::Malformed); + } + let Some((status, meta)) = s.split_once(' ') else { + return Err(Error::Malformed); + }; + let status: u8 = status.parse()?; + let status: Status = status.try_into()?; + Ok(Self { + status, + meta: meta.trim_end().to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_success() { + let response: Response = "20 message delivered\r\n".parse().unwrap(); + assert_eq!(response.status, Status::Success); + assert_eq!(response.meta.as_str(), "message delivered"); + } + + #[test] + fn parse_badend() { + let response = "20 message delivered\n".parse::(); + assert_eq!(response, Err(Error::Malformed)); + } + + #[test] + fn parse_badint() { + let response = "twenty message deliverred\r\n".parse::(); + match response { + Err(Error::ParseInt(_)) => {} + _ => panic!(), + } + } +} diff --git a/src/sender/error.rs b/src/sender/error.rs new file mode 100644 index 0000000..13a9ce4 --- /dev/null +++ b/src/sender/error.rs @@ -0,0 +1,59 @@ +use { + crate::prelude::{ParseRequestError, ParseResponseError}, + std::{fmt, io}, +}; + +#[derive(Debug)] +/// Errors which might occur when sending a message +pub enum Error { + TlsError(rustls::Error), + RequestError(ParseRequestError), + ResponseError(ParseResponseError), + IoError(io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TlsError(e) => write!(f, "{e}"), + Self::RequestError(e) => write!(f, "{e}"), + Self::ResponseError(e) => write!(f, "{e}"), + Self::IoError(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::RequestError(e) => Some(e), + Self::ResponseError(e) => Some(e), + Self::TlsError(e) => Some(e), + Self::IoError(e) => Some(e), + } + } +} + +impl From for Error { + fn from(value: rustls::Error) -> Self { + Self::TlsError(value) + } +} + +impl From for Error { + fn from(value: ParseRequestError) -> Self { + Self::RequestError(value) + } +} + +impl From for Error { + fn from(value: ParseResponseError) -> Self { + Self::ResponseError(value) + } +} + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} diff --git a/src/sender/mod.rs b/src/sender/mod.rs new file mode 100644 index 0000000..69d9fbb --- /dev/null +++ b/src/sender/mod.rs @@ -0,0 +1,41 @@ +pub use self::{error::Error, verifier::Verifier}; +use { + crate::prelude::{CertificateStore, Request, Response}, + std::io::{Read, Write}, +}; + +mod error; +mod verifier; + +#[derive(Debug)] +/// Sends a piece of mail from the sending server to the receiving server +pub struct Sender +where + S: CertificateStore, + C: Sized, + T: Read + Write + Sized, +{ + /// The full message text to be sent + pub request: Request, + /// Verifies the receiving server's certificate + pub verifier: Verifier, + /// The TLS stream used for the connection + pub stream: rustls::StreamOwned, +} + +impl Sender +where + S: CertificateStore, + C: Sized, + T: Read + Write + Sized, +{ + pub fn new(request_str: &str, store: S) -> Result { + let request: Request = request_str.parse()?; + let verifier = Verifier::new(store); + unimplemented!(); + } + + pub fn send(&mut self) -> Result { + unimplemented!(); + } +} diff --git a/src/sender/verifier.rs b/src/sender/verifier.rs new file mode 100644 index 0000000..7e9c041 --- /dev/null +++ b/src/sender/verifier.rs @@ -0,0 +1,80 @@ +use { + crate::prelude::{CertificateStore, GetFingerprint}, + rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, + }, + std::{ + borrow::BorrowMut, + sync::{Arc, Mutex}, + time, + }, +}; + +#[derive(Debug)] +/// A verifier is used to verify certificates sent by the receiving server +/// during the tls handshake. +pub struct Verifier { + /// An item which serves as storage for certificates + pub store: Arc>, +} + +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &Certificate, + _intermediates: &[Certificate], + server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: time::SystemTime, + ) -> Result { + let fp = end_entity + .fingerprint() + .map_err(|e| rustls::Error::General(e.to_string()))?; + let name = match server_name { + rustls::ServerName::DnsName(n) => n.as_ref().to_string(), + rustls::ServerName::IpAddress(ip) => ip.to_string(), + _ => todo!(), + }; + let mut store = self.store.lock().unwrap(); + if let Some(fingerprint) = store.get_certificate(&name) { + // TODO: needs a lot more checking for certificate validity + if fingerprint == fp.fingerprint { + for n in fp.names { + if n == name { + return Ok(ServerCertVerified::assertion()); + } + } + Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidForName, + )) + } else { + Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidForName, + )) + } + } else { + if !store.contains_certificate(&name) { + let _key = store + .borrow_mut() + .insert_certificate(&name, &fp.fingerprint); + } + return Ok(ServerCertVerified::assertion()); + } + } +} + +impl From for Verifier { + fn from(value: T) -> Self { + Self { + store: Arc::new(Mutex::new(value)), + } + } +} + +impl Verifier { + pub fn new(store: T) -> Self { + store.into() + } +} diff --git a/src/server.rs b/src/server.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/status/error.rs b/src/status/error.rs new file mode 100644 index 0000000..ecb3e4c --- /dev/null +++ b/src/status/error.rs @@ -0,0 +1,13 @@ +use std::fmt; + +#[derive(Debug)] +/// The receiving server sent an unrecognized or invalid status code +pub struct Error; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} diff --git a/src/status.rs b/src/status/mod.rs similarity index 79% rename from src/status.rs rename to src/status/mod.rs index a0991d6..ca88b1d 100644 --- a/src/status.rs +++ b/src/status/mod.rs @@ -1,18 +1,14 @@ -use std::fmt; - -#[derive(Debug, Clone, PartialEq)] -pub struct ParseStatusError; - -impl fmt::Display for ParseStatusError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for ParseStatusError {} +mod error; +pub use error::Error; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[repr(u8)] +/// Status codes sent back to the sender representing how a receiving server has +/// processed a message. pub enum Status { /// These codes are reserved, and must not be sent by a Misfin server. Input = 10, @@ -46,7 +42,7 @@ impl From for u8 { } impl TryFrom for Status { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value / 10 { @@ -56,12 +52,14 @@ impl TryFrom for Status { 4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)), 5 => Ok(Self::PermanentFailure((value % 50).try_into()?)), 6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Status codes representing that a redirect is required pub enum Redirect { /// The mailbox has moved to a different address, and this message /// should be resent to that address. @@ -73,19 +71,22 @@ pub enum Redirect { } impl TryFrom for Redirect { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::Temporary), 1 => Ok(Self::Permanent), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Status codes representing that a temporary failure has occurred. The sending server should +/// retry sending the message. pub enum TemporaryFailure { /// The mailserver experienced a transient issue, and the message /// should be resent. @@ -105,7 +106,7 @@ pub enum TemporaryFailure { } impl TryFrom for TemporaryFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -116,23 +117,33 @@ impl TryFrom for TemporaryFailure { 4 => Ok(Self::RateLimit), 5 => Ok(Self::MailboxFull), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Status codes representing that a permanent failure has occurred and the sending +/// server should not resend the message. pub enum PermanentFailure { + /// Something is wrong with the mailserver, and you should not try to resend + /// your message. PermanentError = 0, + /// The mailbox you are trying to send to doesn't exist, and the mailserver + /// won't accept your message. MailboxNonexistent = 1, + /// The mailbox you are trying to send to existed once, but doesn't anymore. MailboxGone = 2, + /// This mailserver doesn't serve mail for the hostname you provided. DomainNotServiced = 3, + /// Your request is malformed, and won't be accepted by the mailserver. BadRequest = 9, Other, } impl TryFrom for PermanentFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -142,23 +153,35 @@ impl TryFrom for PermanentFailure { 3 => Ok(Self::DomainNotServiced), 9 => Ok(Self::BadRequest), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Status codes representing an authentication failure pub enum AuthenticationFailure { + /// This mailserver doesn't accept anonymous mail, and you need to repeat your + /// request with a certificate. CertificateRequired = 0, + /// Your certificate was validated, but you are not allowed to send mail to + /// that mailbox. UnauthorizedSender = 1, + /// Your certificate might be legitimate, but it has a problem - it is expired, + /// or it doesn't point to a valid Misfin identity, etc. CertificateInvalid = 2, + /// Your certificate matches an identity that the mailserver recognizes, but + /// the fingerprint has changed, so it is rejecting your message. IdentityMismatch = 3, + /// The mailserver needs you to complete a task to confirm that you are a + /// legitimate sender. (This is reserved for a Hashcash style anti-spam measure). ProofRequired = 4, Other, } impl TryFrom for AuthenticationFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -168,7 +191,7 @@ impl TryFrom for AuthenticationFailure { 3 => Ok(Self::IdentityMismatch), 4 => Ok(Self::ProofRequired), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } }