use std::collections::BTreeMap;
use std::io;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LocalInstanceRegistryError {
#[error("same-host FIPS discovery disabled")]
Disabled,
#[error("could not resolve FIPS local instance registry directory")]
NoRegistryDir,
#[error("local instance registry IO failed at {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("local instance registry serialization failed: {0}")]
Json(#[from] serde_json::Error),
}
pub const DEFAULT_LOCAL_RENDEZVOUS_ADDR: SocketAddrV4 =
SocketAddrV4::new(Ipv4Addr::LOCALHOST, 21_211);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct LocalInstanceDiscoveryConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_rendezvous_addr")]
pub rendezvous_addr: SocketAddrV4,
#[serde(default = "default_retry_interval_ms")]
pub retry_interval_ms: u64,
}
impl Default for LocalInstanceDiscoveryConfig {
fn default() -> Self {
Self {
enabled: false,
rendezvous_addr: DEFAULT_LOCAL_RENDEZVOUS_ADDR,
retry_interval_ms: default_retry_interval_ms(),
}
}
}
impl LocalInstanceDiscoveryConfig {
pub(crate) fn has_valid_rendezvous_addr(&self) -> bool {
self.rendezvous_addr.ip().is_loopback() && self.rendezvous_addr.port() != 0
}
}
const fn default_rendezvous_addr() -> SocketAddrV4 {
DEFAULT_LOCAL_RENDEZVOUS_ADDR
}
const fn default_retry_interval_ms() -> u64 {
1_000
}
pub(crate) fn lan_discovery_scope(config: &crate::Config) -> Option<String> {
normalized_scope(config.node.discovery.lan.scope.as_deref()).or_else(|| {
let app = config.node.discovery.nostr.app.trim();
normalized_scope(Some(app.strip_prefix("fips-overlay-v1:").unwrap_or(app)))
})
}
fn normalized_scope(scope: Option<&str>) -> Option<String> {
scope
.map(str::trim)
.filter(|scope| !scope.is_empty())
.map(str::to_string)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalInstanceCapability {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fsp_port: Option<u16>,
#[serde(default, skip_serializing_if = "is_zero_priority")]
pub priority: i16,
}
impl LocalInstanceCapability {
pub fn service(name: impl Into<String>, fsp_port: u16) -> Self {
Self {
name: name.into(),
fsp_port: Some(fsp_port),
priority: 0,
}
}
pub fn role(name: impl Into<String>) -> Self {
Self {
name: name.into(),
fsp_port: None,
priority: 0,
}
}
pub fn with_priority(mut self, priority: i16) -> Self {
self.priority = priority;
self
}
}
fn is_zero_priority(priority: &i16) -> bool {
*priority == 0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalInstanceAdvertisement {
pub npub: String,
pub startup_epoch: [u8; 8],
pub capabilities: Vec<LocalInstanceCapability>,
}
impl LocalInstanceAdvertisement {
pub fn capability(&self, name: &str) -> Option<&LocalInstanceCapability> {
self.capabilities
.iter()
.filter(|capability| capability.name == name)
.min_by(|left, right| {
right
.priority
.cmp(&left.priority)
.then_with(|| left.fsp_port.cmp(&right.fsp_port))
})
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct LocalCapabilityDirectory {
providers: Arc<RwLock<BTreeMap<String, LocalInstanceAdvertisement>>>,
}
impl LocalCapabilityDirectory {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn snapshot(&self) -> Vec<LocalInstanceAdvertisement> {
self.read().values().cloned().collect()
}
pub(crate) fn replace(&self, providers: impl IntoIterator<Item = LocalInstanceAdvertisement>) {
let mut replacement = BTreeMap::new();
for provider in providers {
replacement.insert(provider.npub.clone(), provider);
}
*self.write() = replacement;
}
pub(crate) fn upsert(
&self,
provider: LocalInstanceAdvertisement,
) -> Option<LocalInstanceAdvertisement> {
self.write().insert(provider.npub.clone(), provider)
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<String, LocalInstanceAdvertisement>> {
self.providers
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn write(
&self,
) -> std::sync::RwLockWriteGuard<'_, BTreeMap<String, LocalInstanceAdvertisement>> {
self.providers
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
pub fn select_capability_provider<'a>(
adverts: &'a [LocalInstanceAdvertisement],
capability_name: &str,
) -> Option<&'a LocalInstanceAdvertisement> {
adverts
.iter()
.filter(|advert| advert.capability(capability_name).is_some())
.min_by(|left, right| capability_provider_order(left, right, capability_name))
}
pub fn rank_capability_providers<'a>(
adverts: &'a [LocalInstanceAdvertisement],
capability_name: &str,
) -> Vec<&'a LocalInstanceAdvertisement> {
let mut providers = adverts
.iter()
.filter(|advert| advert.capability(capability_name).is_some())
.collect::<Vec<_>>();
providers.sort_by(|left, right| capability_provider_order(left, right, capability_name));
providers
}
fn capability_provider_order(
left: &LocalInstanceAdvertisement,
right: &LocalInstanceAdvertisement,
capability_name: &str,
) -> std::cmp::Ordering {
right
.capability(capability_name)
.map(|capability| capability.priority)
.cmp(
&left
.capability(capability_name)
.map(|capability| capability.priority),
)
.then_with(|| left.npub.cmp(&right.npub))
.then_with(|| left.startup_epoch.cmp(&right.startup_epoch))
}
#[cfg(test)]
#[path = "local_tests.rs"]
mod tests;