Compare commits

..

2 Commits

Author SHA1 Message Date
Brian Picciano
92f7d3d52a Ensure that the interface_domain gets a cert requested for it 2023-08-04 12:31:04 +02:00
Brian Picciano
818c728258 Move interface_domain under domain configuration 2023-08-04 10:34:09 +02:00
7 changed files with 84 additions and 57 deletions

View File

@ -64,10 +64,17 @@ domain:
# renewed. # renewed.
#contact_email: REQUIRED if service.http.https_addr is set #contact_email: REQUIRED if service.http.https_addr is set
# The domain name which will be used to serve the web interface of Domani. If
# service.http.https_addr is enabled then an HTTPS certificate for this domain
# will be retrieved automatically.
#
# This can be set to null to disable the web interface entirely.
#interface_domain: "localhost"
# builtins are domains whose configuration is built into domani. These domains # builtins are domains whose configuration is built into domani. These domains
# are not able to be configured via the web interface, and will be hidden from # are not able to be configured via the web interface, and will be hidden from
# it unless the `public` key is set to true. # it unless the `public` key is set to true.
#builtins: #builtin_domains:
# An example built-in domain backed by a git repo. # An example built-in domain backed by a git repo.
#git.example.com: #git.example.com:
@ -79,13 +86,14 @@ domain:
# domain list, but will not be configurable in the web interface # domain list, but will not be configurable in the web interface
#public: false #public: false
#proxied: #proxied_domains:
# An example built-in domain backed by an gemini and HTTP reverse-proxies to # An example proxied domain backed by an gemini and HTTP reverse-proxies to
# other backends. # other backends.
# #
# HTTP requests will be proxied to http_url, and gemini requests will be # HTTP requests will be proxied to http_url, and gemini requests will be
# proxied to gemini_url. Either can be null. # proxied to gemini_url. Either can be null to disable serving on that
# protocol.
# #
# HTTP requests to the backing service will automatically have # HTTP requests to the backing service will automatically have
# X-Forwarded-For and (if HTTPS) X-Forwarded-Proto headers added to them. # X-Forwarded-For and (if HTTPS) X-Forwarded-Proto headers added to them.
@ -106,7 +114,8 @@ domain:
# - name: X-HEADER-TO-DELETE # - name: X-HEADER-TO-DELETE
# value: "" # value: ""
# Set to true to prevent the domain from being served over https. # Set to true to prevent the domain from being served over https, even if
# http_url is set.
#https_disabled: false #https_disabled: false
service: service:
@ -132,13 +141,6 @@ service:
#- kind: CNAME #- kind: CNAME
# name: domain.com # name: domain.com
# The domain name which will be used to serve the web interface of Domani. If
# service.http.https_addr is enabled then an HTTPS certificate for this domain
# will be retrieved automatically.
#
# This can be set to null to disable the web interface entirely.
#interface_domain: "localhost"
#http: #http:
# The address to listen for HTTP requests on. This must use port 80 if # The address to listen for HTTP requests on. This must use port 80 if

View File

@ -9,7 +9,11 @@ fn default_resolver_addr() -> net::SocketAddr {
net::SocketAddr::from_str("1.1.1.1:53").unwrap() net::SocketAddr::from_str("1.1.1.1:53").unwrap()
} }
#[derive(Deserialize, Serialize)] fn default_interface_domain() -> Option<domain::Name> {
Some(domain::Name::from_str("localhost").unwrap())
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ConfigDNS { pub struct ConfigDNS {
#[serde(default = "default_resolver_addr")] #[serde(default = "default_resolver_addr")]
pub resolver_addr: net::SocketAddr, pub resolver_addr: net::SocketAddr,
@ -23,7 +27,7 @@ impl Default for ConfigDNS {
} }
} }
#[derive(Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct ConfigACME { pub struct ConfigACME {
pub contact_email: String, pub contact_email: String,
} }
@ -54,7 +58,7 @@ pub struct ConfigProxiedDomain {
pub https_disabled: bool, pub https_disabled: bool,
} }
#[derive(Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct Config { pub struct Config {
pub store_dir_path: path::PathBuf, pub store_dir_path: path::PathBuf,
#[serde(default)] #[serde(default)]
@ -62,8 +66,11 @@ pub struct Config {
pub acme: Option<ConfigACME>, pub acme: Option<ConfigACME>,
#[serde(default)] #[serde(default)]
pub builtins: collections::HashMap<domain::Name, ConfigBuiltinDomain>, pub builtin_domains: collections::HashMap<domain::Name, ConfigBuiltinDomain>,
#[serde(default)] #[serde(default)]
pub proxied: collections::HashMap<domain::Name, ConfigProxiedDomain>, pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
#[serde(default = "default_interface_domain")]
pub interface_domain: Option<domain::Name>,
} }

