From 7a1a2297d41c18b3ff78f2f4ad2e6e00dceb0aa4 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 3 Aug 2023 14:14:51 +0200 Subject: [PATCH] Move proxy config into domain (bigger change than it sounds like) --- src/domain.rs | 6 +- src/domain/config.rs | 29 ++++- src/domain/config/proxied_domain.rs | 131 +++++++++++++++++++ src/domain/manager.rs | 192 +++++++++++++++++----------- src/domain/store.rs | 138 +++----------------- src/main.rs | 9 +- src/service/gemini.rs | 33 +++-- src/service/gemini/config.rs | 63 +-------- src/service/http.rs | 82 ++++-------- src/service/http/config.rs | 112 +--------------- src/service/http/proxy.rs | 21 ++- 11 files changed, 353 insertions(+), 463 deletions(-) create mode 100644 src/domain/config/proxied_domain.rs diff --git a/src/domain.rs b/src/domain.rs index 327aa09..6204651 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,13 +1,13 @@ pub mod acme; pub mod checker; -mod config; pub mod gemini; pub mod manager; -mod name; -mod settings; pub mod store; mod tls; +mod config; +mod name; +mod settings; pub use config::*; pub use name::*; pub use settings::*; diff --git a/src/domain/config.rs b/src/domain/config.rs index 10308f8..5811bee 100644 --- a/src/domain/config.rs +++ b/src/domain/config.rs @@ -1,8 +1,9 @@ -use std::{collections, net, path, str::FromStr}; - -use serde::{Deserialize, Serialize}; +mod proxied_domain; use crate::domain; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, TryFromInto}; +use std::{collections, net, path, str::FromStr}; fn default_resolver_addr() -> net::SocketAddr { net::SocketAddr::from_str("1.1.1.1:53").unwrap() @@ -27,7 +28,7 @@ pub struct ConfigACME { pub contact_email: String, } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct ConfigBuiltinDomain { #[serde(flatten)] pub settings: domain::Settings, @@ -36,12 +37,32 @@ pub struct ConfigBuiltinDomain { pub public: bool, } +#[serde_as] +#[derive(Clone, Deserialize, Serialize)] +pub struct ConfigProxiedDomain { + #[serde(default)] + #[serde_as(as = "Option>")] + pub gemini_url: Option, + + #[serde(default)] + #[serde_as(as = "Option>")] + pub http_url: Option, + + #[serde(default)] + #[serde_as(as = "TryFromInto>")] + pub http_request_headers: proxied_domain::HttpRequestHeaders, +} + #[derive(Deserialize, Serialize)] pub struct Config { pub store_dir_path: path::PathBuf, #[serde(default)] pub dns: ConfigDNS, pub acme: Option, + #[serde(default)] pub builtins: collections::HashMap, + + #[serde(default)] + pub proxied: collections::HashMap, } diff --git a/src/domain/config/proxied_domain.rs b/src/domain/config/proxied_domain.rs new file mode 100644 index 0000000..33ba4c3 --- /dev/null +++ b/src/domain/config/proxied_domain.rs @@ -0,0 +1,131 @@ +use crate::error::unexpected::{self, Mappable}; +use serde::{Deserialize, Serialize}; + +fn addr_from_url( + url: &str, + expected_scheme: &str, + default_port: u16, +) -> unexpected::Result { + let parsed: http::Uri = url + .parse() + .map_unexpected_while(|| format!("could not parse as url"))?; + + let scheme = parsed + .scheme() + .map_unexpected_while(|| format!("scheme is missing"))?; + + if scheme != expected_scheme { + return Err(unexpected::Error::from( + format!("scheme should be {expected_scheme}").as_str(), + )); + } + + if let Some(path_and_query) = parsed.path_and_query() { + let path_and_query = path_and_query.as_str(); + if path_and_query != "" && path_and_query != "/" { + return Err(unexpected::Error::from( + format!("path must be empty").as_str(), + )); + } + } + + match parsed.authority() { + None => Err(unexpected::Error::from(format!("host is missing").as_str())), + Some(authority) => { + let port = authority.port().map(|p| p.as_u16()).unwrap_or(default_port); + Ok(format!("{}:{port}", authority.host())) + } + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct GeminiUrl { + pub original_url: String, + pub addr: String, +} + +impl TryFrom for GeminiUrl { + type Error = unexpected::Error; + fn try_from(url: String) -> Result { + let addr = addr_from_url(&url, "gemini", 1965)?; + Ok(Self { + original_url: url, + addr, + }) + } +} + +impl From for String { + fn from(u: GeminiUrl) -> Self { + u.original_url + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct HttpUrl { + pub original_url: String, + pub addr: String, +} + +impl TryFrom for HttpUrl { + type Error = unexpected::Error; + fn try_from(url: String) -> Result { + let addr = addr_from_url(&url, "http", 80)?; + Ok(Self { + original_url: url, + addr, + }) + } +} + +impl From for String { + fn from(u: HttpUrl) -> Self { + u.original_url + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct HttpRequestHeader { + name: String, + value: String, +} + +#[derive(Clone)] +pub struct HttpRequestHeaders(pub http::header::HeaderMap); + +impl Default for HttpRequestHeaders { + fn default() -> Self { + Self(http::header::HeaderMap::default()) + } +} + +impl TryFrom for Vec { + type Error = http::header::ToStrError; + + fn try_from(h: HttpRequestHeaders) -> Result { + let mut v = vec![]; + for (name, value) in &h.0 { + v.push(HttpRequestHeader { + name: name.to_string(), + value: value.to_str()?.to_string(), + }) + } + Ok(v) + } +} + +impl TryFrom> for HttpRequestHeaders { + type Error = unexpected::Error; + + fn try_from(v: Vec) -> Result { + use http::header::{HeaderMap, HeaderName, HeaderValue}; + + let mut h = HeaderMap::new(); + for pair in v { + let name: HeaderName = pair.name.parse().or_unexpected()?; + let value: HeaderValue = pair.value.parse().or_unexpected()?; + h.insert(name, value); + } + Ok(Self(h)) + } +} diff --git a/src/domain/manager.rs b/src/domain/manager.rs index 9bc8365..7678e1c 100644 --- a/src/domain/manager.rs +++ b/src/domain/manager.rs @@ -1,11 +1,15 @@ -use crate::domain::{self, acme, checker, gemini, store, tls}; +use crate::domain::{self, acme, checker, config, gemini, store, tls}; use crate::error::unexpected::{self, Mappable}; use crate::{origin, task_stack, util}; -use std::sync; +use std::{collections, sync}; use tokio_util::sync::CancellationToken; -pub type GetSettingsResult = domain::store::GetResult; +pub enum GetSettingsResult { + Stored(domain::Settings), + Builtin(domain::config::ConfigBuiltinDomain), + Proxied(domain::config::ConfigProxiedDomain), +} #[derive(thiserror::Error, Debug)] pub enum GetSettingsError { @@ -37,11 +41,11 @@ pub enum GetFileError { Unexpected(#[from] unexpected::Error), } -impl From for GetFileError { - fn from(e: store::GetError) -> Self { +impl From for GetFileError { + fn from(e: GetSettingsError) -> Self { match e { - store::GetError::NotFound => Self::DomainNotFound, - store::GetError::Unexpected(e) => Self::Unexpected(e), + GetSettingsError::NotFound => Self::DomainNotFound, + GetSettingsError::Unexpected(e) => Self::Unexpected(e), } } } @@ -58,31 +62,10 @@ impl From for GetFileError { } } -#[derive(thiserror::Error, Debug)] -pub enum SyncError { - #[error("not found")] - NotFound, - - #[error("already in progress")] - AlreadyInProgress, - - #[error(transparent)] - Unexpected(#[from] unexpected::Error), -} - -impl From for SyncError { - fn from(e: store::GetError) -> SyncError { - match e { - store::GetError::NotFound => SyncError::NotFound, - store::GetError::Unexpected(e) => SyncError::Unexpected(e), - } - } -} - #[derive(thiserror::Error, Debug)] pub enum SyncWithSettingsError { - #[error("cannot call SyncWithSettings on builtin domain")] - BuiltinDomain, + #[error("domain's settings cannot be modified")] + NotModifiable, #[error("invalid url")] InvalidURL, @@ -128,18 +111,12 @@ impl From for SyncWithSettingsError { } } -impl From for SyncWithSettingsError { - fn from(e: store::SetError) -> SyncWithSettingsError { - match e { - store::SetError::BuiltinDomain => SyncWithSettingsError::BuiltinDomain, - store::SetError::Unexpected(e) => SyncWithSettingsError::Unexpected(e), - } - } -} - pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError; -pub type StoredDomain = domain::store::StoredDomain; +pub struct ManagedDomain { + pub domain: domain::Name, + pub public: bool, +} #[mockall::automock] pub trait Manager: Sync + Send { @@ -167,7 +144,7 @@ pub trait Manager: Sync + Send { domain: &domain::Name, ) -> unexpected::Result>; - fn all_domains(&self) -> Result, unexpected::Error>; + fn all_domains(&self) -> Result, unexpected::Error>; } pub struct ManagerImpl { @@ -176,6 +153,8 @@ pub struct ManagerImpl { domain_checker: checker::DNSChecker, acme_manager: Option>, gemini_store: Option>, + builtins: collections::HashMap, + proxied: collections::HashMap, } impl ManagerImpl { @@ -191,6 +170,8 @@ impl ManagerImpl { domain_checker: checker::DNSChecker, acme_manager: Option, gemini_store: Option, + builtins: collections::HashMap, + proxied: collections::HashMap, ) -> sync::Arc { let manager = sync::Arc::new(ManagerImpl { origin_store: Box::from(origin_store), @@ -199,6 +180,8 @@ impl ManagerImpl { acme_manager: acme_manager .map(|m| Box::new(m) as Box), gemini_store: gemini_store.map(|m| Box::new(m) as Box), + builtins, + proxied, }); task_stack.push_spawn(|canceller| { @@ -212,14 +195,18 @@ impl ManagerImpl { manager } - async fn sync_domain_certs_and_origin( + fn sync_domain_origin( &self, domain: &domain::Name, - settings: &domain::Settings, - ) -> Result<(), SyncWithSettingsError> { - self.origin_store.sync(&settings.origin_descr)?; + origin_descr: &origin::Descr, + ) -> Result<(), origin::SyncError> { + log::info!("Syncing origin {:?} for domain {domain}", origin_descr,); + self.origin_store.sync(origin_descr) + } + async fn sync_domain_certs(&self, domain: &domain::Name) -> unexpected::Result<()> { if let Some(ref gemini_store) = self.gemini_store { + log::info!("Syncing gemini certificate for domain {domain}"); if let Some(_) = gemini_store.get_certificate(domain).or_unexpected()? { return Ok(()); } @@ -233,39 +220,45 @@ impl ManagerImpl { } if let Some(ref acme_manager) = self.acme_manager { + log::info!("Syncing HTTPS certificate for domain {domain}"); acme_manager.sync_domain(domain.clone()).await?; } Ok(()) } - async fn sync_all_domains_once(&self) { - let domains = match self.domain_store.all_domains() { - Ok(domains) => domains, - Err(err) => { - log::error!("Error fetching all domains: {err}"); - return; - } - } - .into_iter(); + async fn sync_all_domains_once(&self) -> unexpected::Result<()> { + let domains = self + .all_domains() + .or_unexpected_while("fetching all domains")? + .into_iter(); - for StoredDomain { domain, .. } in domains { - log::info!("Syncing domain {}", &domain); - - let get_res = match self.domain_store.get(&domain) { - Ok(get_res) => get_res, - Err(err) => { - log::error!("Failed to fetch settings for domain {domain}: {err}"); - return; - } + for ManagedDomain { domain, .. } in domains { + let settings = match self + .get_settings(&domain) + .map_unexpected_while(|| format!("fetching settings for {domain}"))? + { + GetSettingsResult::Stored(settings) => Some(settings), + GetSettingsResult::Builtin(config) => Some(config.settings), + GetSettingsResult::Proxied(_) => None, }; - let settings = get_res.settings; - - if let Err(err) = self.sync_domain_certs_and_origin(&domain, &settings).await { - log::error!("Failed to sync settings for {domain}, settings:{settings:?}: {err}",) + if let Some(settings) = settings { + self.sync_domain_origin(&domain, &settings.origin_descr) + .map_unexpected_while(|| { + format!( + "syncing origin {:?} for domain {domain}", + &settings.origin_descr, + ) + })?; } + + self.sync_domain_certs(&domain) + .await + .map_unexpected_while(|| format!("syncing certs for domain {domain}",))?; } + + Ok(()) } async fn sync_all_domains(&self, canceller: CancellationToken) { @@ -273,7 +266,9 @@ impl ManagerImpl { loop { tokio::select! { _ = canceller.cancelled() => return, - _ = interval.tick() => self.sync_all_domains_once().await, + _ = interval.tick() => if let Err(err) = self.sync_all_domains_once().await { + log::error!("Failed to sync all domains: {err}") + }, } } } @@ -281,7 +276,15 @@ impl ManagerImpl { impl Manager for ManagerImpl { fn get_settings(&self, domain: &domain::Name) -> Result { - Ok(self.domain_store.get(domain)?) + if let Some(config) = self.builtins.get(domain) { + return Ok(GetSettingsResult::Builtin(config.clone())); + } + + if let Some(config) = self.proxied.get(domain) { + return Ok(GetSettingsResult::Proxied(config.clone())); + } + + Ok(GetSettingsResult::Stored(self.domain_store.get(domain)?)) } fn get_file<'store>( @@ -289,7 +292,15 @@ impl Manager for ManagerImpl { domain: &domain::Name, path: &str, ) -> Result { - let settings = self.domain_store.get(domain)?.settings; + let settings = match self.get_settings(domain)? { + GetSettingsResult::Stored(settings) => settings, + GetSettingsResult::Builtin(config) => config.settings, + GetSettingsResult::Proxied(_) => { + return Err( + unexpected::Error::from("can't call get_file on proxied domain").into(), + ); + } + }; let path = settings.process_path(path); @@ -306,14 +317,21 @@ impl Manager for ManagerImpl { settings: domain::Settings, ) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> { Box::pin(async move { + if self.builtins.contains_key(&domain) || self.proxied.contains_key(&domain) { + return Err(SyncWithSettingsError::NotModifiable); + } + let hash = settings .hash() .or_unexpected_while("calculating config hash")?; self.domain_checker.check_domain(&domain, &hash).await?; - self.sync_domain_certs_and_origin(&domain, &settings) - .await?; + self.sync_domain_origin(&domain, &settings.origin_descr)?; + self.sync_domain_certs(&domain) + .await + .or_unexpected_while("syncing domain certs")?; self.domain_store.set(&domain, &settings)?; + Ok(()) }) } @@ -336,8 +354,34 @@ impl Manager for ManagerImpl { self.domain_checker.get_challenge_token(domain) } - fn all_domains(&self) -> Result, unexpected::Error> { - self.domain_store.all_domains() + fn all_domains(&self) -> Result, unexpected::Error> { + let mut res: Vec = self + .domain_store + .all_domains()? + .into_iter() + .map(|domain| ManagedDomain { + domain, + public: true, + }) + .collect(); + + self.builtins + .iter() + .map(|(domain, config)| ManagedDomain { + domain: domain.clone(), + public: config.public, + }) + .collect_into(&mut res); + + self.proxied + .iter() + .map(|(domain, _)| ManagedDomain { + domain: domain.clone(), + public: false, + }) + .collect_into(&mut res); + + Ok(res) } } diff --git a/src/domain/store.rs b/src/domain/store.rs index 3bd8bfa..7e4744a 100644 --- a/src/domain/store.rs +++ b/src/domain/store.rs @@ -1,21 +1,8 @@ -use std::{collections, fs, io, path}; +use std::{fs, io, path}; use crate::domain; use crate::error::unexpected::{self, Intoable, Mappable}; -#[derive(Debug, PartialEq)] -/// Extra information about a domain which is related to how its stored. -pub struct Metadata { - pub builtin: bool, - pub public: bool, -} - -#[derive(Debug, PartialEq)] -pub struct GetResult { - pub settings: domain::Settings, - pub metadata: Metadata, -} - #[derive(thiserror::Error, Debug)] pub enum GetError { #[error("not found")] @@ -25,25 +12,11 @@ pub enum GetError { Unexpected(#[from] unexpected::Error), } -#[derive(thiserror::Error, Debug)] -pub enum SetError { - #[error("cannot call set on builtin domain")] - BuiltinDomain, - - #[error(transparent)] - Unexpected(#[from] unexpected::Error), -} - -pub struct StoredDomain { - pub domain: domain::Name, - pub metadata: Metadata, -} - #[mockall::automock] pub trait Store { - fn get(&self, domain: &domain::Name) -> Result; - fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>; - fn all_domains(&self) -> Result, unexpected::Error>; + fn get(&self, domain: &domain::Name) -> Result; + fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> unexpected::Result<()>; + fn all_domains(&self) -> unexpected::Result>; } pub struct FSStore { @@ -68,7 +41,7 @@ impl FSStore { } impl Store for FSStore { - fn get(&self, domain: &domain::Name) -> Result { + fn get(&self, domain: &domain::Name) -> Result { let path = self.settings_file_path(domain); let settings_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() { io::ErrorKind::NotFound => GetError::NotFound, @@ -80,16 +53,10 @@ impl Store for FSStore { let settings = serde_json::from_reader(settings_file) .map_unexpected_while(|| format!("json parsing {}", path.display()))?; - Ok(GetResult { - settings, - metadata: Metadata { - public: true, - builtin: false, - }, - }) + Ok(settings) } - fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> { + fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> unexpected::Result<()> { let dir_path = self.settings_dir_path(domain); fs::create_dir_all(dir_path.as_path()) .map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?; @@ -104,85 +71,25 @@ impl Store for FSStore { Ok(()) } - fn all_domains(&self) -> Result, unexpected::Error> { + fn all_domains(&self) -> unexpected::Result> { fs::read_dir(&self.dir_path) .or_unexpected()? .map( - |dir_entry_res: io::Result| -> Result { + |dir_entry_res: io::Result| -> unexpected::Result { let domain = dir_entry_res.or_unexpected()?.file_name(); let domain = domain.to_str().ok_or(unexpected::Error::from( "couldn't convert os string to &str", ))?; - Ok(StoredDomain{ - domain: domain.parse().map_unexpected_while(|| format!("parsing {domain} as domain name"))?, - metadata: Metadata{ - public: true, - builtin: false, - }, - }) + Ok(domain + .parse() + .map_unexpected_while(|| format!("parsing {domain} as domain name"))?) }, ) .try_collect() } } -pub struct StoreWithBuiltin { - inner: S, - domains: collections::HashMap, -} - -impl StoreWithBuiltin { - pub fn new( - inner: S, - builtin_domains: collections::HashMap, - ) -> StoreWithBuiltin { - StoreWithBuiltin { - inner, - domains: builtin_domains, - } - } -} - -impl Store for StoreWithBuiltin { - fn get(&self, domain: &domain::Name) -> Result { - if let Some(domain) = self.domains.get(domain) { - return Ok(GetResult { - settings: domain.settings.clone(), - metadata: Metadata { - public: domain.public, - builtin: true, - }, - }); - } - self.inner.get(domain) - } - - fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> { - if self.domains.get(domain).is_some() { - return Err(SetError::BuiltinDomain); - } - self.inner.set(domain, settings) - } - - fn all_domains(&self) -> Result, unexpected::Error> { - let inner_domains = self.inner.all_domains()?; - let mut domains: Vec = self - .domains - .iter() - .map(|(domain, v)| StoredDomain { - domain: domain.clone(), - metadata: Metadata { - public: v.public, - builtin: true, - }, - }) - .collect(); - domains.extend(inner_domains); - Ok(domains) - } -} - #[cfg(test)] mod tests { use super::{Store, *}; @@ -211,20 +118,11 @@ mod tests { assert!(matches!( store.get(&domain), - Err::(GetError::NotFound) + Err::(GetError::NotFound) )); store.set(&domain, &settings).expect("set"); - assert_eq!( - GetResult { - settings, - metadata: Metadata { - public: true, - builtin: false, - }, - }, - store.get(&domain).expect("settings retrieved") - ); + assert_eq!(settings, store.get(&domain).expect("settings retrieved")); let new_settings = domain::Settings { origin_descr: Descr::Git { @@ -236,13 +134,7 @@ mod tests { store.set(&domain, &new_settings).expect("set"); assert_eq!( - GetResult { - settings: new_settings, - metadata: Metadata { - public: true, - builtin: false, - }, - }, + new_settings, store.get(&domain).expect("settings retrieved") ); } diff --git a/src/main.rs b/src/main.rs index bd15032..28acc23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,9 +102,6 @@ async fn main() { domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains")) .expect("domain config store initialization failed"); - let domain_store = - domani::domain::store::StoreWithBuiltin::new(domain_store, config.domain.builtins); - let domain_acme_manager = if config.service.http.https_addr.is_some() { let acme_config = config .domain @@ -146,6 +143,8 @@ async fn main() { domain_checker, domain_acme_manager, domain_gemini_store, + config.domain.builtins.clone(), + config.domain.proxied.clone(), ); let _ = domani::service::http::Service::new( @@ -153,6 +152,7 @@ async fn main() { domain_manager.clone(), domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()), config.service.clone(), + config.domain.proxied.clone(), ); if gemini_enabled { @@ -160,7 +160,8 @@ async fn main() { &mut task_stack, domain_manager.clone(), domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()), - config.service, + config.service.gemini.clone(), + config.domain.proxied.clone(), ); } diff --git a/src/service/gemini.rs b/src/service/gemini.rs index 7a2b605..5de80d2 100644 --- a/src/service/gemini.rs +++ b/src/service/gemini.rs @@ -6,13 +6,14 @@ pub use config::*; use crate::error::unexpected::{self, Mappable}; use crate::{domain, service, task_stack, util}; -use std::sync; +use std::{collections, sync}; use tokio_util::sync::CancellationToken; pub struct Service { domain_manager: sync::Arc, cert_resolver: sync::Arc, - config: service::Config, + config: Config, + proxied: collections::HashMap, } #[derive(thiserror::Error, Debug)] @@ -29,7 +30,8 @@ impl Service { task_stack: &mut task_stack::TaskStack, domain_manager: sync::Arc, cert_resolver: CertResolver, - config: service::Config, + config: Config, + proxied: collections::HashMap, ) -> sync::Arc where CertResolver: rustls::server::ResolvesServerCert + 'static, @@ -38,6 +40,7 @@ impl Service { domain_manager, cert_resolver: sync::Arc::from(cert_resolver), config, + proxied, }); task_stack.push_spawn(|canceller| listen(service.clone(), canceller)); service @@ -111,19 +114,13 @@ impl Service { .await?) } - async fn proxy_conn( - &self, - proxied_domain: &ConfigProxiedDomain, - mut conn: IO, - ) -> unexpected::Result<()> + async fn proxy_conn(&self, proxy_addr: &str, mut conn: IO) -> unexpected::Result<()> where IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, { - let mut proxy_conn = tokio::net::TcpStream::connect(&proxied_domain.url.addr) + let mut proxy_conn = tokio::net::TcpStream::connect(proxy_addr) .await - .map_unexpected_while(|| { - format!("failed to connect to proxy {}", proxied_domain.url.url,) - })?; + .map_unexpected_while(|| format!("failed to connect to proxy {proxy_addr}"))?; _ = tokio::io::copy_bidirectional(&mut conn, &mut proxy_conn).await; @@ -160,10 +157,13 @@ impl Service { })?; // If the domain should be proxied, then proxy it - if let Some(proxied_domain) = self.config.gemini.proxied_domains.get(&domain) { - let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner()); - self.proxy_conn(proxied_domain, prefixed_conn).await?; - return Ok(()); + if let Some(config) = self.proxied.get(&domain) { + if let Some(ref gemini_url) = config.gemini_url { + let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner()); + self.proxy_conn(gemini_url.addr.as_str(), prefixed_conn) + .await?; + return Ok(()); + } } let conn = start.into_stream(tls_config).await.or_unexpected()?; @@ -185,7 +185,6 @@ async fn listen( ) -> unexpected::Result<()> { let addr = &service .config - .gemini .gemini_addr .expect("listen called with gemini_addr not set"); diff --git a/src/service/gemini/config.rs b/src/service/gemini/config.rs index d49283e..6a4d392 100644 --- a/src/service/gemini/config.rs +++ b/src/service/gemini/config.rs @@ -1,79 +1,20 @@ -use crate::domain; -use crate::error::unexpected::{self, Mappable}; - use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, TryFromInto}; - -use std::{collections, net, str::FromStr}; +use std::net; fn default_gemini_addr() -> Option { - Some(net::SocketAddr::from_str("[::]:3965").unwrap()) -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct ConfigProxiedDomainUrl { - pub url: String, - pub addr: String, -} - -impl From for String { - fn from(url: ConfigProxiedDomainUrl) -> Self { - url.url - } -} - -impl TryFrom for ConfigProxiedDomainUrl { - type Error = unexpected::Error; - - fn try_from(url: String) -> Result { - // use http's implementation, should be the same - let parsed = http::Uri::from_str(url.as_str()) - .map_unexpected_while(|| format!("parsing proxy url {url}"))?; - - let scheme = parsed.scheme().map_unexpected_while(|| { - format!("expected a scheme of gemini in the proxy url {url}") - })?; - - if scheme != "gemini" { - return Err(unexpected::Error::from( - format!("scheme of proxy url {url} should be 'gemini'",).as_str(), - )); - } - - match parsed.authority() { - None => Err(unexpected::Error::from( - format!("proxy url {url} should have a host",).as_str(), - )), - Some(authority) => { - let port = authority.port().map(|p| p.as_u16()).unwrap_or(1965); - Ok(ConfigProxiedDomainUrl { - url: url, - addr: format!("{}:{port}", authority.host()), - }) - } - } - } -} - -#[serde_as] -#[derive(Deserialize, Serialize, Clone)] -pub struct ConfigProxiedDomain { - #[serde_as(as = "TryFromInto")] - pub url: ConfigProxiedDomainUrl, + Some("[::]:3965".parse().unwrap()) } #[derive(Deserialize, Serialize, Clone)] pub struct Config { #[serde(default = "default_gemini_addr")] pub gemini_addr: Option, - pub proxied_domains: collections::HashMap, } impl Default for Config { fn default() -> Self { Self { gemini_addr: default_gemini_addr(), - proxied_domains: Default::default(), } } } diff --git a/src/service/http.rs b/src/service/http.rs index 725166a..29bb7c1 100644 --- a/src/service/http.rs +++ b/src/service/http.rs @@ -10,7 +10,7 @@ use http::request::Parts; use hyper::{Body, Method, Request, Response}; use serde::{Deserialize, Serialize}; -use std::{future, net, sync}; +use std::{collections, future, net, sync}; use crate::error::unexpected; use crate::{domain, service, task_stack}; @@ -20,6 +20,7 @@ pub struct Service { cert_resolver: sync::Arc, handlebars: handlebars::Handlebars<'static>, config: service::Config, + proxied: collections::HashMap, } #[derive(Serialize)] @@ -57,6 +58,7 @@ impl Service { domain_manager: sync::Arc, cert_resolver: CertResolver, config: service::Config, + proxied: collections::HashMap, ) -> sync::Arc where CertResolver: rustls::server::ResolvesServerCert + 'static, @@ -68,6 +70,7 @@ impl Service { cert_resolver: sync::Arc::from(cert_resolver), handlebars: tpl::get(), config, + proxied, }); task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller)); @@ -209,8 +212,6 @@ impl Service { } fn domain(&self, args: DomainArgs) -> Response { - use domain::store::Metadata; - #[derive(Serialize)] struct Data { domain: domain::Name, @@ -218,42 +219,11 @@ impl Service { } let settings = match self.domain_manager.get_settings(&args.domain) { - Ok(domain::manager::GetSettingsResult { - metadata: - Metadata { - public: false, - builtin: _, - }, - .. - }) => None, - - Ok(domain::manager::GetSettingsResult { - settings, - metadata: - Metadata { - public: true, - builtin: false, - }, - }) => Some(settings), - - Ok(domain::manager::GetSettingsResult { - metadata: - Metadata { - public: true, - builtin: true, - }, - .. - }) => { - return self.render_error_page( - 403, - format!( - "Settings for domain {} cannot be viewed or modified", - args.domain - ) - .as_str(), - ) + Ok(domain::manager::GetSettingsResult::Stored(settings)) => Some(settings), + Ok(domain::manager::GetSettingsResult::Builtin(config)) => { + config.public.then(|| config.settings) } - + Ok(domain::manager::GetSettingsResult::Proxied(_)) => None, Err(domain::manager::GetSettingsError::NotFound) => None, Err(domain::manager::GetSettingsError::Unexpected(e)) => { return self.internal_error( @@ -355,7 +325,7 @@ impl Service { let error_msg = match sync_result { Ok(_) => None, - Err(domain::manager::SyncWithSettingsError::BuiltinDomain) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()), + Err(domain::manager::SyncWithSettingsError::NotModifiable) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()), Err(domain::manager::SyncWithSettingsError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()), Err(domain::manager::SyncWithSettingsError::InvalidBranchName) => Some("The git repository does not have a branch of the given name, please double check that you input the correct name.".to_string()), Err(domain::manager::SyncWithSettingsError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()), @@ -386,7 +356,7 @@ impl Service { let mut domains: Vec = domains .into_iter() - .filter(|d| d.metadata.public) + .filter(|d| d.public) .map(|d| d.domain.as_str().to_string()) .collect(); @@ -487,23 +457,23 @@ impl Service { } } - if let Some(proxied_domain_config) = self.config.http.proxied_domains.get(&domain) { - return service::http::proxy::serve_http_request( - proxied_domain_config, - client_ip, - req, - req_is_https, - ) - .await - .unwrap_or_else(|e| { - self.internal_error( - format!( - "serving {domain} via proxy {}: {e}", - proxied_domain_config.url.as_ref() - ) - .as_str(), + if let Some(config) = self.proxied.get(&domain) { + if let Some(ref http_url) = config.http_url { + return service::http::proxy::serve_http_request( + http_url.original_url.as_str(), + &config.http_request_headers.0, + client_ip, + req, + req_is_https, ) - }); + .await + .unwrap_or_else(|e| { + self.internal_error( + format!("serving {domain} via proxy {}: {e}", http_url.original_url) + .as_str(), + ) + }); + } } if Some(&domain) == self.config.interface_domain.as_ref() { diff --git a/src/service/http/config.rs b/src/service/http/config.rs index 7212efa..165ae31 100644 --- a/src/service/http/config.rs +++ b/src/service/http/config.rs @@ -1,13 +1,8 @@ -use crate::domain; -use crate::error::unexpected::{self, Mappable}; - use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, TryFromInto}; - -use std::{collections, net, str::FromStr}; +use std::net; fn default_http_addr() -> net::SocketAddr { - net::SocketAddr::from_str("[::]:3080").unwrap() + "[::]:3080".parse().unwrap() } #[derive(Deserialize, Serialize, Clone)] @@ -40,105 +35,6 @@ impl AsRef for ConfigFormMethod { } } -#[derive(Clone)] -pub struct ConfigProxiedDomainUrl(String); - -impl AsRef for ConfigProxiedDomainUrl { - fn as_ref(&self) -> &str { - return &self.0; - } -} - -impl From for String { - fn from(url: ConfigProxiedDomainUrl) -> Self { - url.0 - } -} - -impl TryFrom for ConfigProxiedDomainUrl { - type Error = unexpected::Error; - - fn try_from(url: String) -> Result { - let parsed = http::Uri::from_str(url.as_str()) - .map_unexpected_while(|| format!("parsing proxy url {url}"))?; - - let scheme = parsed - .scheme() - .map_unexpected_while(|| format!("expected a scheme of http in the proxy url {url}"))?; - - if scheme != "http" { - return Err(unexpected::Error::from( - format!("scheme of proxy url {url} should be 'http'",).as_str(), - )); - } - - Ok(ConfigProxiedDomainUrl(url)) - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct ConfigProxiedDomainRequestHeader { - pub name: String, - pub value: String, -} - -#[derive(Clone)] -pub struct ConfigProxiedDomainRequestHeaders(http::header::HeaderMap); - -impl AsRef for ConfigProxiedDomainRequestHeaders { - fn as_ref(&self) -> &http::header::HeaderMap { - &self.0 - } -} - -impl Default for ConfigProxiedDomainRequestHeaders { - fn default() -> Self { - ConfigProxiedDomainRequestHeaders(http::header::HeaderMap::default()) - } -} - -impl TryFrom for Vec { - type Error = http::header::ToStrError; - - fn try_from(h: ConfigProxiedDomainRequestHeaders) -> Result { - let mut v = vec![]; - for (name, value) in &h.0 { - v.push(ConfigProxiedDomainRequestHeader { - name: name.to_string(), - value: value.to_str()?.to_string(), - }) - } - Ok(v) - } -} - -impl TryFrom> for ConfigProxiedDomainRequestHeaders { - type Error = unexpected::Error; - - fn try_from(v: Vec) -> Result { - use http::header::{HeaderMap, HeaderName, HeaderValue}; - - let mut h = HeaderMap::new(); - for pair in v { - let name: HeaderName = pair.name.parse().or_unexpected()?; - let value: HeaderValue = pair.value.parse().or_unexpected()?; - h.insert(name, value); - } - Ok(ConfigProxiedDomainRequestHeaders(h)) - } -} - -#[serde_as] -#[derive(Deserialize, Serialize, Clone)] -pub struct ConfigProxiedDomain { - #[serde_as(as = "TryFromInto")] - pub url: ConfigProxiedDomainUrl, - - #[serde(default)] - #[serde_as(as = "TryFromInto>")] - pub request_headers: ConfigProxiedDomainRequestHeaders, -} - #[derive(Deserialize, Serialize, Clone)] pub struct Config { #[serde(default = "default_http_addr")] @@ -147,9 +43,6 @@ pub struct Config { #[serde(default)] pub form_method: ConfigFormMethod, - - #[serde(default)] - pub proxied_domains: collections::HashMap, } impl Default for Config { @@ -158,7 +51,6 @@ impl Default for Config { http_addr: default_http_addr(), https_addr: None, form_method: Default::default(), - proxied_domains: Default::default(), } } } diff --git a/src/service/http/proxy.rs b/src/service/http/proxy.rs index ec89319..ff5bfd2 100644 --- a/src/service/http/proxy.rs +++ b/src/service/http/proxy.rs @@ -1,15 +1,14 @@ -use crate::error::unexpected::{self}; -use crate::service; -use http::header::HeaderValue; +use crate::error::unexpected; use std::net; pub async fn serve_http_request( - config: &service::http::ConfigProxiedDomain, + proxy_addr: &str, + headers: &http::header::HeaderMap, client_ip: net::IpAddr, mut req: hyper::Request, req_is_https: bool, ) -> unexpected::Result> { - for (name, value) in config.request_headers.as_ref() { + for (name, value) in headers { if value == "" { req.headers_mut().remove(name); continue; @@ -19,18 +18,18 @@ pub async fn serve_http_request( } if req_is_https { - req.headers_mut() - .insert("x-forwarded-proto", HeaderValue::from_static("https")); + req.headers_mut().insert( + "x-forwarded-proto", + http::header::HeaderValue::from_static("https"), + ); } - let url = config.url.as_ref(); - - match hyper_reverse_proxy::call(client_ip, url, req).await { + match hyper_reverse_proxy::call(client_ip, proxy_addr, req).await { Ok(res) => Ok(res), // ProxyError doesn't actually implement Error :facepalm: so we have to format the error // manually Err(e) => Err(unexpected::Error::from( - format!("error while proxying to {url}: {e:?}").as_str(), + format!("error while proxying to {proxy_addr}: {e:?}").as_str(), )), } }