mirror of
https://git.asonix.dog/asonix/relay.git
synced 2025-01-01 23:28:41 +00:00
478 lines
16 KiB
Rust
478 lines
16 KiB
Rust
use crate::{
|
|
data::{ActorCache, State},
|
|
error::Error,
|
|
extractors::{AdminConfig, XApiToken},
|
|
middleware::MyVerify,
|
|
requests::Requests,
|
|
};
|
|
use activitystreams::{
|
|
iri,
|
|
iri_string::{
|
|
format::ToDedicatedString,
|
|
resolve::FixedBaseResolver,
|
|
types::{IriAbsoluteString, IriFragmentStr, IriRelativeStr, IriString},
|
|
},
|
|
};
|
|
use config::Environment;
|
|
use http_signature_normalization_actix::prelude::{VerifyDigest, VerifySignature};
|
|
use rustls::{Certificate, PrivateKey};
|
|
use sha2::{Digest, Sha256};
|
|
use std::{io::BufReader, net::IpAddr, 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,
|
|
opentelemetry_url: Option<IriString>,
|
|
telegram_token: Option<String>,
|
|
telegram_admin_handle: Option<String>,
|
|
api_token: Option<String>,
|
|
tls_key: Option<PathBuf>,
|
|
tls_cert: Option<PathBuf>,
|
|
footer_blurb: Option<String>,
|
|
local_domains: Option<String>,
|
|
local_blurb: Option<String>,
|
|
}
|
|
|
|
#[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<IriString>,
|
|
telegram_token: Option<String>,
|
|
telegram_admin_handle: Option<String>,
|
|
api_token: Option<String>,
|
|
tls: Option<TlsConfig>,
|
|
footer_blurb: Option<String>,
|
|
local_domains: Vec<String>,
|
|
local_blurb: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct TlsConfig {
|
|
key: PathBuf,
|
|
cert: PathBuf,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
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)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
pub(crate) fn build() -> Result<Self, Error> {
|
|
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("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>)?
|
|
.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();
|
|
|
|
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: config.source_repo,
|
|
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,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn open_keys(&self) -> Result<Option<(Vec<Certificate>, 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<crate::templates::Html<String>> {
|
|
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<crate::templates::Html<String>> {
|
|
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<Sha256> {
|
|
if self.validate_signatures {
|
|
VerifyDigest::new(Sha256::new())
|
|
} else {
|
|
VerifyDigest::new(Sha256::new()).optional()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn signature_middleware(
|
|
&self,
|
|
requests: Requests,
|
|
actors: ActorCache,
|
|
state: State,
|
|
) -> VerifySignature<MyVerify> {
|
|
if self.validate_signatures {
|
|
VerifySignature::new(MyVerify(requests, actors, state), Default::default())
|
|
} else {
|
|
VerifySignature::new(MyVerify(requests, actors, state), Default::default()).optional()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn x_api_token(&self) -> Option<XApiToken> {
|
|
self.api_token.clone().map(XApiToken::new)
|
|
}
|
|
|
|
pub(crate) fn admin_config(&self) -> Option<actix_web::web::Data<AdminConfig>> {
|
|
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{}-{}", Self::version(), git);
|
|
}
|
|
|
|
format!("v{}", Self::version())
|
|
}
|
|
|
|
fn git_version() -> Option<String> {
|
|
let branch = Self::git_branch()?;
|
|
let hash = Self::git_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")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
#[tracing::instrument(level = "debug", skip_all, fields(base_uri = tracing::field::debug(&self.base_uri), kind = tracing::field::debug(&kind)))]
|
|
fn do_generate_url(&self, kind: UrlKind) -> Result<IriString, Error> {
|
|
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<IriString, Error> {
|
|
let iri = match kind {
|
|
AdminUrlKind::Allow => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/allow")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Disallow => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/disallow")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Block => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/block")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Unblock => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/unblock")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Allowed => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/allowed")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Blocked => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/blocked")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Connected => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/connected")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
AdminUrlKind::Stats => FixedBaseResolver::new(self.base_uri.as_ref())
|
|
.resolve(IriRelativeStr::new("api/v1/admin/stats")?.as_ref())
|
|
.try_to_dedicated_string()?,
|
|
};
|
|
|
|
Ok(iri)
|
|
}
|
|
}
|