Finish refactor and add a number of doc comments

This commit is contained in:
Nathan Fisher 2023-05-24 13:14:14 -04:00
parent 02de655640
commit 4a40603efc
15 changed files with 193 additions and 155 deletions

View file

@ -1,16 +1,7 @@
use digest::Digest;
use rustls::Certificate;
use sha2::Sha256;
use std::fmt::{self, Write};
use x509_parser::prelude::*;
pub trait Fingerprint {
type Error;
fn fingerprint(&self) -> Result<(String, String), Self::Error>;
}
use {std::fmt, x509_parser::prelude::X509Error};
#[derive(Clone, Debug, PartialEq)]
/// Errors which can occur when fingerprinting a certificate
pub enum Error {
Fmt,
X509(X509Error),
@ -45,21 +36,3 @@ impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for Error {
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))
}
}

33
src/fingerprint/mod.rs Normal file
View file

@ -0,0 +1,33 @@
use digest::Digest;
use rustls::Certificate;
use sha2::Sha256;
use std::fmt::Write;
use x509_parser::prelude::*;
mod error;
pub use error::Error;
/// Creates an sha256 fingerprint for a certificate
pub trait Fingerprint {
type Error;
fn fingerprint(&self) -> Result<(String, String), Self::Error>;
}
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))
}
}

19
src/host/error.rs Normal file
View file

@ -0,0 +1,19 @@
use std::fmt;
#[derive(Clone, 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 {}

View file

@ -1,6 +1,9 @@
use std::{fmt, str::FromStr};
mod error;
pub use error::Error;
#[derive(Clone, Debug, Default, PartialEq)]
/// Represents the fully qualified domain name for this host
pub struct Host {
pub subdomain: Option<String>,
pub domain: String,
@ -17,40 +20,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<Self, Self::Err> {
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()),
@ -66,7 +52,7 @@ impl FromStr for Host {
})
}
} else {
Err(ParseHostError::MissingSeparator)
Err(Error::MissingSeparator)
}
}
}
@ -79,37 +65,31 @@ mod tests {
fn parse_missing_separator() {
assert_eq!(
"exampledotcom".parse::<Host>(),
Err(ParseHostError::MissingSeparator)
Err(Error::MissingSeparator)
);
}
#[test]
fn parse_empty_tld() {
assert_eq!("example.".parse::<Host>(), Err(ParseHostError::EmptyTld));
assert_eq!("example.".parse::<Host>(), Err(Error::EmptyTld));
}
#[test]
fn parse_empty_domain() {
assert_eq!(".com".parse::<Host>(), Err(ParseHostError::EmptyDomain));
assert_eq!(
"example..com".parse::<Host>(),
Err(ParseHostError::EmptyDomain)
);
assert_eq!(".com".parse::<Host>(), Err(Error::EmptyDomain));
assert_eq!("example..com".parse::<Host>(), Err(Error::EmptyDomain));
}
#[test]
fn parse_empty_subdomain() {
assert_eq!(
".example.com".parse::<Host>(),
Err(ParseHostError::EmptySubdomain)
);
assert_eq!(".example.com".parse::<Host>(), Err(Error::EmptySubdomain));
}
#[test]
fn parse_illegal_whitespace() {
assert_eq!(
"exam\tple.com".parse::<Host>(),
Err(ParseHostError::IllegalWhitespace)
Err(Error::IllegalWhitespace)
);
}

View file

@ -1,9 +1,9 @@
pub use super::{
fingerprint::{Fingerprint, Error as FingerprintError},
host::{Host, ParseHostError},
fingerprint::{Error as FingerprintError, Fingerprint},
host::{Error as ParseHostError, Host},
receiver,
response::{Response, Error as ParseResponseError},
request::{Request, ParseRequestError},
request::{Error as ParseRequestError, Request},
response::{Error as ParseResponseError, Response},
sender::{CertificateStore, Error as SenderError, Sender, Verifier},
status::*,
status::{Error as ParseStatusError, Status, Redirect, TemporaryFailure, PermanentFailure, AuthenticationFailure},
};

36
src/request/error.rs Normal file
View file

@ -0,0 +1,36 @@
use {crate::prelude::ParseHostError, std::fmt};
#[derive(Clone, 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<ParseHostError> for Error {
fn from(value: ParseHostError) -> Self {
Self::ParseHostError(value)
}
}

View file