View File

@ -1,14 +1,15 @@
use crate::domain::{self, acme, checker, config, gemini, store, tls}; use crate::domain::{self, acme, checker, gemini, store, tls};
use crate::error::unexpected::{self, Mappable}; use crate::error::unexpected::{self, Mappable};
use crate::{origin, task_stack, util}; use crate::{origin, task_stack, util};
use std::{collections, sync}; use std::sync;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
pub enum GetSettingsResult { pub enum GetSettingsResult {
Stored(domain::Settings), Stored(domain::Settings),
Builtin(domain::config::ConfigBuiltinDomain), Builtin(domain::config::ConfigBuiltinDomain),
Proxied(domain::config::ConfigProxiedDomain), Proxied(domain::config::ConfigProxiedDomain),
Interface,
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -153,8 +154,7 @@ pub struct ManagerImpl {
domain_checker: checker::DNSChecker, domain_checker: checker::DNSChecker,
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>, acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
gemini_store: Option<Box<dyn gemini::Store + Send + Sync>>, gemini_store: Option<Box<dyn gemini::Store + Send + Sync>>,
builtins: collections::HashMap<domain::Name, config::ConfigBuiltinDomain>, config: domain::Config,
proxied: collections::HashMap<domain::Name, config::ConfigProxiedDomain>,
} }
impl ManagerImpl { impl ManagerImpl {
@ -170,8 +170,7 @@ impl ManagerImpl {
domain_checker: checker::DNSChecker, domain_checker: checker::DNSChecker,
acme_manager: Option<AcmeManager>, acme_manager: Option<AcmeManager>,
gemini_store: Option<GeminiStore>, gemini_store: Option<GeminiStore>,
builtins: collections::HashMap<domain::Name, config::ConfigBuiltinDomain>, config: domain::Config,
proxied: collections::HashMap<domain::Name, config::ConfigProxiedDomain>,
) -> sync::Arc<Self> { ) -> sync::Arc<Self> {
let manager = sync::Arc::new(ManagerImpl { let manager = sync::Arc::new(ManagerImpl {
origin_store: Box::from(origin_store), origin_store: Box::from(origin_store),
@ -180,8 +179,7 @@ impl ManagerImpl {
acme_manager: acme_manager acme_manager: acme_manager
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>), .map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>), gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>),
builtins, config,
proxied,
}); });
task_stack.push_spawn(|canceller| { task_stack.push_spawn(|canceller| {
@ -247,6 +245,8 @@ impl ManagerImpl {
// A proxied domain never needs gemini certs, since gemini requests will be // A proxied domain never needs gemini certs, since gemini requests will be
// transparently proxied to the backing server anyway. // transparently proxied to the backing server anyway.
GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false), GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false),
GetSettingsResult::Interface => (None, true, false),
}; };
if let Some(settings) = settings { if let Some(settings) = settings {
@ -289,11 +289,15 @@ impl ManagerImpl {
impl Manager for ManagerImpl { impl Manager for ManagerImpl {
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError> { fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError> {
if let Some(config) = self.builtins.get(domain) { if Some(domain) == self.config.interface_domain.as_ref() {
return Ok(GetSettingsResult::Interface);
}
if let Some(config) = self.config.builtin_domains.get(domain) {
return Ok(GetSettingsResult::Builtin(config.clone())); return Ok(GetSettingsResult::Builtin(config.clone()));
} }
if let Some(config) = self.proxied.get(domain) { if let Some(config) = self.config.proxied_domains.get(domain) {
return Ok(GetSettingsResult::Proxied(config.clone())); return Ok(GetSettingsResult::Proxied(config.clone()));
} }
@ -313,6 +317,11 @@ impl Manager for ManagerImpl {
unexpected::Error::from("can't call get_file on proxied domain").into(), 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(),
);
}
}; };
let path = settings.process_path(path); let path = settings.process_path(path);
@ -330,7 +339,11 @@ impl Manager for ManagerImpl {
settings: domain::Settings, settings: domain::Settings,
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> { ) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
Box::pin(async move { Box::pin(async move {
if self.builtins.contains_key(&domain) || self.proxied.contains_key(&domain) { let is_interface = Some(&domain) == self.config.interface_domain.as_ref();
let is_builtin = self.config.builtin_domains.contains_key(&domain);
let is_proxied = self.config.proxied_domains.contains_key(&domain);
if is_interface || is_builtin || is_proxied {
return Err(SyncWithSettingsError::NotModifiable); return Err(SyncWithSettingsError::NotModifiable);
} }
@ -384,7 +397,8 @@ impl Manager for ManagerImpl {
}) })
.collect(); .collect();
self.builtins self.config
.builtin_domains
.iter() .iter()
.map(|(domain, config)| ManagedDomain { .map(|(domain, config)| ManagedDomain {
domain: domain.clone(), domain: domain.clone(),
@ -392,7 +406,8 @@ impl Manager for ManagerImpl {
}) })
.collect_into(&mut res); .collect_into(&mut res);
self.proxied self.config
.proxied_domains
.iter() .iter()
.map(|(domain, _)| ManagedDomain { .map(|(domain, _)| ManagedDomain {
domain: domain.clone(), domain: domain.clone(),
@ -400,6 +415,13 @@ impl Manager for ManagerImpl {
}) })
.collect_into(&mut res); .collect_into(&mut res);
if let Some(ref interface_domain) = self.config.interface_domain {
res.push(ManagedDomain {
domain: interface_domain.clone(),
public: false,
})
}
Ok(res) Ok(res)
} }
} }

View File

@ -60,7 +60,7 @@ async fn main() {
// Since the interface domain _must_ point to the service (otherwise it wouldn't work) it's // Since the interface domain _must_ point to the service (otherwise it wouldn't work) it's
// reasonable to assume that a CNAME on any domain would suffice to point that domain to // reasonable to assume that a CNAME on any domain would suffice to point that domain to
// the service. // the service.
if let Some(ref interface_domain) = config.service.interface_domain { if let Some(ref interface_domain) = config.domain.interface_domain {
let interface_cname = domani::service::ConfigDNSRecord::CNAME { let interface_cname = domani::service::ConfigDNSRecord::CNAME {
name: interface_domain.clone(), name: interface_domain.clone(),
}; };
@ -106,6 +106,7 @@ async fn main() {
let acme_config = config let acme_config = config
.domain .domain
.acme .acme
.clone()
.expect("acme configuration must be set if https is enabled"); .expect("acme configuration must be set if https is enabled");
let domain_acme_store = let domain_acme_store =
@ -143,8 +144,7 @@ async fn main() {
domain_checker, domain_checker,
domain_acme_manager, domain_acme_manager,
domain_gemini_store, domain_gemini_store,
config.domain.builtins.clone(), config.domain.clone(),
config.domain.proxied.clone(),
); );
let _ = domani::service::http::Service::new( let _ = domani::service::http::Service::new(
@ -152,7 +152,8 @@ 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.clone(), config.domain.proxied_domains.clone(),
config.domain.interface_domain.clone(),
); );
if gemini_enabled { if gemini_enabled {
@ -161,7 +162,7 @@ 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.clone(), config.domain.proxied_domains.clone(),
); );
} }

View File

@ -1,10 +1,6 @@
use crate::{domain, service}; use crate::{domain, service};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{net, str::FromStr}; use std::net;
fn default_interface_domain() -> Option<domain::Name> {
Some(domain::Name::from_str("localhost").unwrap())
}
#[derive(Serialize, Deserialize, Clone, PartialEq)] #[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "kind")] #[serde(tag = "kind")]
@ -31,9 +27,6 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub dns_records: Vec<ConfigDNSRecord>, pub dns_records: Vec<ConfigDNSRecord>,
#[serde(default = "default_interface_domain")]
pub interface_domain: Option<domain::Name>,
#[serde(default)] #[serde(default)]
pub http: service::http::Config, pub http: service::http::Config,

View File

@ -15,14 +15,6 @@ use std::{collections, future, net, sync};
use crate::error::unexpected; use crate::error::unexpected;
use crate::{domain, service, task_stack}; use crate::{domain, service, task_stack};
pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
handlebars: handlebars::Handlebars<'static>,
config: service::Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
}
#[derive(Serialize)] #[derive(Serialize)]
struct BasePresenter<'a, T> { struct BasePresenter<'a, T> {
page_name: &'a str, page_name: &'a str,
@ -52,13 +44,23 @@ struct DomainSyncArgs {
url_encoded_domain_settings: util::UrlEncodedDomainSettings, url_encoded_domain_settings: util::UrlEncodedDomainSettings,
} }
pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
handlebars: handlebars::Handlebars<'static>,
config: service::Config,
proxied_domains: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
interface_domain: Option<domain::Name>,
}
impl Service { impl Service {
pub fn new<CertResolver>( pub fn new<CertResolver>(
task_stack: &mut task_stack::TaskStack<unexpected::Error>, task_stack: &mut task_stack::TaskStack<unexpected::Error>,
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: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>, 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,
@ -70,7 +72,8 @@ 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, 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));
@ -224,6 +227,7 @@ impl Service {
config.public.then(|| config.settings) config.public.then(|| config.settings)
} }
Ok(domain::manager::GetSettingsResult::Proxied(_)) => None, Ok(domain::manager::GetSettingsResult::Proxied(_)) => None,
Ok(domain::manager::GetSettingsResult::Interface) => None,
Err(domain::manager::GetSettingsError::NotFound) => None, Err(domain::manager::GetSettingsError::NotFound) => None,
Err(domain::manager::GetSettingsError::Unexpected(e)) => { Err(domain::manager::GetSettingsError::Unexpected(e)) => {
return self.internal_error( return self.internal_error(
@ -457,7 +461,7 @@ impl Service {
} }
} }
if let Some(config) = self.proxied.get(&domain) { if let Some(config) = self.proxied_domains.get(&domain) {
if let Some(ref http_url) = config.http_url { if let Some(ref http_url) = config.http_url {
return service::http::proxy::serve_http_request( return service::http::proxy::serve_http_request(
http_url.original_url.as_str(), http_url.original_url.as_str(),
@ -478,7 +482,7 @@ impl Service {
} }
} }
if Some(&domain) == self.config.interface_domain.as_ref() { if Some(&domain) == self.interface_domain.as_ref() {
return self.serve_interface(req).await; return self.serve_interface(req).await;
} }

View File

@ -16,7 +16,6 @@ pub async fn listen_http(
// only used for logging // only used for logging
let listen_host = service let listen_host = service
.config
.interface_domain .interface_domain
.clone() .clone()
.map_or(addr.ip().to_string(), |ref d| d.as_str().to_string()); .map_or(addr.ip().to_string(), |ref d| d.as_str().to_string());
@ -56,7 +55,6 @@ pub async fn listen_https(
// only used for logging // only used for logging
let listen_host = service let listen_host = service
.config
.interface_domain .interface_domain
.clone() .clone()
.map_or(addr.ip().to_string(), |ref d| d.as_str().to_string()); .map_or(addr.ip().to_string(), |ref d| d.as_str().to_string());