Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/dory into odin
This commit is contained in:
commit
c57bb83c5c
25 changed files with 626 additions and 337 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
tags
|
tags
|
||||||
tags.lock
|
tags.lock
|
||||||
|
tags.temp
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,8 @@ x509-parser = "0.15.0"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
features = [ "dangerous_configuration" ]
|
features = [ "dangerous_configuration" ]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0"
|
||||||
|
features = ["derive"]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
|
36
src/certificate_store.rs
Normal file
36
src/certificate_store.rs
Normal file
|
@ -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<String>;
|
||||||
|
fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option<String>;
|
||||||
|
fn contains_certificate(&self, host: &str) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertificateStore for HashMap<String, String> {
|
||||||
|
fn get_certificate(&self, host: &str) -> Option<String> {
|
||||||
|
self.get(host).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option<String> {
|
||||||
|
self.insert(host.to_string(), fingerprint.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_certificate(&self, host: &str) -> bool {
|
||||||
|
self.contains_key(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertificateStore for BTreeMap<String, String> {
|
||||||
|
fn get_certificate(&self, host: &str) -> Option<String> {
|
||||||
|
self.get(host).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_certificate(&mut self, host: &str, fingerprint: &str) -> Option<String> {
|
||||||
|
self.insert(host.to_string(), fingerprint.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_certificate(&self, host: &str) -> bool {
|
||||||
|
self.contains_key(host)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod store;
|
|
||||||
pub mod verifier;
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub trait CertificateStore {
|
|
||||||
fn get(&self, host: &str) -> Option<String>;
|
|
||||||
fn insert(&mut self, host: &str, fingerprint: &str);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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<Item = &[u8]>,
|
|
||||||
_ocsp_response: &[u8],
|
|
||||||
_now: std::time::SystemTime,
|
|
||||||
) -> Result<ServerCertVerified, rustls::Error> {
|
|
||||||
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<Mutex<T>> for interior mutability
|
|
||||||
}
|
|
||||||
return Err(rustls::Error::General("Unrecognized certificate".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<fmt::Error> for Error {
|
|
||||||
fn from(_value: fmt::Error) -> Self {
|
|
||||||
Self::Fmt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for Error {
|
|
||||||
fn from(value: x509_parser::nom::Err<x509_parser::error::X509Error>) -> 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
40
src/fingerprint/error.rs
Normal file
40
src/fingerprint/error.rs
Normal file
|
@ -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<fmt::Error> for Error {
|
||||||
|
fn from(_value: fmt::Error) -> Self {
|
||||||
|
Self::Fmt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for Error {
|
||||||
|
fn from(value: x509_parser::nom::Err<x509_parser::error::X509Error>) -> Self {
|
||||||
|
Self::X509(value.into())
|
||||||
|
}
|
||||||
|
}
|
54
src/fingerprint/mod.rs
Normal file
54
src/fingerprint/mod.rs
Normal file
|
@ -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<Fingerprint, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Fingerprint {
|
||||||
|
pub names: Vec<String>,
|
||||||
|
pub fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetFingerprint for Certificate {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn fingerprint(&self) -> Result<Fingerprint, Self::Error> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
19
src/host/error.rs
Normal file
19
src/host/error.rs
Normal file
|
@ -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 {}
|
|
@ -12,8 +12,15 @@
|
||||||
//! assert_eq!(host.tld.as_str(), "com");
|
//! assert_eq!(host.tld.as_str(), "com");
|
||||||
//! ```
|
//! ```
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[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 struct Host {
|
||||||
pub subdomain: Option<String>,
|
pub subdomain: Option<String>,
|
||||||
pub domain: String,
|
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 {
|
impl FromStr for Host {
|
||||||
type Err = ParseHostError;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s.contains(char::is_whitespace) {
|
if s.contains(char::is_whitespace) {
|
||||||
return Err(ParseHostError::IllegalWhitespace);
|
return Err(Error::IllegalWhitespace);
|
||||||
}
|
}
|
||||||
if let Some((domain, tld)) = s.rsplit_once('.') {
|
if let Some((domain, tld)) = s.rsplit_once('.') {
|
||||||
if domain.is_empty() {
|
if domain.is_empty() {
|
||||||
Err(ParseHostError::EmptyDomain)
|
Err(Error::EmptyDomain)
|
||||||
} else if tld.is_empty() {
|
} else if tld.is_empty() {
|
||||||
Err(ParseHostError::EmptyTld)
|
Err(Error::EmptyTld)
|
||||||
} else if let Some((subdomain, domain)) = domain.rsplit_once('.') {
|
} else if let Some((subdomain, domain)) = domain.rsplit_once('.') {
|
||||||
if subdomain.is_empty() {
|
if subdomain.is_empty() {
|
||||||
Err(ParseHostError::EmptySubdomain)
|
Err(Error::EmptySubdomain)
|
||||||
} else if domain.is_empty() {
|
} else if domain.is_empty() {
|
||||||
Err(ParseHostError::EmptyDomain)
|
Err(Error::EmptyDomain)
|
||||||
} else {
|
} else {
|
||||||
Ok(Host {
|
Ok(Host {
|
||||||
subdomain: Some(subdomain.to_string()),
|
subdomain: Some(subdomain.to_string()),
|
||||||
|
@ -79,7 +69,7 @@ impl FromStr for Host {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ParseHostError::MissingSeparator)
|
Err(Error::MissingSeparator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,37 +82,31 @@ mod tests {
|
||||||
fn parse_missing_separator() {
|
fn parse_missing_separator() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"exampledotcom".parse::<Host>(),
|
"exampledotcom".parse::<Host>(),
|
||||||
Err(ParseHostError::MissingSeparator)
|
Err(Error::MissingSeparator)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_tld() {
|
fn parse_empty_tld() {
|
||||||
assert_eq!("example.".parse::<Host>(), Err(ParseHostError::EmptyTld));
|
assert_eq!("example.".parse::<Host>(), Err(Error::EmptyTld));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_domain() {
|
fn parse_empty_domain() {
|
||||||
assert_eq!(".com".parse::<Host>(), Err(ParseHostError::EmptyDomain));
|
assert_eq!(".com".parse::<Host>(), Err(Error::EmptyDomain));
|
||||||
assert_eq!(
|
assert_eq!("example..com".parse::<Host>(), Err(Error::EmptyDomain));
|
||||||
"example..com".parse::<Host>(),
|
|
||||||
Err(ParseHostError::EmptyDomain)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_subdomain() {
|
fn parse_empty_subdomain() {
|
||||||
assert_eq!(
|
assert_eq!(".example.com".parse::<Host>(), Err(Error::EmptySubdomain));
|
||||||
".example.com".parse::<Host>(),
|
|
||||||
Err(ParseHostError::EmptySubdomain)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_illegal_whitespace() {
|
fn parse_illegal_whitespace() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"exam\tple.com".parse::<Host>(),
|
"exam\tple.com".parse::<Host>(),
|
||||||
Err(ParseHostError::IllegalWhitespace)
|
Err(Error::IllegalWhitespace)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -1,8 +1,10 @@
|
||||||
#![warn(clippy::all, clippy::pedantic)]
|
#![warn(clippy::all, clippy::pedantic)]
|
||||||
pub mod client;
|
mod certificate_store;
|
||||||
pub mod fingerprint;
|
mod fingerprint;
|
||||||
pub mod host;
|
mod host;
|
||||||
pub mod request;
|
pub mod prelude;
|
||||||
pub mod response;
|
mod receiver;
|
||||||
pub mod server;
|
mod request;
|
||||||
pub mod status;
|
mod response;
|
||||||
|
mod sender;
|
||||||
|
mod status;
|
||||||
|
|
13
src/prelude.rs
Normal file
13
src/prelude.rs
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
1
src/receiver/mod.rs
Normal file
1
src/receiver/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
36
src/request/error.rs
Normal file
36
src/request/error.rs
Normal file
|
@ -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<ParseHostError> for Error {
|
||||||
|
fn from(value: ParseHostError) -> Self {
|
||||||
|
Self::ParseHostError(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,22 @@
|
||||||
use crate::host::{Host, ParseHostError};
|
use crate::prelude::Host;
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[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 {
|
pub struct Request {
|
||||||
user: String,
|
/// The username of the sender
|
||||||
host: Host,
|
pub user: String,
|
||||||
message: String,
|
/// The fully qualified domain name of the sending server
|
||||||
|
pub host: Host,
|
||||||
|
/// The message body
|
||||||
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Request {
|
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<ParseHostError> for ParseRequestError {
|
|
||||||
fn from(value: ParseHostError) -> Self {
|
|
||||||
Self::ParseHostError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Request {
|
impl FromStr for Request {
|
||||||
type Err = ParseRequestError;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if let Some((user, message)) = s.split_once(' ') {
|
if let Some((user, message)) = s.split_once(' ') {
|
||||||
let Some(message) = message.strip_suffix("\r\n").map(ToString::to_string) else {
|
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() {
|
if message.is_empty() {
|
||||||
return Err(ParseRequestError::EmptyMessage);
|
return Err(Error::EmptyMessage);
|
||||||
}
|
}
|
||||||
if let Some((user, host)) = user.rsplit_once('@') {
|
if let Some((user, host)) = user.rsplit_once('@') {
|
||||||
if host.is_empty() {
|
if host.is_empty() {
|
||||||
return Err(ParseRequestError::EmptyHost);
|
return Err(Error::EmptyHost);
|
||||||
} else if user == "misfin://" {
|
} else if user == "misfin://" {
|
||||||
return Err(ParseRequestError::EmptyUser);
|
return Err(Error::EmptyUser);
|
||||||
}
|
}
|
||||||
let host = host.parse()?;
|
let host = host.parse()?;
|
||||||
let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else {
|
let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else {
|
||||||
return Err(ParseRequestError::Malformed);
|
return Err(Error::Malformed);
|
||||||
};
|
};
|
||||||
Ok(Request {
|
Ok(Request {
|
||||||
user,
|
user,
|
||||||
|
@ -79,10 +56,10 @@ impl FromStr for Request {
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(ParseRequestError::MissingSeparator)
|
Err(Error::MissingSeparator)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ParseRequestError::MissingSeparator)
|
Err(Error::MissingSeparator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +67,7 @@ impl FromStr for Request {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::prelude::ParseHostError;
|
||||||
|
|
||||||
const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n";
|
const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n";
|
||||||
|
|
||||||
|
@ -118,33 +96,33 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_missing_sep() {
|
fn parse_missing_sep() {
|
||||||
let req = "misfin://john@example.comHelloWorld!\r\n".parse::<Request>();
|
let req = "misfin://john@example.comHelloWorld!\r\n".parse::<Request>();
|
||||||
assert_eq!(req, Err(ParseRequestError::MissingSeparator));
|
assert_eq!(req, Err(Error::MissingSeparator));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_malformed() {
|
fn parse_malformed() {
|
||||||
let req = "misfin://john@example.com Hello World!\n".parse::<Request>();
|
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>();
|
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]
|
#[test]
|
||||||
fn parse_empty_user() {
|
fn parse_empty_user() {
|
||||||
let req = "misfin://@example.com Hello World!\r\n".parse::<Request>();
|
let req = "misfin://@example.com Hello World!\r\n".parse::<Request>();
|
||||||
assert_eq!(req, Err(ParseRequestError::EmptyUser));
|
assert_eq!(req, Err(Error::EmptyUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_host() {
|
fn parse_empty_host() {
|
||||||
let req = "misfin://john@ Hello World!\r\n".parse::<Request>();
|
let req = "misfin://john@ Hello World!\r\n".parse::<Request>();
|
||||||
assert_eq!(req, Err(ParseRequestError::EmptyHost));
|
assert_eq!(req, Err(Error::EmptyHost));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_msg() {
|
fn parse_empty_msg() {
|
||||||
let req = "misfin://john@example.com \r\n".parse::<Request>();
|
let req = "misfin://john@example.com \r\n".parse::<Request>();
|
||||||
assert_eq!(req, Err(ParseRequestError::EmptyMessage));
|
assert_eq!(req, Err(Error::EmptyMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -152,9 +130,7 @@ mod tests {
|
||||||
let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::<Request>();
|
let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::<Request>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
req,
|
req,
|
||||||
Err(ParseRequestError::ParseHostError(
|
Err(Error::ParseHostError(ParseHostError::IllegalWhitespace))
|
||||||
ParseHostError::IllegalWhitespace
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
104
src/response.rs
104
src/response.rs
|
@ -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<ParseIntError> for ParseResponseError {
|
|
||||||
fn from(value: ParseIntError) -> Self {
|
|
||||||
Self::ParseInt(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParseStatusError> for ParseResponseError {
|
|
||||||
fn from(_value: ParseStatusError) -> Self {
|
|
||||||
Self::StatusError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Response {
|
|
||||||
type Err = ParseResponseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
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::<Response>();
|
|
||||||
assert_eq!(response, Err(ParseResponseError::Malformed));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_badint() {
|
|
||||||
let response = "twenty message deliverred\r\n".parse::<Response>();
|
|
||||||
match response {
|
|
||||||
Err(ParseResponseError::ParseInt(_)) => {}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
51
src/response/error.rs
Normal file
51
src/response/error.rs
Normal file
|
@ -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<ParseIntError> for Error {
|
||||||
|
fn from(value: ParseIntError) -> Self {
|
||||||
|
Self::ParseInt(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseStatusError> for Error {
|
||||||
|
fn from(_value: ParseStatusError) -> Self {
|
||||||
|
Self::StatusError
|
||||||
|
}
|
||||||
|
}
|
71
src/response/mod.rs
Normal file
71
src/response/mod.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||||
|
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::<Response>();
|
||||||
|
assert_eq!(response, Err(Error::Malformed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_badint() {
|
||||||
|
let response = "twenty message deliverred\r\n".parse::<Response>();
|
||||||
|
match response {
|
||||||
|
Err(Error::ParseInt(_)) => {}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/sender/error.rs
Normal file
59
src/sender/error.rs
Normal file
|
@ -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<rustls::Error> for Error {
|
||||||
|
fn from(value: rustls::Error) -> Self {
|
||||||
|
Self::TlsError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseRequestError> for Error {
|
||||||
|
fn from(value: ParseRequestError) -> Self {
|
||||||
|
Self::RequestError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseResponseError> for Error {
|
||||||
|
fn from(value: ParseResponseError) -> Self {
|
||||||
|
Self::ResponseError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
Self::IoError(value)
|
||||||
|
}
|
||||||
|
}
|
41
src/sender/mod.rs
Normal file
41
src/sender/mod.rs
Normal file
|
@ -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<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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, C, T> Sender<S, C, T>
|
||||||
|
where
|
||||||
|
S: CertificateStore,
|
||||||
|
C: Sized,
|
||||||
|
T: Read + Write + Sized,
|
||||||
|
{
|
||||||
|
pub fn new(request_str: &str, store: S) -> Result<Self, Error> {
|
||||||
|
let request: Request = request_str.parse()?;
|
||||||
|
let verifier = Verifier::new(store);
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self) -> Result<Response, Error> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
}
|
80
src/sender/verifier.rs
Normal file
80
src/sender/verifier.rs
Normal file
|
@ -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<S: CertificateStore> {
|
||||||
|
/// An item which serves as storage for certificates
|
||||||
|
pub store: Arc<Mutex<S>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: CertificateStore> ServerCertVerifier for Verifier<S> {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
end_entity: &Certificate,
|
||||||
|
_intermediates: &[Certificate],
|
||||||
|
server_name: &rustls::ServerName,
|
||||||
|
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||||
|
_ocsp_response: &[u8],
|
||||||
|
_now: time::SystemTime,
|
||||||
|
) -> Result<ServerCertVerified, rustls::Error> {
|
||||||
|
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<T: CertificateStore> From<T> for Verifier<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
store: Arc::new(Mutex::new(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: CertificateStore> Verifier<T> {
|
||||||
|
pub fn new(store: T) -> Self {
|
||||||
|
store.into()
|
||||||
|
}
|
||||||
|
}
|
13
src/status/error.rs
Normal file
13
src/status/error.rs
Normal file
|
@ -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 {}
|
|
@ -1,18 +1,14 @@
|
||||||
use std::fmt;
|
mod error;
|
||||||
|
pub use error::Error;
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct ParseStatusError;
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
impl fmt::Display for ParseStatusError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{self:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for ParseStatusError {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
/// Status codes sent back to the sender representing how a receiving server has
|
||||||
|
/// processed a message.
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
/// These codes are reserved, and must not be sent by a Misfin server.
|
/// These codes are reserved, and must not be sent by a Misfin server.
|
||||||
Input = 10,
|
Input = 10,
|
||||||
|
@ -46,7 +42,7 @@ impl From<Status> for u8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Status {
|
impl TryFrom<u8> for Status {
|
||||||
type Error = ParseStatusError;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value / 10 {
|
match value / 10 {
|
||||||
|
@ -56,12 +52,14 @@ impl TryFrom<u8> for Status {
|
||||||
4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)),
|
4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)),
|
||||||
5 => Ok(Self::PermanentFailure((value % 50).try_into()?)),
|
5 => Ok(Self::PermanentFailure((value % 50).try_into()?)),
|
||||||
6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)),
|
6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)),
|
||||||
_ => Err(ParseStatusError),
|
_ => Err(Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
/// Status codes representing that a redirect is required
|
||||||
pub enum Redirect {
|
pub enum Redirect {
|
||||||
/// The mailbox has moved to a different address, and this message
|
/// The mailbox has moved to a different address, and this message
|
||||||
/// should be resent to that address.
|
/// should be resent to that address.
|
||||||
|
@ -73,19 +71,22 @@ pub enum Redirect {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Redirect {
|
impl TryFrom<u8> for Redirect {
|
||||||
type Error = ParseStatusError;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
0 => Ok(Self::Temporary),
|
0 => Ok(Self::Temporary),
|
||||||
1 => Ok(Self::Permanent),
|
1 => Ok(Self::Permanent),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(ParseStatusError),
|
_ => Err(Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[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 {
|
pub enum TemporaryFailure {
|
||||||
/// The mailserver experienced a transient issue, and the message
|
/// The mailserver experienced a transient issue, and the message
|
||||||
/// should be resent.
|
/// should be resent.
|
||||||
|
@ -105,7 +106,7 @@ pub enum TemporaryFailure {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for TemporaryFailure {
|
impl TryFrom<u8> for TemporaryFailure {
|
||||||
type Error = ParseStatusError;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -116,23 +117,33 @@ impl TryFrom<u8> for TemporaryFailure {
|
||||||
4 => Ok(Self::RateLimit),
|
4 => Ok(Self::RateLimit),
|
||||||
5 => Ok(Self::MailboxFull),
|
5 => Ok(Self::MailboxFull),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(ParseStatusError),
|
_ => Err(Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[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 {
|
pub enum PermanentFailure {
|
||||||
|
/// Something is wrong with the mailserver, and you should not try to resend
|
||||||
|
/// your message.
|
||||||
PermanentError = 0,
|
PermanentError = 0,
|
||||||
|
/// The mailbox you are trying to send to doesn't exist, and the mailserver
|
||||||
|
/// won't accept your message.
|
||||||
MailboxNonexistent = 1,
|
MailboxNonexistent = 1,
|
||||||
|
/// The mailbox you are trying to send to existed once, but doesn't anymore.
|
||||||
MailboxGone = 2,
|
MailboxGone = 2,
|
||||||
|
/// This mailserver doesn't serve mail for the hostname you provided.
|
||||||
DomainNotServiced = 3,
|
DomainNotServiced = 3,
|
||||||
|
/// Your request is malformed, and won't be accepted by the mailserver.
|
||||||
BadRequest = 9,
|
BadRequest = 9,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for PermanentFailure {
|
impl TryFrom<u8> for PermanentFailure {
|
||||||
type Error = ParseStatusError;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -142,23 +153,35 @@ impl TryFrom<u8> for PermanentFailure {
|
||||||
3 => Ok(Self::DomainNotServiced),
|
3 => Ok(Self::DomainNotServiced),
|
||||||
9 => Ok(Self::BadRequest),
|
9 => Ok(Self::BadRequest),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(ParseStatusError),
|
_ => Err(Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
/// Status codes representing an authentication failure
|
||||||
pub enum AuthenticationFailure {
|
pub enum AuthenticationFailure {
|
||||||
|
/// This mailserver doesn't accept anonymous mail, and you need to repeat your
|
||||||
|
/// request with a certificate.
|
||||||
CertificateRequired = 0,
|
CertificateRequired = 0,
|
||||||
|
/// Your certificate was validated, but you are not allowed to send mail to
|
||||||
|
/// that mailbox.
|
||||||
UnauthorizedSender = 1,
|
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,
|
CertificateInvalid = 2,
|
||||||
|
/// Your certificate matches an identity that the mailserver recognizes, but
|
||||||
|
/// the fingerprint has changed, so it is rejecting your message.
|
||||||
IdentityMismatch = 3,
|
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,
|
ProofRequired = 4,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for AuthenticationFailure {
|
impl TryFrom<u8> for AuthenticationFailure {
|
||||||
type Error = ParseStatusError;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -168,7 +191,7 @@ impl TryFrom<u8> for AuthenticationFailure {
|
||||||
3 => Ok(Self::IdentityMismatch),
|
3 => Ok(Self::IdentityMismatch),
|
||||||
4 => Ok(Self::ProofRequired),
|
4 => Ok(Self::ProofRequired),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(ParseStatusError),
|
_ => Err(Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue