use std::path::PathBuf;
use std::time::Duration;
use weida_core::{Error, Fingerprint, Limits};
use weida_protocol::header::GuaranteeSet;
use weida_runtime::SharedResolver;
use crate::identity::{IdentitySource, TrustSource};
use crate::reconnect::{OutboxFull, ReconnectPolicy};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Discovery {
#[default]
Aware,
Single,
}
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
pub limits: Limits,
pub max_connections: usize,
pub max_connections_per_peer: usize,
pub endpoint_queue: usize,
pub max_resolved_addresses: usize,
pub worker_threads: usize,
pub shutdown_timeout: Duration,
pub guarantees: GuaranteeSet,
pub discovery: Discovery,
pub resolver: SharedResolver,
pub connect_attempt_timeout: Duration,
pub reconnect: ReconnectPolicy,
pub send_timeout: Option<Duration>,
pub outbox_messages: usize,
pub outbox_bytes: usize,
pub outbox_full: OutboxFull,
}
impl Default for RuntimeConfig {
fn default() -> Self {
RuntimeConfig {
limits: Limits::default(),
max_connections_per_peer: 64,
max_connections: 1024,
endpoint_queue: 256,
max_resolved_addresses: 8,
worker_threads: 1,
shutdown_timeout: Duration::from_secs(1),
guarantees: GuaranteeSet::CORE,
connect_attempt_timeout: Duration::from_millis(250),
discovery: Discovery::default(),
resolver: SharedResolver::default(),
reconnect: ReconnectPolicy::default(),
send_timeout: None,
outbox_messages: 1000,
outbox_full: OutboxFull::default(),
outbox_bytes: 8 * 1024 * 1024,
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum Pem {
Bytes(Vec<u8>),
File(PathBuf),
}
impl Pem {
pub(crate) fn describe(&self) -> String {
match self {
Pem::Bytes(_) => "in-memory PEM".to_owned(),
Pem::File(path) => path.display().to_string(),
}
}
}
impl std::fmt::Debug for Pem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Pem::Bytes(b) => write!(f, "Pem::Bytes({} bytes)", b.len()),
Pem::File(p) => f.debug_tuple("Pem::File").field(p).finish(),
}
}
}
impl From<PathBuf> for Pem {
fn from(path: PathBuf) -> Pem {
Pem::File(path)
}
}
#[cfg_attr(
feature = "generate",
doc = "Generated identities ([`Identity::generate`]) are self-signed and made for"
)]
#[cfg_attr(
not(feature = "generate"),
doc = "Generated identities (`Identity::generate`, behind the `generate` feature) are self-signed and made for"
)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Identity {
pub cert_chain: Pem,
pub key: Pem,
}
impl Identity {
#[cfg(feature = "generate")]
pub fn generate() -> Result<Identity, Error> {
Identity::generate_for(Vec::<String>::new())
}
#[cfg(feature = "generate")]
pub fn generate_for(
names: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Identity, Error> {
let names: Vec<String> = names.into_iter().map(Into::into).collect();
let generated = rcgen::generate_simple_self_signed(names)
.map_err(|e| Error::Tls(format!("generating an identity: {e}")))?;
Ok(Identity {
cert_chain: Pem::Bytes(generated.cert.pem().into_bytes()),
key: Pem::Bytes(generated.signing_key.serialize_pem().into_bytes()),
})
}
pub fn from_pem(cert_chain: impl Into<Vec<u8>>, key: impl Into<Vec<u8>>) -> Identity {
Identity {
cert_chain: Pem::Bytes(cert_chain.into()),
key: Pem::Bytes(key.into()),
}
}
pub fn from_pem_files(cert_chain: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Identity {
Identity {
cert_chain: Pem::File(cert_chain.into()),
key: Pem::File(key.into()),
}
}
pub fn from_pem_file(path: impl Into<PathBuf>) -> Identity {
let path = path.into();
Identity {
cert_chain: Pem::File(path.clone()),
key: Pem::File(path),
}
}
pub fn fingerprint(&self) -> Result<Fingerprint, Error> {
let chain = crate::tls::certs_from(&self.cert_chain)?;
crate::tls::spki_fingerprint(&chain[0])
}
pub fn certificate_pem(&self) -> Result<String, Error> {
crate::tls::certificate_pem(&self.cert_chain)
}
pub fn to_pem(&self) -> Result<String, Error> {
let mut out = self.certificate_pem()?;
out.push_str(&crate::tls::key_pem(&self.key)?);
Ok(out)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Trust {
pub anchors: Vec<Pem>,
pub pins: Vec<Fingerprint>,
}
impl Trust {
pub fn by_address() -> Trust {
Trust::default()
}
pub fn pin(fingerprint: Fingerprint) -> Trust {
Trust::default().and_pin(fingerprint)
}
pub fn anchor(pem: impl Into<Vec<u8>>) -> Trust {
Trust::default().and_anchor(pem)
}
pub fn anchor_file(path: impl Into<PathBuf>) -> Trust {
Trust::default().and_anchor_file(path)
}
pub fn and_pin(mut self, fingerprint: Fingerprint) -> Trust {
if !self.pins.contains(&fingerprint) {
self.pins.push(fingerprint);
}
self
}
pub fn and_anchor(mut self, pem: impl Into<Vec<u8>>) -> Trust {
self.anchors.push(Pem::Bytes(pem.into()));
self
}
pub fn and_anchor_file(mut self, path: impl Into<PathBuf>) -> Trust {
self.anchors.push(Pem::File(path.into()));
self
}
pub fn is_empty(&self) -> bool {
self.anchors.is_empty() && self.pins.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ClientTls {
pub trust: TrustSource,
pub identity: Option<IdentitySource>,
}
impl ClientTls {
pub fn new(trust: impl Into<TrustSource>) -> ClientTls {
ClientTls {
trust: trust.into(),
identity: None,
}
}
pub fn with_identity(mut self, identity: impl Into<IdentitySource>) -> ClientTls {
self.identity = Some(identity.into());
self
}
}
impl From<Trust> for ClientTls {
fn from(trust: Trust) -> ClientTls {
ClientTls::new(trust)
}
}
impl From<TrustSource> for ClientTls {
fn from(trust: TrustSource) -> ClientTls {
ClientTls::new(trust)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ServerTls {
pub identity: IdentitySource,
pub client_trust: Option<TrustSource>,
}
impl ServerTls {
pub fn new(identity: impl Into<IdentitySource>) -> ServerTls {
ServerTls {
identity: identity.into(),
client_trust: None,
}
}
pub fn require_client(mut self, trust: impl Into<TrustSource>) -> ServerTls {
self.client_trust = Some(trust.into());
self
}
}
impl From<Identity> for ServerTls {
fn from(identity: Identity) -> ServerTls {
ServerTls::new(identity)
}
}
impl From<IdentitySource> for ServerTls {
fn from(identity: IdentitySource) -> ServerTls {
ServerTls::new(identity)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_the_protocol_document() {
let c = RuntimeConfig::default();
assert_eq!(c.limits, Limits::default());
assert_eq!(c.limits.keep_alive, Duration::from_secs(10));
assert_eq!(c.limits.idle_timeout, Duration::from_secs(30));
assert_eq!(c.max_connections, 1024);
assert_eq!(c.max_connections_per_peer, 64);
assert_eq!(c.endpoint_queue, 256);
assert_eq!(c.max_resolved_addresses, 8);
}
#[test]
fn a_pem_source_never_prints_its_bytes() {
assert_eq!(Pem::File("/tmp/ca.pem".into()).describe(), "/tmp/ca.pem");
assert_eq!(Pem::Bytes(b"secret".to_vec()).describe(), "in-memory PEM");
let debug = format!("{:?}", Identity::from_pem("-cert-", "-secret-key-"));
assert!(!debug.contains("secret"), "{debug}");
}
#[test]
fn trust_composes_and_deduplicates_pins() {
let fp = Fingerprint::from_bytes([7; 32]);
let t = Trust::pin(fp).and_pin(fp).and_anchor("-ca-");
assert_eq!(t.pins, vec![fp]);
assert_eq!(t.anchors, vec![Pem::Bytes(b"-ca-".to_vec())]);
assert!(!t.is_empty());
assert!(Trust::by_address().is_empty());
}
#[test]
fn a_combined_pem_file_feeds_both_sources() {
let id = Identity::from_pem_file("/tmp/id.pem");
assert_eq!(id.cert_chain, Pem::File("/tmp/id.pem".into()));
assert_eq!(id.key, Pem::File("/tmp/id.pem".into()));
}
#[test]
fn different_trust_or_identity_is_a_different_client_tls() {
let fp = Fingerprint::from_bytes([1; 32]);
let a = ClientTls::new(Trust::pin(fp));
let b = ClientTls::new(Trust::pin(Fingerprint::from_bytes([2; 32])));
let c = a.clone().with_identity(Identity::from_pem("-c-", "-k-"));
assert_ne!(a, b);
assert_ne!(a, c);
assert_eq!(a, ClientTls::from(Trust::pin(fp)));
}
}