use std::collections::BTreeMap;
use std::fmt;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use dynamic_config::{Builder, Changes, ConfigStatus, Dynamic};
use crate::audit::{AuditEntry, AuditSink, StderrAudit};
use crate::auth::{Authenticator, Principal, Token};
use crate::config::{Refusal, SectionConfig, ServerConfig};
use crate::document::Document;
pub struct Section {
application: String,
profile: String,
config: Dynamic<Document>,
_watch: Option<dynamic_config::watch::WatchHandle>,
}
impl Section {
#[must_use]
pub fn application(&self) -> &str {
&self.application
}
#[must_use]
pub fn profile(&self) -> &str {
&self.profile
}
#[must_use]
pub fn current(&self) -> Option<Arc<Document>> {
self.config.current()
}
#[must_use]
pub fn generation(&self) -> u64 {
self.config.generation()
}
#[must_use]
pub fn installed(&self) -> Option<(u64, Arc<Document>)> {
let generation = self.generation();
self.current().map(|document| (generation, document))
}
#[must_use]
pub fn status(&self) -> ConfigStatus {
self.config.status()
}
#[must_use]
pub fn is_ready(&self) -> bool {
self.current().is_some() && self.status().is_healthy()
}
#[must_use]
pub fn changes(&self) -> Changes<Document> {
self.config.changes()
}
pub fn reload(&self) -> Result<(), dynamic_config::Error> {
self.config.reload()
}
#[must_use]
pub fn sources(&self) -> Builder<Document> {
self.config.builder().clone()
}
}
impl fmt::Debug for Section {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Section")
.field("application", &self.application)
.field("profile", &self.profile)
.field("generation", &self.generation())
.finish_non_exhaustive()
}
}
pub struct Server {
sections: BTreeMap<(String, String), Arc<Section>>,
authenticator: Authenticator,
audit: Arc<dyn AuditSink>,
address: SocketAddr,
#[cfg(feature = "tls")]
tls: Option<crate::tls::Tls>,
max_streams: usize,
streams: Arc<AtomicUsize>,
}
#[derive(Debug)]
pub struct StreamPermit {
open: Arc<AtomicUsize>,
}
impl Drop for StreamPermit {
fn drop(&mut self) {
self.open.fetch_sub(1, Ordering::Release);
}
}
impl Server {
pub fn start(config: &ServerConfig) -> Result<Self, StartupError> {
Self::start_with(config, StderrAudit)
}
pub fn start_with(config: &ServerConfig, audit: impl AuditSink) -> Result<Self, StartupError> {
config.validate()?;
let address = config.address()?;
#[cfg(feature = "tls")]
let tls = config
.tls
.as_ref()
.map(crate::tls::Tls::load)
.transpose()
.map_err(StartupError::Tls)?;
let debounce = Duration::from_millis(config.watch_debounce_ms);
let mut sections = BTreeMap::new();
for described in &config.sections {
let section = load_section(described, debounce)?;
sections.insert(
(described.application.clone(), described.profile.clone()),
Arc::new(section),
);
}
let mut clients: Vec<(Token, Principal)> = Vec::new();
let mut anonymous = None;
for client in &config.clients {
let principal = Principal::new(&client.name, client.applications.clone());
match &client.token {
Some(token) => clients.push((token.clone(), principal)),
None => anonymous = Some(principal),
}
}
Ok(Self {
sections,
authenticator: Authenticator::new(clients, anonymous),
audit: Arc::new(audit),
address,
#[cfg(feature = "tls")]
tls,
max_streams: config.max_stream_connections,
streams: Arc::new(AtomicUsize::new(0)),
})
}
#[must_use]
pub fn address(&self) -> SocketAddr {
self.address
}
#[cfg(feature = "tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
#[must_use]
pub fn tls(&self) -> Option<&crate::tls::Tls> {
self.tls.as_ref()
}
#[cfg(feature = "tls")]
pub(crate) fn audit_sink(&self) -> Arc<dyn AuditSink> {
Arc::clone(&self.audit)
}
#[must_use]
pub fn authenticate(&self, authorization: Option<&str>) -> Option<Principal> {
self.authenticator.authenticate(authorization)
}
#[must_use]
pub fn section(&self, application: &str, profile: &str) -> Option<&Arc<Section>> {
self.sections
.get(&(application.to_owned(), profile.to_owned()))
}
pub fn sections(&self) -> impl Iterator<Item = &Arc<Section>> {
self.sections.values()
}
#[must_use]
pub fn is_ready(&self) -> bool {
self.sections().all(|section| section.is_ready())
}
pub fn record(&self, entry: &AuditEntry) {
self.audit.record(entry);
}
#[must_use]
pub fn streams_enabled(&self) -> bool {
self.max_streams > 0
}
#[must_use]
pub fn open_stream(&self) -> Option<StreamPermit> {
let mut open = self.streams.load(Ordering::Acquire);
loop {
if open >= self.max_streams {
return None;
}
match self.streams.compare_exchange_weak(
open,
open + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
return Some(StreamPermit {
open: Arc::clone(&self.streams),
})
}
Err(current) => open = current,
}
}
}
#[must_use]
pub fn open_streams(&self) -> usize {
self.streams.load(Ordering::Acquire)
}
}
impl fmt::Debug for Server {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Server")
.field("address", &self.address)
.field("sections", &self.sections.len())
.field("anonymous", &self.authenticator.allows_anonymous())
.field("tls", &self.posture())
.finish_non_exhaustive()
}
}
impl Server {
#[cfg(feature = "tls")]
#[must_use]
pub fn posture(&self) -> &'static str {
match self.tls.as_ref().map(crate::tls::Tls::is_mutual) {
Some(true) => "tls, client certificate required",
Some(false) => "tls",
None => "none",
}
}
#[cfg(not(feature = "tls"))]
#[must_use]
pub fn posture(&self) -> &'static str {
"none"
}
}
fn load_section(described: &SectionConfig, debounce: Duration) -> Result<Section, StartupError> {
let mut builder = Builder::<Document>::new(described.application.as_str());
if described.whole_document {
builder = builder.whole_document();
}
for file in &described.files {
builder = builder.file(file.as_str());
}
if let Some(prefix) = &described.env_prefix {
builder = builder.env(prefix.as_str());
}
let config = Dynamic::new(builder);
config.init().map_err(|source| StartupError::Section {
application: described.application.clone(),
profile: described.profile.clone(),
source,
})?;
let watch = if debounce.is_zero() {
None
} else {
Some(
config
.watch(debounce)
.map_err(|source| StartupError::Watch {
application: described.application.clone(),
profile: described.profile.clone(),
source,
})?,
)
};
Ok(Section {
application: described.application.clone(),
profile: described.profile.clone(),
config,
_watch: watch,
})
}
#[derive(Debug)]
#[non_exhaustive]
pub enum StartupError {
Refused(Refusal),
Section {
application: String,
profile: String,
source: dynamic_config::Error,
},
Watch {
application: String,
profile: String,
source: std::io::Error,
},
#[cfg(feature = "tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
Tls(crate::tls::TlsError),
}
impl From<Refusal> for StartupError {
fn from(refusal: Refusal) -> Self {
Self::Refused(refusal)
}
}
impl fmt::Display for StartupError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Refused(refusal) => write!(f, "refusing to start: {refusal}"),
Self::Section {
application,
profile,
source,
} => write!(
f,
"refusing to start: the section `{application}`/`{profile}` will not load: \
{source}"
),
Self::Watch {
application,
profile,
source,
} => write!(
f,
"refusing to start: the section `{application}`/`{profile}` cannot be \
watched: {source}"
),
#[cfg(feature = "tls")]
Self::Tls(error) => write!(f, "refusing to start: {error}"),
}
}
}
impl std::error::Error for StartupError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Refused(refusal) => Some(refusal),
Self::Section { source, .. } => Some(source),
Self::Watch { source, .. } => Some(source),
#[cfg(feature = "tls")]
Self::Tls(error) => Some(error),
}
}
}