Compare commits

..

2 Commits

Author SHA1 Message Date
6a611c371c 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.
2024-01-15 18:07:49 +01:00
43502f82f9 Remove platform suffix from binary produced by default flake target 2024-01-15 17:07:55 +01:00
6 changed files with 129 additions and 145 deletions

View File

@ -57,7 +57,7 @@
{} {}
supportedSystems; supportedSystems;
# eachCrossSystem [system] (buildSystem: targetSystem: ...) # eachCrossSystem [system] ({buildSystem, targetSystem, isDefault }: ...)
# #
# Returns an attrset with a key "$buildSystem.cross-$targetSystem" for # Returns an attrset with a key "$buildSystem.cross-$targetSystem" for
# every combination of the elements of the array of system strings. The # every combination of the elements of the array of system strings. The
@ -72,13 +72,20 @@
pkgs = mkPkgs buildSystem null; pkgs = mkPkgs buildSystem null;
crosses = builtins.foldl' crosses = builtins.foldl'
(inner: targetSystem: inner // { (inner: targetSystem: inner // {
"cross-${targetSystem}" = callback buildSystem targetSystem; "cross-${targetSystem}" = callback {
inherit buildSystem targetSystem;
isDefault = false;
};
}) })
{ default = callback buildSystem buildSystem; } {}
supportedSystems; supportedSystems;
in in
crosses // (rec { crosses // (rec {
default = callback buildSystem buildSystem; default = callback {
inherit buildSystem;
targetSystem = buildSystem;
isDefault = true;
};
release = let release = let
bins = pkgs.symlinkJoin { bins = pkgs.symlinkJoin {
name = "${default.name}-all-bins"; name = "${default.name}-all-bins";
@ -164,7 +171,7 @@
packages = eachCrossSystem packages = eachCrossSystem
(builtins.attrNames buildTargets) (builtins.attrNames buildTargets)
(buildSystem: targetSystem: let ({ buildSystem, targetSystem, isDefault }: let
pkgs = mkPkgs buildSystem null; pkgs = mkPkgs buildSystem null;
toolchain = mkToolchain buildSystem targetSystem; toolchain = mkToolchain buildSystem targetSystem;
naersk-lib = pkgs.callPackage naersk { naersk-lib = pkgs.callPackage naersk {
@ -177,7 +184,7 @@
strictDeps = true; strictDeps = true;
doCheck = false; doCheck = false;
release = true; release = true;
postInstall = '' postInstall = if isDefault then "" else ''
cd "$out"/bin cd "$out"/bin
for f in "$(ls)"; do for f in "$(ls)"; do
if ext="$(echo "$f" | grep -oP '\.[a-z]+$')"; then if ext="$(echo "$f" | grep -oP '\.[a-z]+$')"; then
@ -193,7 +200,7 @@
devShells = eachCrossSystem devShells = eachCrossSystem
(builtins.attrNames buildTargets) (builtins.attrNames buildTargets)
(buildSystem: targetSystem: let ({ buildSystem, targetSystem, isDefault }: let
pkgs = mkPkgs buildSystem null; pkgs = mkPkgs buildSystem null;
toolchain = mkToolchain buildSystem targetSystem; toolchain = mkToolchain buildSystem targetSystem;
in in

View File

@ -30,38 +30,7 @@ impl From<store::GetError> for GetSettingsError {
} }
} }
#[derive(thiserror::Error, Debug)] pub type GetFileError = origin::GetFileError;
pub enum GetFileError {
#[error("domain not found")]
DomainNotFound,
#[error("file not found")]
FileNotFound,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
}
impl From<GetSettingsError> for GetFileError {
fn from(e: GetSettingsError) -> Self {
match e {
GetSettingsError::NotFound => Self::DomainNotFound,
GetSettingsError::Unexpected(e) => Self::Unexpected(e),
}
}
}
impl From<origin::GetFileError> 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),
}
}
}
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum SyncWithSettingsError { pub enum SyncWithSettingsError {
@ -125,7 +94,7 @@ pub trait Manager: Sync + Send {
fn get_file( fn get_file(
&self, &self,
domain: &domain::Name, settings: &domain::Settings,
path: &str, path: &str,
) -> Result<util::BoxByteStream, GetFileError>; ) -> Result<util::BoxByteStream, GetFileError>;
@ -425,34 +394,12 @@ impl Manager for ManagerImpl {
fn get_file( fn get_file(
&self, &self,
domain: &domain::Name, settings: &domain::Settings,
path: &str, path: &str,
) -> Result<util::BoxByteStream, GetFileError> { ) -> Result<util::BoxByteStream, GetFileError> {
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 path = settings.process_path(path);
self.origin_store
let f = self .get_file(&settings.origin_descr, path.as_ref())
.origin_store
.get_file(&settings.origin_descr, path.as_ref())?;
Ok(f)
} }
fn sync_with_settings( fn sync_with_settings(

View File

@ -176,8 +176,6 @@ async fn main() {
domain_manager.clone(), domain_manager.clone(),
domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()), domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()),
config.service.clone(), config.service.clone(),
config.domain.proxied_domains.clone(),
config.domain.interface_domain.clone(),
); );
if gemini_enabled { if gemini_enabled {
@ -186,7 +184,6 @@ async fn main() {
domain_manager.clone(), domain_manager.clone(),
domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()), domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()),
config.service.gemini.clone(), config.service.gemini.clone(),
config.domain.proxied_domains.clone(),
); );
} }

View File

@ -6,14 +6,13 @@ pub use config::*;
use crate::error::unexpected::{self, Mappable}; use crate::error::unexpected::{self, Mappable};
use crate::{domain, service, task_stack, util}; use crate::{domain, service, task_stack, util};
use std::{collections, sync}; use std::sync;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
pub struct Service { pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>, domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
config: Config, config: Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -31,7 +30,6 @@ impl Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>, domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: CertResolver, cert_resolver: CertResolver,
config: Config, config: Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
) -> sync::Arc<Service> ) -> sync::Arc<Service>
where where
CertResolver: rustls::server::ResolvesServerCert + 'static, CertResolver: rustls::server::ResolvesServerCert + 'static,
@ -40,7 +38,6 @@ impl Service {
domain_manager, domain_manager,
cert_resolver: sync::Arc::from(cert_resolver), cert_resolver: sync::Arc::from(cert_resolver),
config, config,
proxied,
}); });
task_stack.push_spawn(|canceller| listen(service.clone(), canceller)); task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
service service
@ -74,7 +71,11 @@ impl Service {
Ok(()) Ok(())
} }
async fn serve_conn<IO>(&self, domain: &domain::Name, conn: IO) -> Result<(), HandleConnError> async fn serve_conn<IO>(
&self,
settings: &domain::Settings,
conn: IO,
) -> Result<(), HandleConnError>
where where
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, 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 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, Ok(f) => f,
Err(domain::manager::GetFileError::DomainNotFound) => { Err(GetFileError::FileNotFound) => {
return Err(unexpected::Error::from("domain not found when serving file").into())
}
Err(domain::manager::GetFileError::FileNotFound) => {
return Ok(self.respond_conn(w, "51", "File not found", None).await?) 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); let content_type = service::guess_mime(&path);
@ -156,8 +165,14 @@ impl Service {
)) ))
})?; })?;
// If the domain should be proxied, then proxy it use domain::manager::{GetSettingsError, GetSettingsResult};
if let Some(config) = self.proxied.get(&domain) {
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 { if let Some(ref gemini_url) = config.gemini_url {
let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner()); let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner());
self.proxy_conn(gemini_url.addr.as_str(), prefixed_conn) 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()?; 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) => { Err(err) => {
return Err(unexpected::Error::from( return Err(unexpected::Error::from(

View File

@ -10,7 +10,7 @@ use http::request::Parts;
use hyper::{Body, Method, Request, Response}; use hyper::{Body, Method, Request, Response};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections, future, net, sync}; use std::{future, net, sync};
use crate::error::unexpected::{self, Mappable}; use crate::error::unexpected::{self, Mappable};
use crate::{domain, service, task_stack}; use crate::{domain, service, task_stack};
@ -50,8 +50,6 @@ pub struct Service {
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
handlebars: handlebars::Handlebars<'static>, handlebars: handlebars::Handlebars<'static>,
config: service::Config, config: service::Config,
proxied_domains: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
interface_domain: Option<domain::Name>,
} }
impl Service { impl Service {
@ -60,8 +58,6 @@ impl Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>, domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: CertResolver, cert_resolver: CertResolver,
config: service::Config, config: service::Config,
proxied_domains: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
interface_domain: Option<domain::Name>,
) -> sync::Arc<Service> ) -> sync::Arc<Service>
where where
CertResolver: rustls::server::ResolvesServerCert + 'static, CertResolver: rustls::server::ResolvesServerCert + 'static,
@ -71,8 +67,6 @@ impl Service {
cert_resolver: sync::Arc::from(cert_resolver), cert_resolver: sync::Arc::from(cert_resolver),
handlebars: tpl::get(), handlebars: tpl::get(),
config, config,
proxied_domains,
interface_domain,
}); });
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller)); 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<Body>) -> Response<Body> { async fn serve_origin(&self, settings: domain::Settings, req: Request<Body>) -> Response<Body> {
let path = service::append_index_to_path(req.uri().path(), "index.html"); 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)), Ok(f) => self.serve(200, &path, Body::wrap_stream(f)),
Err(domain::manager::GetFileError::DomainNotFound) => { Err(GetFileError::FileNotFound) => self.render_error_page(404, "File not found"),
self.render_error_page(404, "Unknown domain name") Err(GetFileError::DescrNotSynced) => self.internal_error(
} format!(
Err(domain::manager::GetFileError::FileNotFound) => { "Backend for {:?} has not yet been synced",
self.render_error_page(404, "File not found") settings.origin_descr
} )
Err(domain::manager::GetFileError::Unexpected(e)) => { .as_str(),
),
Err(GetFileError::Unexpected(e)) => {
self.internal_error(format!("failed to fetch file {path}: {e}").as_str()) 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. // everything else must use https if possible.
let https_upgradable = self.https_enabled() && !req_is_https; let https_upgradable = self.https_enabled() && !req_is_https;
if let Some(config) = self.proxied_domains.get(&domain) { let settings = {
if config.http_url.is_none() { use domain::manager::{GetSettingsError, GetSettingsResult};
return self.render_error_page(404, "Domain not found"); 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(); self.serve_origin(settings, req).await
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
} }
} }

View File

@ -14,12 +14,6 @@ pub async fn listen_http(
) -> Result<(), unexpected::Error> { ) -> Result<(), unexpected::Error> {
let addr = service.config.http.http_addr; 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 make_service = hyper::service::make_service_fn(move |conn: &AddrStream| {
let service = service.clone(); let service = service.clone();
let client_ip = conn.remote_addr().ip(); let client_ip = conn.remote_addr().ip();
@ -36,7 +30,7 @@ pub async fn listen_http(
async move { Ok::<_, convert::Infallible>(hyper_service) } 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 server = hyper::Server::bind(&addr).serve(make_service);
let graceful = server.with_graceful_shutdown(async { let graceful = server.with_graceful_shutdown(async {
@ -53,12 +47,6 @@ pub async fn listen_https(
let cert_resolver = service.cert_resolver.clone(); let cert_resolver = service.cert_resolver.clone();
let addr = service.config.http.https_addr.unwrap(); 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<AddrStream>| { let make_service = hyper::service::make_service_fn(move |conn: &TlsStream<AddrStream>| {
let service = service.clone(); let service = service.clone();
let client_ip = conn.get_ref().0.remote_addr().ip(); 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); 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); let server = hyper::Server::builder(incoming).serve(make_service);