From 6a611c371ce330fc8469858895494374c07a4635 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 15 Jan 2024 18:07:49 +0100 Subject: [PATCH] Retrieve domain settings within each service, rather than in domain manager This allows for fewer dependencies when initializing the services, and more precise handling of the different cases. --- src/domain/manager.rs | 63 ++-------------------- src/main.rs | 3 -- src/service/gemini.rs | 61 ++++++++++++++++----- src/service/http.rs | 110 +++++++++++++++++++++----------------- src/service/http/tasks.rs | 16 +----- 5 files changed, 115 insertions(+), 138 deletions(-) diff --git a/src/domain/manager.rs b/src/domain/manager.rs index 811bab2..d7b9ff1 100644 --- a/src/domain/manager.rs +++ b/src/domain/manager.rs @@ -30,38 +30,7 @@ impl From for GetSettingsError { } } -#[derive(thiserror::Error, Debug)] -pub enum GetFileError { - #[error("domain not found")] - DomainNotFound, - - #[error("file not found")] - FileNotFound, - - #[error(transparent)] - Unexpected(#[from] unexpected::Error), -} - -impl From for GetFileError { - fn from(e: GetSettingsError) -> Self { - match e { - GetSettingsError::NotFound => Self::DomainNotFound, - GetSettingsError::Unexpected(e) => Self::Unexpected(e), - } - } -} - -impl From for GetFileError { - fn from(e: origin::GetFileError) -> Self { - match e { - origin::GetFileError::DescrNotSynced => { - Self::Unexpected(unexpected::Error::from("origin descr not synced")) - } - origin::GetFileError::FileNotFound => Self::FileNotFound, - origin::GetFileError::Unexpected(e) => Self::Unexpected(e), - } - } -} +pub type GetFileError = origin::GetFileError; #[derive(thiserror::Error, Debug)] pub enum SyncWithSettingsError { @@ -125,7 +94,7 @@ pub trait Manager: Sync + Send { fn get_file( &self, - domain: &domain::Name, + settings: &domain::Settings, path: &str, ) -> Result; @@ -425,34 +394,12 @@ impl Manager for ManagerImpl { fn get_file( &self, - domain: &domain::Name, + settings: &domain::Settings, path: &str, ) -> Result { - 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(), - ); - } - GetSettingsResult::Interface => { - return Err( - unexpected::Error::from("can't call get_file on interface domain").into(), - ); - } - GetSettingsResult::External(_) => { - return Err(GetFileError::DomainNotFound); - } - }; - let path = settings.process_path(path); - - let f = self - .origin_store - .get_file(&settings.origin_descr, path.as_ref())?; - - Ok(f) + self.origin_store + .get_file(&settings.origin_descr, path.as_ref()) } fn sync_with_settings( diff --git a/src/main.rs b/src/main.rs index 10f5be4..f1dfa98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -176,8 +176,6 @@ async fn main() { domain_manager.clone(), domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()), config.service.clone(), - config.domain.proxied_domains.clone(), - config.domain.interface_domain.clone(), ); if gemini_enabled { @@ -186,7 +184,6 @@ async fn main() { domain_manager.clone(), domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()), config.service.gemini.clone(), - config.domain.proxied_domains.clone(), ); } diff --git a/src/service/gemini.rs b/src/service/gemini.rs index 68c3dcf..ebf0b5b 100644 --- a/src/service/gemini.rs +++ b/src/service/gemini.rs @@ -6,14 +6,13 @@ pub use config::*; use crate::error::unexpected::{self, Mappable}; use crate::{domain, service, task_stack, util}; -use std::{collections, sync}; +use std::sync; use tokio_util::sync::CancellationToken; pub struct Service { domain_manager: sync::Arc, cert_resolver: sync::Arc, config: Config, - proxied: collections::HashMap, } #[derive(thiserror::Error, Debug)] @@ -31,7 +30,6 @@ impl Service { domain_manager: sync::Arc, cert_resolver: CertResolver, config: Config, - proxied: collections::HashMap, ) -> sync::Arc where CertResolver: rustls::server::ResolvesServerCert + 'static, @@ -40,7 +38,6 @@ impl Service { domain_manager, cert_resolver: sync::Arc::from(cert_resolver), config, - proxied, }); task_stack.push_spawn(|canceller| listen(service.clone(), canceller)); service @@ -74,7 +71,11 @@ impl Service { Ok(()) } - async fn serve_conn(&self, domain: &domain::Name, conn: IO) -> Result<(), HandleConnError> + async fn serve_conn( + &self, + settings: &domain::Settings, + conn: IO, + ) -> Result<(), HandleConnError> where IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, { @@ -96,15 +97,23 @@ impl Service { let path = service::append_index_to_path(req.path(), "index.gmi"); - let f = match self.domain_manager.get_file(domain, &path) { + use domain::manager::GetFileError; + let f = match self.domain_manager.get_file(settings, &path) { Ok(f) => f, - Err(domain::manager::GetFileError::DomainNotFound) => { - return Err(unexpected::Error::from("domain not found when serving file").into()) - } - Err(domain::manager::GetFileError::FileNotFound) => { + Err(GetFileError::FileNotFound) => { return Ok(self.respond_conn(w, "51", "File not found", None).await?) } - Err(domain::manager::GetFileError::Unexpected(e)) => return Err(e.into()), + Err(GetFileError::DescrNotSynced) => { + return Err(unexpected::Error::from( + format!( + "Backend for {:?} has not yet been synced", + settings.origin_descr + ) + .as_str(), + ) + .into()) + } + Err(GetFileError::Unexpected(e)) => return Err(e.into()), }; let content_type = service::guess_mime(&path); @@ -156,8 +165,14 @@ impl Service { )) })?; - // If the domain should be proxied, then proxy it - if let Some(config) = self.proxied.get(&domain) { + use domain::manager::{GetSettingsError, GetSettingsResult}; + + let get_settings_res = self.domain_manager.get_settings(&domain); + + // If the domain should be proxied, then proxy it. This case gets checked before + // any of the others because the others require terminating the TLS stream to be + // handled. + if let Ok(GetSettingsResult::Proxied(ref config)) = get_settings_res { 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) @@ -166,8 +181,26 @@ impl Service { } } + // Terminate TLS stream let conn = start.into_stream(tls_config).await.or_unexpected()?; - self.serve_conn(&domain, conn).await + + let settings = match get_settings_res { + Ok(GetSettingsResult::Stored(settings)) => settings, + Ok(GetSettingsResult::Builtin(config)) => config.settings, + Ok(GetSettingsResult::Proxied(_)) => { + panic!("proxied case already handled") + } + Ok(GetSettingsResult::Interface) + | Ok(GetSettingsResult::External(_)) + | Err(GetSettingsError::NotFound) => { + return Ok(self + .respond_conn(conn, "51", "File not found", None) + .await?) + } + Err(GetSettingsError::Unexpected(e)) => return Err(e.into()), + }; + + self.serve_conn(&settings, conn).await } Err(err) => { return Err(unexpected::Error::from( diff --git a/src/service/http.rs b/src/service/http.rs index 36b9950..38d340d 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::{collections, future, net, sync}; +use std::{future, net, sync}; use crate::error::unexpected::{self, Mappable}; use crate::{domain, service, task_stack}; @@ -50,8 +50,6 @@ pub struct Service { cert_resolver: sync::Arc, handlebars: handlebars::Handlebars<'static>, config: service::Config, - proxied_domains: collections::HashMap, - interface_domain: Option, } impl Service { @@ -60,8 +58,6 @@ impl Service { domain_manager: sync::Arc, cert_resolver: CertResolver, config: service::Config, - proxied_domains: collections::HashMap, - interface_domain: Option, ) -> sync::Arc where CertResolver: rustls::server::ResolvesServerCert + 'static, @@ -71,8 +67,6 @@ impl Service { cert_resolver: sync::Arc::from(cert_resolver), handlebars: tpl::get(), config, - proxied_domains, - interface_domain, }); task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller)); @@ -208,18 +202,21 @@ impl Service { ) } - async fn serve_origin(&self, domain: domain::Name, req: Request) -> Response { + async fn serve_origin(&self, settings: domain::Settings, req: Request) -> Response { let path = service::append_index_to_path(req.uri().path(), "index.html"); - match self.domain_manager.get_file(&domain, &path) { + use domain::manager::GetFileError; + match self.domain_manager.get_file(&settings, &path) { Ok(f) => self.serve(200, &path, Body::wrap_stream(f)), - Err(domain::manager::GetFileError::DomainNotFound) => { - self.render_error_page(404, "Unknown domain name") - } - Err(domain::manager::GetFileError::FileNotFound) => { - self.render_error_page(404, "File not found") - } - Err(domain::manager::GetFileError::Unexpected(e)) => { + Err(GetFileError::FileNotFound) => self.render_error_page(404, "File not found"), + Err(GetFileError::DescrNotSynced) => self.internal_error( + format!( + "Backend for {:?} has not yet been synced", + settings.origin_descr + ) + .as_str(), + ), + Err(GetFileError::Unexpected(e)) => { self.internal_error(format!("failed to fetch file {path}: {e}").as_str()) } } @@ -546,41 +543,56 @@ impl Service { // everything else must use https if possible. let https_upgradable = self.https_enabled() && !req_is_https; - if let Some(config) = self.proxied_domains.get(&domain) { - if config.http_url.is_none() { - return self.render_error_page(404, "Domain not found"); + let settings = { + use domain::manager::{GetSettingsError, GetSettingsResult}; + match self.domain_manager.get_settings(&domain) { + Ok(GetSettingsResult::Stored(settings)) => settings, + Ok(GetSettingsResult::Builtin(config)) => config.settings, + Ok(GetSettingsResult::Proxied(config)) => { + if config.http_url.is_none() { + return self.render_error_page(404, "Domain not found"); + } else if https_upgradable && !config.https_disabled { + return self.https_redirect(domain, req); + } + + let http_url = config.http_url.as_ref().unwrap(); + + 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(), + ) + }); + } + Ok(GetSettingsResult::Interface) => { + if https_upgradable { + return self.https_redirect(domain, req); + } + return self.serve_interface(req).await; + } + Ok(GetSettingsResult::External(_)) => { + return self.render_error_page(404, "Unknown domain name") + } + Err(GetSettingsError::NotFound) => { + return self.render_error_page(404, "Unknown domain name") + } + Err(GetSettingsError::Unexpected(e)) => { + return self.internal_error( + format!("failed to fetch settings for domain {domain}: {e}").as_str(), + ) + } } + }; - let http_url = config.http_url.as_ref().unwrap(); - - if https_upgradable && !config.https_disabled { - return self.https_redirect(domain, req); - } - - 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 https_upgradable { - return self.https_redirect(domain, req); - } - - if Some(&domain) == self.interface_domain.as_ref() { - return self.serve_interface(req).await; - } - - self.serve_origin(domain, req).await + self.serve_origin(settings, req).await } } diff --git a/src/service/http/tasks.rs b/src/service/http/tasks.rs index f26376b..87c5fd1 100644 --- a/src/service/http/tasks.rs +++ b/src/service/http/tasks.rs @@ -14,12 +14,6 @@ pub async fn listen_http( ) -> Result<(), unexpected::Error> { let addr = service.config.http.http_addr; - // only used for logging - let listen_host = service - .interface_domain - .clone() - .map_or(addr.ip().to_string(), |ref d| d.as_str().to_string()); - let make_service = hyper::service::make_service_fn(move |conn: &AddrStream| { let service = service.clone(); let client_ip = conn.remote_addr().ip(); @@ -36,7 +30,7 @@ pub async fn listen_http( async move { Ok::<_, convert::Infallible>(hyper_service) } }); - log::info!("Listening on http://{}:{}", listen_host, addr.port(),); + log::info!("Listening on http://{}", &addr); let server = hyper::Server::bind(&addr).serve(make_service); let graceful = server.with_graceful_shutdown(async { @@ -53,12 +47,6 @@ pub async fn listen_https( let cert_resolver = service.cert_resolver.clone(); let addr = service.config.http.https_addr.unwrap(); - // only used for logging - let listen_host = service - .interface_domain - .clone() - .map_or(addr.ip().to_string(), |ref d| d.as_str().to_string()); - let make_service = hyper::service::make_service_fn(move |conn: &TlsStream| { let service = service.clone(); let client_ip = conn.get_ref().0.remote_addr().ip(); @@ -97,7 +85,7 @@ pub async fn listen_https( let incoming = hyper::server::accept::from_stream(incoming); - log::info!("Listening on https://{}:{}", listen_host, addr.port()); + log::info!("Listening on https://{}", addr); let server = hyper::Server::builder(incoming).serve(make_service);