use crate::{ error::Error, extractors::{AdminConfig, XApiToken}, }; use activitystreams::{ iri, iri_string::{ format::ToDedicatedString, resolve::FixedBaseResolver, types::{IriAbsoluteString, IriFragmentStr, IriRelativeStr, IriString}, }, }; use config::Environment; use http_signature_normalization_actix::prelude::VerifyDigest; use rsa::sha2::{Digest, Sha256}; use rustls::{Certificate, PrivateKey}; use std::{ io::BufReader, net::{IpAddr, SocketAddr}, path::PathBuf, }; use uuid::Uuid; #[derive(Clone, Debug, serde::Deserialize)] pub(crate) struct ParsedConfig { hostname: String, addr: IpAddr, port: u16, debug: bool, restricted_mode: bool, validate_signatures: bool, https: bool, publish_blocks: bool, sled_path: PathBuf, source_repo: IriString, repository_commit_base: String, opentelemetry_url: Option, telegram_token: Option, telegram_admin_handle: Option, api_token: Option, tls_key: Option, tls_cert: Option, footer_blurb: Option, local_domains: Option, local_blurb: Option, prometheus_addr: Option, prometheus_port: Option, } #[derive(Clone)] pub struct Config { hostname: String, addr: IpAddr, port: u16, debug: bool, restricted_mode: bool, validate_signatures: bool, publish_blocks: bool, base_uri: IriAbsoluteString, sled_path: PathBuf, source_repo: IriString, opentelemetry_url: Option, telegram_token: Option, telegram_admin_handle: Option, api_token: Option, tls: Option, footer_blurb: Option, local_domains: Vec, local_blurb: Option, prometheus_config: Option, } #[derive(Clone)] struct TlsConfig { key: PathBuf, cert: PathBuf, } #[derive(Clone, Debug)] struct PrometheusConfig { addr: IpAddr, port: u16, } #[derive(Debug)] pub enum UrlKind { Activity, Actor, Followers, Following, Inbox, Index, MainKey, Media(Uuid), NodeInfo, Outbox, } #[derive(Debug)] pub enum AdminUrlKind { Allow, Disallow, Block, Unblock, Allowed, Blocked, Connected, Stats, LastSeen, } impl std::fmt::Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Config") .field("hostname", &self.hostname) .field("addr", &self.addr) .field("port", &self.port) .field("debug", &self.debug) .field("restricted_mode", &self.restricted_mode) .field("validate_signatures", &self.validate_signatures) .field("publish_blocks", &self.publish_blocks) .field("base_uri", &self.base_uri.to_string()) .field("sled_path", &self.sled_path) .field("source_repo", &self.source_repo.to_string()) .field( "opentelemetry_url", &self.opentelemetry_url.as_ref().map(|url| url.to_string()), ) .field("telegram_token", &"[redacted]") .field("telegram_admin_handle", &self.telegram_admin_handle) .field("api_token", &"[redacted]") .field("tls_key", &"[redacted]") .field("tls_cert", &"[redacted]") .field("footer_blurb", &self.footer_blurb) .field("local_domains", &self.local_domains) .field("local_blurb", &self.local_blurb) .field("prometheus_config", &self.prometheus_config) .finish() } } impl Config { pub(crate) fn build() -> Result { let config = config::Config::builder() .set_default("hostname", "localhost:8080")? .set_default("addr", "127.0.0.1")? .set_default("port", 8080u64)? .set_default("debug", true)? .set_default("restricted_mode", false)? .set_default("validate_signatures", true)? .set_default("https", true)? .set_default("publish_blocks", false)? .set_default("sled_path", "./sled/db-0-34")? .set_default("source_repo", "https://git.asonix.dog/asonix/relay")? .set_default("repository_commit_base", "/src/commit/")? .set_default("opentelemetry_url", None as Option<&str>)? .set_default("telegram_token", None as Option<&str>)? .set_default("telegram_admin_handle", None as Option<&str>)? .set_default("api_token", None as Option<&str>)? .set_default("tls_key", None as Option<&str>)? .set_default("tls_cert", None as Option<&str>)? .set_default("footer_blurb", None as Option<&str>)? .set_default("local_domains", None as Option<&str>)? .set_default("local_blurb", None as Option<&str>)? .set_default("prometheus_addr", None as Option<&str>)? .set_default("prometheus_port", None as Option)? .add_source(Environment::default()) .build()?; let config: ParsedConfig = config.try_deserialize()?; let scheme = if config.https { "https" } else { "http" }; let base_uri = iri!(format!("{scheme}://{}", config.hostname)).into_absolute(); let tls = match (config.tls_key, config.tls_cert) { (Some(key), Some(cert)) => Some(TlsConfig { key, cert }), (Some(_), None) => { tracing::warn!("TLS_KEY is set but TLS_CERT isn't , not building TLS config"); None } (None, Some(_)) => { tracing::warn!("TLS_CERT is set but TLS_KEY isn't , not building TLS config"); None } (None, None) => None, }; let local_domains = config .local_domains .iter() .flat_map(|s| s.split(',')) .map(|d| d.to_string()) .collect(); let prometheus_config = match (config.prometheus_addr, config.prometheus_port) { (Some(addr), Some(port)) => Some(PrometheusConfig { addr, port }), (Some(_), None) => { tracing::warn!("PROMETHEUS_ADDR is set but PROMETHEUS_PORT is not set, not building Prometheus config"); None } (None, Some(_)) => { tracing::warn!("PROMETHEUS_PORT is set but PROMETHEUS_ADDR is not set, not building Prometheus config"); None } (None, None) => None, }; let source_url = match Self::git_hash() { Some(hash) => format!( "{}{}{hash}", config.source_repo, config.repository_commit_base ) .parse() .expect("constructed source URL is valid"), None => config.source_repo.clone(), }; Ok(Config { hostname: config.hostname, addr: config.addr, port: config.port, debug: config.debug, restricted_mode: config.restricted_mode, validate_signatures: config.validate_signatures, publish_blocks: config.publish_blocks, base_uri, sled_path: config.sled_path, source_repo: source_url, opentelemetry_url: config.opentelemetry_url, telegram_token: config.telegram_token, telegram_admin_handle: config.telegram_admin_handle, api_token: config.api_token, tls, footer_blurb: config.footer_blurb, local_domains, local_blurb: config.local_blurb, prometheus_config, }) } pub(crate) fn prometheus_bind_address(&self) -> Option { let config = self.prometheus_config.as_ref()?; Some((config.addr, config.port).into()) } pub(crate) fn open_keys(&self) -> Result, PrivateKey)>, Error> { let tls = if let Some(tls) = &self.tls { tls } else { tracing::warn!("No TLS config present"); return Ok(None); }; let mut certs_reader = BufReader::new(std::fs::File::open(&tls.cert)?); let certs = rustls_pemfile::certs(&mut certs_reader)?; if certs.is_empty() { tracing::warn!("No certs read from certificate file"); return Ok(None); } let mut key_reader = BufReader::new(std::fs::File::open(&tls.key)?); let key = rustls_pemfile::read_one(&mut key_reader)?; let certs = certs.into_iter().map(Certificate).collect(); let key = if let Some(key) = key { match key { rustls_pemfile::Item::RSAKey(der) => PrivateKey(der), rustls_pemfile::Item::PKCS8Key(der) => PrivateKey(der), rustls_pemfile::Item::ECKey(der) => PrivateKey(der), _ => { tracing::warn!("Unknown key format: {:?}", key); return Ok(None); } } } else { tracing::warn!("Failed to read private key"); return Ok(None); }; Ok(Some((certs, key))) } pub(crate) fn footer_blurb(&self) -> Option> { if let Some(blurb) = &self.footer_blurb { if !blurb.is_empty() { return Some(crate::templates::Html(ammonia::clean(blurb))); } } None } pub(crate) fn local_blurb(&self) -> Option> { if let Some(blurb) = &self.local_blurb { if !blurb.is_empty() { return Some(crate::templates::Html(ammonia::clean(blurb))); } } None } pub(crate) fn local_domains(&self) -> &[String] { &self.local_domains } pub(crate) fn sled_path(&self) -> &PathBuf { &self.sled_path } pub(crate) fn validate_signatures(&self) -> bool { self.validate_signatures } pub(crate) fn digest_middleware(&self) -> VerifyDigest { if self.validate_signatures { VerifyDigest::new(Sha256::new()) } else { VerifyDigest::new(Sha256::new()).optional() } } pub(crate) fn x_api_token(&self) -> Option { self.api_token.clone().map(XApiToken::new) } pub(crate) fn admin_config(&self) -> Option> { if let Some(api_token) = &self.api_token { match AdminConfig::build(api_token) { Ok(conf) => Some(actix_web::web::Data::new(conf)), Err(e) => { tracing::error!("Error creating admin config: {e}"); None } } } else { None } } pub(crate) fn bind_address(&self) -> (IpAddr, u16) { (self.addr, self.port) } pub(crate) fn debug(&self) -> bool { self.debug } pub(crate) fn publish_blocks(&self) -> bool { self.publish_blocks } pub(crate) fn restricted_mode(&self) -> bool { self.restricted_mode } pub(crate) fn hostname(&self) -> &str { &self.hostname } pub(crate) fn generate_resource(&self) -> String { format!("relay@{}", self.hostname) } pub(crate) fn software_name() -> &'static str { "AodeRelay" } pub(crate) fn software_version() -> String { if let Some(git) = Self::git_version() { return format!("v{}-{git}", Self::version()); } format!("v{}", Self::version()) } fn git_version() -> Option { let branch = Self::git_branch()?; let hash = Self::git_short_hash()?; Some(format!("{branch}-{hash}")) } fn name() -> &'static str { env!("PKG_NAME") } fn version() -> &'static str { env!("PKG_VERSION") } fn git_branch() -> Option<&'static str> { option_env!("GIT_BRANCH") } fn git_hash() -> Option<&'static str> { option_env!("GIT_HASH") } fn git_short_hash() -> Option<&'static str> { option_env!("GIT_SHORT_HASH") } pub(crate) fn user_agent(&self) -> String { format!( "{} ({}/{}; +{})", Self::software_name(), Self::name(), Self::software_version(), self.generate_url(UrlKind::Index), ) } pub(crate) fn source_code(&self) -> &IriString { &self.source_repo } pub(crate) fn opentelemetry_url(&self) -> Option<&IriString> { self.opentelemetry_url.as_ref() } pub(crate) fn telegram_info(&self) -> Option<(&str, &str)> { self.telegram_token.as_deref().and_then(|token| { let handle = self.telegram_admin_handle.as_deref()?; Some((token, handle)) }) } pub(crate) fn generate_url(&self, kind: UrlKind) -> IriString { self.do_generate_url(kind).expect("Generated valid IRI") } fn do_generate_url(&self, kind: UrlKind) -> Result { let iri = match kind { UrlKind::Activity => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new(&format!("activity/{}", Uuid::new_v4()))?.as_ref()) .try_to_dedicated_string()?, UrlKind::Actor => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("actor")?.as_ref()) .try_to_dedicated_string()?, UrlKind::Followers => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("followers")?.as_ref()) .try_to_dedicated_string()?, UrlKind::Following => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("following")?.as_ref()) .try_to_dedicated_string()?, UrlKind::Inbox => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("inbox")?.as_ref()) .try_to_dedicated_string()?, UrlKind::Index => self.base_uri.clone().into(), UrlKind::MainKey => { let actor = IriRelativeStr::new("actor")?; let fragment = IriFragmentStr::new("main-key")?; let mut resolved = FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(actor.as_ref()) .try_to_dedicated_string()?; resolved.set_fragment(Some(fragment)); resolved } UrlKind::Media(uuid) => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new(&format!("media/{uuid}"))?.as_ref()) .try_to_dedicated_string()?, UrlKind::NodeInfo => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("nodeinfo/2.0.json")?.as_ref()) .try_to_dedicated_string()?, UrlKind::Outbox => FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new("outbox")?.as_ref()) .try_to_dedicated_string()?, }; Ok(iri) } pub(crate) fn generate_admin_url(&self, kind: AdminUrlKind) -> IriString { self.do_generate_admin_url(kind) .expect("Generated valid IRI") } fn do_generate_admin_url(&self, kind: AdminUrlKind) -> Result { let path = match kind { AdminUrlKind::Allow => "api/v1/admin/allow", AdminUrlKind::Disallow => "api/v1/admin/disallow", AdminUrlKind::Block => "api/v1/admin/block", AdminUrlKind::Unblock => "api/v1/admin/unblock", AdminUrlKind::Allowed => "api/v1/admin/allowed", AdminUrlKind::Blocked => "api/v1/admin/blocked", AdminUrlKind::Connected => "api/v1/admin/connected", AdminUrlKind::Stats => "api/v1/admin/stats", AdminUrlKind::LastSeen => "api/v1/admin/last_seen", }; let iri = FixedBaseResolver::new(self.base_uri.as_ref()) .resolve(IriRelativeStr::new(path)?.as_ref()) .try_to_dedicated_string()?; Ok(iri) } }