use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::broadcast;
use weida_core::{Error, Fingerprint};
use crate::config::{Identity, Trust};
const EVENT_QUEUE: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IdentityEvent {
Loaded {
fingerprint: Fingerprint,
},
Renewed {
fingerprint: Fingerprint,
},
KeyChanged {
from: Fingerprint,
to: Fingerprint,
},
RenewalFailed(String),
HandoffStolen,
TrustRefreshed,
Missed(u64),
}
pub struct IdentityEvents {
rx: broadcast::Receiver<IdentityEvent>,
}
impl IdentityEvents {
pub async fn recv(&mut self) -> Option<IdentityEvent> {
match self.rx.recv().await {
Ok(event) => Some(event),
Err(broadcast::error::RecvError::Lagged(n)) => Some(IdentityEvent::Missed(n)),
Err(broadcast::error::RecvError::Closed) => None,
}
}
}
impl std::fmt::Debug for IdentityEvents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IdentityEvents").finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub(crate) struct Loaded {
pub(crate) identity: Identity,
fingerprint: Option<Fingerprint>,
}
impl Loaded {
fn new(identity: Identity) -> Result<Loaded, Error> {
let fingerprint = identity.fingerprint()?;
Ok(Loaded {
identity,
fingerprint: Some(fingerprint),
})
}
pub(crate) fn fingerprint(&self) -> Result<Fingerprint, Error> {
match self.fingerprint {
Some(fp) => Ok(fp),
None => self.identity.fingerprint(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FilesOptions {
pub names: Vec<String>,
pub poll: Duration,
}
impl Default for FilesOptions {
fn default() -> Self {
FilesOptions {
names: Vec::new(),
poll: Duration::from_secs(10),
}
}
}
const CERT_FILE: &str = "cert.pem";
const KEY_FILE: &str = "key.pem";
struct Files {
dir: PathBuf,
poll: Duration,
seen: Mutex<FilesSeen>,
}
struct FilesSeen {
cert: Option<SystemTime>,
key: Option<SystemTime>,
checked: Instant,
}
enum Kind {
Static,
Ephemeral,
Files(Files),
External,
}
struct IdentityInner {
id: u64,
kind: Kind,
current: RwLock<Loaded>,
generation: AtomicU64,
events: broadcast::Sender<IdentityEvent>,
}
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone)]
pub struct IdentitySource {
inner: Arc<IdentityInner>,
}
impl IdentitySource {
fn build(kind: Kind, loaded: Loaded) -> IdentitySource {
let (events, _) = broadcast::channel(EVENT_QUEUE);
let source = IdentitySource {
inner: Arc::new(IdentityInner {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
kind,
current: RwLock::new(loaded),
generation: AtomicU64::new(0),
events,
}),
};
if let Ok(fingerprint) = source.fingerprint() {
source.emit(IdentityEvent::Loaded { fingerprint });
}
source
}
pub fn from_identity(identity: Identity) -> Result<IdentitySource, Error> {
Ok(IdentitySource::build(Kind::Static, Loaded::new(identity)?))
}
#[cfg(feature = "generate")]
pub fn ephemeral(
names: impl IntoIterator<Item = impl Into<String>>,
) -> Result<IdentitySource, Error> {
let identity = Identity::generate_for(names)?;
Ok(IdentitySource::build(
Kind::Ephemeral,
Loaded::new(identity)?,
))
}
pub fn external(initial: Identity) -> Result<IdentitySource, Error> {
Ok(IdentitySource::build(Kind::External, Loaded::new(initial)?))
}
pub fn files(dir: impl Into<PathBuf>, options: FilesOptions) -> Result<IdentitySource, Error> {
let dir = dir.into();
let cert = dir.join(CERT_FILE);
let key = dir.join(KEY_FILE);
match (cert.exists(), key.exists()) {
(true, true) => {}
(false, false) => bootstrap(&dir, &cert, &key, &options.names)?,
_ => {
return Err(Error::Tls(format!(
"{}: one of {CERT_FILE} and {KEY_FILE} exists without the other",
dir.display()
)));
}
}
let identity = Identity::from_pem_files(&cert, &key);
let loaded = Loaded::new(identity)?;
let seen = FilesSeen {
cert: mtime(&cert),
key: mtime(&key),
checked: Instant::now(),
};
Ok(IdentitySource::build(
Kind::Files(Files {
dir,
poll: options.poll,
seen: Mutex::new(seen),
}),
loaded,
))
}
pub fn current(&self) -> Identity {
self.refresh_if_due();
self.inner
.current
.read()
.expect("identity poisoned")
.identity
.clone()
}
pub fn fingerprint(&self) -> Result<Fingerprint, Error> {
self.refresh_if_due();
self.inner
.current
.read()
.expect("identity poisoned")
.fingerprint()
}
pub(crate) fn loaded(&self) -> Loaded {
self.refresh_if_due();
self.inner
.current
.read()
.expect("identity poisoned")
.clone()
}
pub(crate) fn generation(&self) -> u64 {
self.inner.generation.load(Ordering::Acquire)
}
pub fn update(&self, identity: Identity) -> Result<(), Error> {
let loaded = match Loaded::new(identity) {
Ok(loaded) => loaded,
Err(e) => {
self.emit(IdentityEvent::RenewalFailed(e.to_string()));
return Err(e);
}
};
self.install(loaded);
Ok(())
}
pub fn report_failure(&self, reason: impl Into<String>) {
self.emit(IdentityEvent::RenewalFailed(reason.into()));
}
pub fn report_handoff_stolen(&self) {
self.emit(IdentityEvent::HandoffStolen);
}
pub fn reload(&self) -> Result<bool, Error> {
let Kind::Files(files) = &self.inner.kind else {
return Ok(false);
};
let cert = files.dir.join(CERT_FILE);
let key = files.dir.join(KEY_FILE);
let (cert_seen, key_seen) = (mtime(&cert), mtime(&key));
{
let mut seen = files.seen.lock().expect("file times poisoned");
seen.checked = Instant::now();
if seen.cert == cert_seen && seen.key == key_seen {
return Ok(false);
}
seen.cert = cert_seen;
seen.key = key_seen;
}
match Loaded::new(Identity::from_pem_files(&cert, &key)) {
Ok(loaded) => {
self.install(loaded);
Ok(true)
}
Err(e) => {
self.emit(IdentityEvent::RenewalFailed(e.to_string()));
Err(e)
}
}
}
pub fn events(&self) -> IdentityEvents {
IdentityEvents {
rx: self.inner.events.subscribe(),
}
}
fn install(&self, loaded: Loaded) {
let event = {
let mut current = self.inner.current.write().expect("identity poisoned");
let to = loaded
.fingerprint()
.expect("an installed identity was parsed when it was loaded");
let event = match current.fingerprint() {
Ok(from) if from == to => IdentityEvent::Renewed { fingerprint: to },
Ok(from) => IdentityEvent::KeyChanged { from, to },
Err(_) => IdentityEvent::Loaded { fingerprint: to },
};
*current = loaded;
self.inner.generation.fetch_add(1, Ordering::AcqRel);
event
};
self.emit(event);
}
fn refresh_if_due(&self) {
let Kind::Files(files) = &self.inner.kind else {
return;
};
let due = {
let seen = files.seen.lock().expect("file times poisoned");
seen.checked.elapsed() >= files.poll
};
if due {
let _ = self.reload();
}
}
fn emit(&self, event: IdentityEvent) {
let _ = self.inner.events.send(event);
}
fn is_static(&self) -> bool {
matches!(self.inner.kind, Kind::Static)
}
}
impl From<Identity> for IdentitySource {
fn from(identity: Identity) -> IdentitySource {
IdentitySource::build(
Kind::Static,
Loaded {
identity,
fingerprint: None,
},
)
}
}
impl PartialEq for IdentitySource {
fn eq(&self, other: &IdentitySource) -> bool {
if Arc::ptr_eq(&self.inner, &other.inner) {
return true;
}
self.is_static()
&& other.is_static()
&& self
.inner
.current
.read()
.expect("identity poisoned")
.identity
== other
.inner
.current
.read()
.expect("identity poisoned")
.identity
}
}
impl Eq for IdentitySource {}
impl std::hash::Hash for IdentitySource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if self.is_static() {
self.inner
.current
.read()
.expect("identity poisoned")
.identity
.hash(state);
} else {
self.inner.id.hash(state);
}
}
}
impl std::fmt::Debug for IdentitySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match &self.inner.kind {
Kind::Static => "static",
Kind::Ephemeral => "ephemeral",
Kind::Files(_) => "files",
Kind::External => "external",
};
f.debug_struct("IdentitySource")
.field("kind", &kind)
.field(
"fingerprint",
&self
.inner
.current
.read()
.expect("identity poisoned")
.fingerprint()
.ok(),
)
.finish_non_exhaustive()
}
}
#[cfg(feature = "generate")]
fn bootstrap(dir: &Path, cert: &Path, key: &Path, names: &[String]) -> Result<(), Error> {
std::fs::create_dir_all(dir).map_err(Error::Io)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).map_err(Error::Io)?;
}
let identity = Identity::generate_for(names.iter().cloned())?;
write_owner_only(cert, identity.certificate_pem()?.as_bytes())?;
write_owner_only(key, crate::tls::key_pem(&identity.key)?.as_bytes())?;
Ok(())
}
#[cfg(not(feature = "generate"))]
fn bootstrap(dir: &Path, _cert: &Path, _key: &Path, _names: &[String]) -> Result<(), Error> {
Err(Error::Tls(format!(
"{}: no identity to load, and generating one needs the `generate` feature",
dir.display()
)))
}
#[cfg(feature = "generate")]
fn write_owner_only(path: &Path, bytes: &[u8]) -> Result<(), Error> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path).map_err(Error::Io)?;
file.write_all(bytes).map_err(Error::Io)?;
file.sync_all().map_err(Error::Io)
}
fn mtime(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}
struct TrustInner {
id: u64,
is_static: bool,
current: RwLock<Trust>,
generation: AtomicU64,
events: broadcast::Sender<IdentityEvent>,
}
#[derive(Clone)]
pub struct TrustSource {
inner: Arc<TrustInner>,
}
impl TrustSource {
fn build(trust: Trust, is_static: bool) -> TrustSource {
let (events, _) = broadcast::channel(EVENT_QUEUE);
TrustSource {
inner: Arc::new(TrustInner {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
is_static,
current: RwLock::new(trust),
generation: AtomicU64::new(0),
events,
}),
}
}
pub fn from_trust(trust: Trust) -> TrustSource {
TrustSource::build(trust, true)
}
pub fn external(initial: Trust) -> TrustSource {
TrustSource::build(initial, false)
}
pub fn current(&self) -> Trust {
self.inner.current.read().expect("trust poisoned").clone()
}
pub fn update(&self, trust: Trust) {
{
let mut current = self.inner.current.write().expect("trust poisoned");
*current = trust;
}
self.inner.generation.fetch_add(1, Ordering::AcqRel);
let _ = self.inner.events.send(IdentityEvent::TrustRefreshed);
}
pub fn report_failure(&self, reason: impl Into<String>) {
let _ = self
.inner
.events
.send(IdentityEvent::RenewalFailed(reason.into()));
}
pub fn events(&self) -> IdentityEvents {
IdentityEvents {
rx: self.inner.events.subscribe(),
}
}
pub(crate) fn generation(&self) -> u64 {
self.inner.generation.load(Ordering::Acquire)
}
pub fn is_empty(&self) -> bool {
self.inner
.current
.read()
.expect("trust poisoned")
.is_empty()
}
}
impl From<Trust> for TrustSource {
fn from(trust: Trust) -> TrustSource {
TrustSource::from_trust(trust)
}
}
impl PartialEq for TrustSource {
fn eq(&self, other: &TrustSource) -> bool {
if Arc::ptr_eq(&self.inner, &other.inner) {
return true;
}
self.inner.is_static
&& other.inner.is_static
&& *self.inner.current.read().expect("trust poisoned")
== *other.inner.current.read().expect("trust poisoned")
}
}
impl Eq for TrustSource {}
impl std::hash::Hash for TrustSource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if self.inner.is_static {
self.inner
.current
.read()
.expect("trust poisoned")
.hash(state);
} else {
self.inner.id.hash(state);
}
}
}
impl std::fmt::Debug for TrustSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustSource")
.field("static", &self.inner.is_static)
.field(
"trust",
&*self.inner.current.read().expect("trust poisoned"),
)
.finish()
}
}