@ -1,11 +1,18 @@
use crate::host::{Host, ParseHostError};
use crate::prelude::Host;
use std::{fmt, str::FromStr};
mod error;
pub use error::Error;
#[derive(Clone, Debug, PartialEq)]
/// 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 +25,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<ParseHostError> 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<Self, Self::Err> {
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 +52,10 @@ impl FromStr for Request {
message,
})
} else {
Err(ParseRequestError::MissingSeparator)
Err(Error::MissingSeparator)
}
} else {
Err(ParseRequestError::MissingSeparator)
Err(Error::MissingSeparator)
}
}
}
@ -90,6 +63,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 +92,33 @@ mod tests {
#[test]
fn parse_missing_sep() {
let req = "misfin://john@example.comHelloWorld!\r\n".parse::<Request>();
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::<Request>();
assert_eq!(req, Err(ParseRequestError::Malformed));
assert_eq!(req, Err(Error::Malformed));
let req = "mail://john@example.com Hello World!\r\n".parse::<Request>();
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::<Request>();
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::<Request>();
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::<Request>();
assert_eq!(req, Err(ParseRequestError::EmptyMessage));
assert_eq!(req, Err(Error::EmptyMessage));
}
#[test]
@ -152,9 +126,7 @@ mod tests {
let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::<Request>();
assert_eq!(
req,
Err(ParseRequestError::ParseHostError(
ParseHostError::IllegalWhitespace
))
Err(Error::ParseHostError(ParseHostError::IllegalWhitespace))
);
}
}

View file

@ -4,10 +4,17 @@ use {
};
#[derive(Clone, 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,
}
@ -42,4 +49,3 @@ impl From<ParseStatusError> for Error {
Self::StatusError
}
}

View file

@ -1,10 +1,11 @@
use std::{fmt, str::FromStr};
use crate::prelude::Status;
use std::{fmt, str::FromStr};
mod error;
pub use error::Error;
#[derive(Clone, Debug, PartialEq)]
/// Sent from the receiving server back to the sending server
pub struct Response {
pub status: Status,
pub meta: String,

View file

@ -4,6 +4,7 @@ use {
};
#[derive(Debug)]
/// Errors which might occur when sending a message
pub enum Error {
TlsError(rustls::Error),
RequestError(ParseRequestError),
@ -56,4 +57,3 @@ impl From<io::Error> for Error {
Self::IoError(value)
}
}

View file

@ -1,22 +1,26 @@
use crate::{
request::Request,
response::Response,
pub use self::{
error::Error,
verifier::{CertificateStore, Verifier},
};
use crate::{request::Request, response::Response};
use std::io::{Read, Write};
pub use self::{error::Error, verifier::{CertificateStore, Verifier}};
mod error;
mod verifier;
#[derive(Debug)]
/// Sends a piece of mail from the sending server to the receiving server
pub struct Sender<S, C, T>
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<S>,
/// The TLS stream used for the connection
pub stream: rustls::StreamOwned<C, T>,
}

View file

@ -5,14 +5,18 @@ use rustls::{
};
use std::sync::{Arc, Mutex};
/// An item which stores known certificates
pub trait CertificateStore: Send + Sync {
fn get(&self, host: &str) -> Option<String>;
fn insert(&mut self, host: &str, fingerprint: &str);
}
#[derive(Debug)]
/// A verifier is used to verify certificates sent by the receiving server
/// during the tls handshake.
pub struct Verifier<S: CertificateStore> {
store: Arc<Mutex<S>>,
/// An item which serves as storage for certificates
pub store: Arc<Mutex<S>>,
}
impl<S: CertificateStore> ServerCertVerifier for Verifier<S> {

12
src/status/error.rs Normal file
View file

@ -0,0 +1,12 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
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 {}

View file

@ -1,18 +1,10 @@
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;
#[derive(Debug, Clone, PartialEq)]
#[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 +38,7 @@ impl From<Status> for u8 {
}
impl TryFrom<u8> for Status {
type Error = ParseStatusError;
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value / 10 {
@ -56,12 +48,13 @@ impl TryFrom<u8> 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)]
/// 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 +66,21 @@ pub enum Redirect {
}
impl TryFrom<u8> for Redirect {
type Error = ParseStatusError;
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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)]
/// 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 +100,7 @@ pub enum TemporaryFailure {
}
impl TryFrom<u8> for TemporaryFailure {
type Error = ParseStatusError;
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
@ -116,12 +111,14 @@ impl TryFrom<u8> 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)]
/// Status codes representing that a permanent failure has occurred and the sending server should
/// not resend the message.
pub enum PermanentFailure {
PermanentError = 0,
MailboxNonexistent = 1,
@ -132,7 +129,7 @@ pub enum PermanentFailure {
}
impl TryFrom<u8> for PermanentFailure {
type Error = ParseStatusError;
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
@ -142,12 +139,13 @@ impl TryFrom<u8> 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)]
/// Status codes representing an authentication failure
pub enum AuthenticationFailure {
CertificateRequired = 0,
UnauthorizedSender = 1,
@ -158,7 +156,7 @@ pub enum AuthenticationFailure {
}
impl TryFrom<u8> for AuthenticationFailure {
type Error = ParseStatusError;
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
@ -168,7 +166,7 @@ impl TryFrom<u8> for AuthenticationFailure {
3 => Ok(Self::IdentityMismatch),
4 => Ok(Self::ProofRequired),
n if n < 10 => Ok(Self::Other),
_ => Err(ParseStatusError),
_ => Err(Error),
}
}
}