Compare commits

..

2 Commits

Author SHA1 Message Date
Brian Picciano
b44fd575a9 Rename primary_domain to interface_domain, and make it optional 2023-08-03 11:18:31 +02:00
Brian Picciano
e67cd6725a Return all domains from all_domains
This fixes an issue where the domain manager sync job wouldn't have
synced private builtin domains ever.
2023-08-03 10:29:57 +02:00
10 changed files with 220 additions and 166 deletions

View File

@ -89,8 +89,8 @@ service:
# Domani can serve the domains. All records given must route to this Domani # Domani can serve the domains. All records given must route to this Domani
# instance. # instance.
# #
# A CNAME record with the primary_domain of this server is automatically # A CNAME record with the interface_domain of this server is automatically
# included. # included, if it's not null itself.
dns_records: dns_records:
#- kind: A #- kind: A
# addr: 127.0.0.1 # addr: 127.0.0.1
@ -105,7 +105,9 @@ service:
# The domain name which will be used to serve the web interface of Domani. If # 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 # service.http.https_addr is enabled then an HTTPS certificate for this domain
# will be retrieved automatically. # will be retrieved automatically.
#primary_domain: "localhost" #
# This can be set to null to disable the web interface entirely.
#interface_domain: "localhost"
#http: #http:

View File

@ -13,7 +13,6 @@ domain:
public: true public: true
service: service:
passphrase: foobar passphrase: foobar
primary_domain: localhost
dns_records: dns_records:
- kind: A - kind: A
addr: 127.0.0.1 addr: 127.0.0.1

View File

@ -32,14 +32,12 @@ pub struct DNSChecker {
// TODO we should use some kind of connection pool here, I suppose // TODO we should use some kind of connection pool here, I suppose
client: tokio::sync::Mutex<AsyncClient>, client: tokio::sync::Mutex<AsyncClient>,
token_store: Box<dyn token::Store + Send + Sync>, token_store: Box<dyn token::Store + Send + Sync>,
service_primary_domain: domain::Name,
} }
impl DNSChecker { impl DNSChecker {
pub async fn new<TokenStore>( pub async fn new<TokenStore>(
token_store: TokenStore, token_store: TokenStore,
config: &domain::ConfigDNS, config: &domain::ConfigDNS,
service_primary_domain: domain::Name,
) -> Result<Self, unexpected::Error> ) -> Result<Self, unexpected::Error>
where where
TokenStore: token::Store + Send + Sync + 'static, TokenStore: token::Store + Send + Sync + 'static,
@ -52,7 +50,6 @@ impl DNSChecker {
Ok(Self { Ok(Self {
token_store: Box::from(token_store), token_store: Box::from(token_store),
client: tokio::sync::Mutex::new(client), client: tokio::sync::Mutex::new(client),
service_primary_domain,
}) })
} }
@ -111,7 +108,7 @@ impl DNSChecker {
let body = match reqwest::get(format!( let body = match reqwest::get(format!(
"http://{}/.well-known/domani-challenge", "http://{}/.well-known/domani-challenge",
self.service_primary_domain.as_str() domain.as_str(),
)) ))
.await .await
{ {

View File

@ -139,7 +139,9 @@ impl From<store::SetError> for SyncWithSettingsError {
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError; pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
//#[mockall::automock] pub type StoredDomain = domain::store::StoredDomain;
#[mockall::automock]
pub trait Manager: Sync + Send { pub trait Manager: Sync + Send {
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError>; fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError>;
@ -165,7 +167,7 @@ pub trait Manager: Sync + Send {
domain: &domain::Name, domain: &domain::Name,
) -> unexpected::Result<Option<String>>; ) -> unexpected::Result<Option<String>>;
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>; fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error>;
} }
pub struct ManagerImpl { pub struct ManagerImpl {
@ -247,13 +249,13 @@ impl ManagerImpl {
} }
.into_iter(); .into_iter();
for domain in domains { for StoredDomain { domain, .. } in domains {
log::info!("Syncing domain {}", &domain); log::info!("Syncing domain {}", &domain);
let get_res = match self.domain_store.get(&domain) { let get_res = match self.domain_store.get(&domain) {
Ok(get_res) => get_res, Ok(get_res) => get_res,
Err(err) => { Err(err) => {
log::error!("Error syncing {domain}: {err}"); log::error!("Failed to fetch settings for domain {domain}: {err}");
return; return;
} }
}; };
@ -334,7 +336,7 @@ impl Manager for ManagerImpl {
self.domain_checker.get_challenge_token(domain) self.domain_checker.get_challenge_token(domain)
} }
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> { fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
self.domain_store.all_domains() self.domain_store.all_domains()
} }
} }

View File

@ -1,15 +1,21 @@
use std::{collections, fs, io, path, str::FromStr}; use std::{collections, fs, io, path};
use crate::domain; use crate::domain;
use crate::error::unexpected::{self, Intoable, Mappable}; use crate::error::unexpected::{self, Intoable, Mappable};
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct GetResult { /// Extra information about a domain which is related to how its stored.
pub settings: domain::Settings, pub struct Metadata {
pub builtin: bool, pub builtin: bool,
pub public: bool, pub public: bool,
} }
#[derive(Debug, PartialEq)]
pub struct GetResult {
pub settings: domain::Settings,
pub metadata: Metadata,
}
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum GetError { pub enum GetError {
#[error("not found")] #[error("not found")]
@ -28,11 +34,16 @@ pub enum SetError {
Unexpected(#[from] unexpected::Error), Unexpected(#[from] unexpected::Error),
} }
pub struct StoredDomain {
pub domain: domain::Name,
pub metadata: Metadata,
}
#[mockall::automock] #[mockall::automock]
pub trait Store { pub trait Store {
fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError>; fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError>;
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>; fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>;
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>; fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error>;
} }
pub struct FSStore { pub struct FSStore {
@ -71,8 +82,10 @@ impl Store for FSStore {
Ok(GetResult { Ok(GetResult {
settings, settings,
metadata: Metadata {
public: true, public: true,
builtin: false, builtin: false,
},
}) })
} }
@ -91,18 +104,23 @@ impl Store for FSStore {
Ok(()) Ok(())
} }
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> { fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
fs::read_dir(&self.dir_path) fs::read_dir(&self.dir_path)
.or_unexpected()? .or_unexpected()?
.map( .map(
|dir_entry_res: io::Result<fs::DirEntry>| -> Result<domain::Name, unexpected::Error> { |dir_entry_res: io::Result<fs::DirEntry>| -> Result<StoredDomain, unexpected::Error> {
let domain = dir_entry_res.or_unexpected()?.file_name(); let domain = dir_entry_res.or_unexpected()?.file_name();
let domain = domain.to_str().ok_or(unexpected::Error::from( let domain = domain.to_str().ok_or(unexpected::Error::from(
"couldn't convert os string to &str", "couldn't convert os string to &str",
))?; ))?;
domain::Name::from_str(domain) Ok(StoredDomain{
.map_unexpected_while(|| format!("parsing {domain} as domain name")) domain: domain.parse().map_unexpected_while(|| format!("parsing {domain} as domain name"))?,
metadata: Metadata{
public: true,
builtin: false,
},
})
}, },
) )
.try_collect() .try_collect()
@ -131,8 +149,10 @@ impl<S: Store> Store for StoreWithBuiltin<S> {
if let Some(domain) = self.domains.get(domain) { if let Some(domain) = self.domains.get(domain) {
return Ok(GetResult { return Ok(GetResult {
settings: domain.settings.clone(), settings: domain.settings.clone(),
metadata: Metadata {
public: domain.public, public: domain.public,
builtin: true, builtin: true,
},
}); });
} }
self.inner.get(domain) self.inner.get(domain)
@ -145,13 +165,18 @@ impl<S: Store> Store for StoreWithBuiltin<S> {
self.inner.set(domain, settings) self.inner.set(domain, settings)
} }
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> { fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
let inner_domains = self.inner.all_domains()?; let inner_domains = self.inner.all_domains()?;
let mut domains: Vec<domain::Name> = self let mut domains: Vec<StoredDomain> = self
.domains .domains
.iter() .iter()
.filter(|(_, v)| v.public) .map(|(domain, v)| StoredDomain {
.map(|(k, _)| k.clone()) domain: domain.clone(),
metadata: Metadata {
public: v.public,
builtin: true,
},
})
.collect(); .collect();
domains.extend(inner_domains); domains.extend(inner_domains);
Ok(domains) Ok(domains)
@ -193,9 +218,11 @@ mod tests {
assert_eq!( assert_eq!(
GetResult { GetResult {
settings, settings,
metadata: Metadata {
public: true, public: true,
builtin: false, builtin: false,
}, },
},
store.get(&domain).expect("settings retrieved") store.get(&domain).expect("settings retrieved")
); );
@ -211,9 +238,11 @@ mod tests {
assert_eq!( assert_eq!(
GetResult { GetResult {
settings: new_settings, settings: new_settings,
metadata: Metadata {
public: true, public: true,
builtin: false, builtin: false,
}, },
},
store.get(&domain).expect("settings retrieved") store.get(&domain).expect("settings retrieved")
); );
} }

View File

@ -56,26 +56,25 @@ async fn main() {
}) })
}; };
// primary_cname is a CNAME record which points to the primary domain of the service. Since // inteface_cname is a CNAME record which points to the interface domain of the service.
// the primary domain _must_ point to the service (otherwise HTTPS 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.
let primary_cname = domani::service::ConfigDNSRecord::CNAME { if let Some(ref interface_domain) = config.service.interface_domain {
name: config.service.primary_domain.clone(), let interface_cname = domani::service::ConfigDNSRecord::CNAME {
name: interface_domain.clone(),
}; };
let dns_records_have_primary_cname = config let dns_records_have_interface_cname = config
.service .service
.dns_records .dns_records
.iter() .iter()
.any(|r| r == &primary_cname); .any(|r| r == &interface_cname);
if !dns_records_have_primary_cname { if !dns_records_have_interface_cname {
log::info!( log::info!("Adding 'CNAME {interface_domain}' to service.dns_records");
"Adding 'CNAME {}' to service.dns_records", config.service.dns_records.push(interface_cname);
&config.service.primary_domain }
);
config.service.dns_records.push(primary_cname);
} }
config config
@ -95,7 +94,6 @@ async fn main() {
let domain_checker = domani::domain::checker::DNSChecker::new( let domain_checker = domani::domain::checker::DNSChecker::new(
domani::token::MemStore::new(), domani::token::MemStore::new(),
&config.domain.dns, &config.domain.dns,
config.service.primary_domain.clone(),
) )
.await .await
.expect("domain checker initialization failed"); .expect("domain checker initialization failed");

View File

@ -2,8 +2,8 @@ use crate::{domain, service};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{net, str::FromStr}; use std::{net, str::FromStr};
fn default_primary_domain() -> domain::Name { fn default_interface_domain() -> Option<domain::Name> {
domain::Name::from_str("localhost").unwrap() Some(domain::Name::from_str("localhost").unwrap())
} }
#[derive(Serialize, Deserialize, Clone, PartialEq)] #[derive(Serialize, Deserialize, Clone, PartialEq)]
@ -28,10 +28,13 @@ impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
pub struct Config { pub struct Config {
pub passphrase: String, pub passphrase: String,
pub dns_records: Vec<ConfigDNSRecord>, pub dns_records: Vec<ConfigDNSRecord>,
#[serde(default = "default_primary_domain")]
pub primary_domain: domain::Name, #[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,
#[serde(default)] #[serde(default)]
pub gemini: service::gemini::Config, pub gemini: service::gemini::Config,
} }

View File

@ -196,11 +196,7 @@ async fn listen(
.with_cert_resolver(service.cert_resolver.clone()), .with_cert_resolver(service.cert_resolver.clone()),
); );
log::info!( log::info!("Listening on gemini://{}", addr);
"Listening on gemini://{}:{}",
&service.config.primary_domain.clone(),
addr,
);
let listener = tokio::net::TcpListener::bind(addr) let listener = tokio::net::TcpListener::bind(addr)
.await .await

View File

@ -10,7 +10,6 @@ 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::str::FromStr;
use std::{future, net, sync}; use std::{future, net, sync};
use crate::error::unexpected; use crate::error::unexpected;
@ -164,7 +163,7 @@ impl Service {
match self.domain_manager.get_file(&domain, &path) { match self.domain_manager.get_file(&domain, &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(domain::manager::GetFileError::DomainNotFound) => {
return self.render_error_page(404, "Domain not found") return self.render_error_page(404, "Unknown domain name")
} }
Err(domain::manager::GetFileError::FileNotFound) => { Err(domain::manager::GetFileError::FileNotFound) => {
self.render_error_page(404, "File not found") self.render_error_page(404, "File not found")
@ -210,6 +209,8 @@ impl Service {
} }
fn domain(&self, args: DomainArgs) -> Response<Body> { fn domain(&self, args: DomainArgs) -> Response<Body> {
use domain::store::Metadata;
#[derive(Serialize)] #[derive(Serialize)]
struct Data { struct Data {
domain: domain::Name, domain: domain::Name,
@ -217,14 +218,32 @@ impl Service {
} }
let settings = match self.domain_manager.get_settings(&args.domain) { let settings = match self.domain_manager.get_settings(&args.domain) {
Ok(domain::manager::GetSettingsResult {
metadata:
Metadata {
public: false,
builtin: _,
},
..
}) => None,
Ok(domain::manager::GetSettingsResult { Ok(domain::manager::GetSettingsResult {
settings, settings,
public, metadata:
builtin, Metadata {
}) => match (public, builtin) { public: true,
(false, _) => None, builtin: false,
(true, false) => Some(settings), },
(true, true) => { }) => Some(settings),
Ok(domain::manager::GetSettingsResult {
metadata:
Metadata {
public: true,
builtin: true,
},
..
}) => {
return self.render_error_page( return self.render_error_page(
403, 403,
format!( format!(
@ -234,7 +253,7 @@ impl Service {
.as_str(), .as_str(),
) )
} }
},
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(
@ -367,7 +386,8 @@ impl Service {
let mut domains: Vec<String> = domains let mut domains: Vec<String> = domains
.into_iter() .into_iter()
.map(|domain| domain.as_str().to_string()) .filter(|d| d.metadata.public)
.map(|d| d.domain.as_str().to_string())
.collect(); .collect();
domains.sort(); domains.sort();
@ -375,83 +395,7 @@ impl Service {
self.render_page("/domains.html", Response { domains }) self.render_page("/domains.html", Response { domains })
} }
async fn handle_request( async fn serve_interface(&self, req: Request<Body>) -> Response<Body> {
&self,
client_ip: net::IpAddr,
req: Request<Body>,
req_is_https: bool,
) -> Response<Body> {
let maybe_host = match (
req.headers()
.get("Host")
.and_then(|v| v.to_str().ok())
.map(strip_port),
req.uri().host().map(strip_port),
) {
(Some(h), _) if h != self.config.primary_domain.as_str() => Some(h),
(_, Some(h)) if h != self.config.primary_domain.as_str() => Some(h),
_ => None,
}
.and_then(|h| domain::Name::from_str(h).ok());
{
let path = req.uri().path();
// Serving acme challenges always takes priority. We serve them from the same store no
// matter the domain, presumably they are cryptographically random enough that it doesn't
// matter.
if req.method() == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
let token = path.trim_start_matches("/.well-known/acme-challenge/");
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
return self.serve(200, "token.txt", key.into());
}
}
// Serving domani challenges similarly takes priority.
if req.method() == Method::GET && path == "/.well-known/domani-challenge" {
if let Some(ref domain) = maybe_host {
match self
.domain_manager
.get_domain_checker_challenge_token(domain)
{
Ok(Some(token)) => return self.serve(200, "token.txt", token.into()),
Ok(None) => return self.render_error_page(404, "Token not found"),
Err(e) => {
return self.internal_error(
format!("failed to get token for domain {}: {e}", domain).as_str(),
)
}
}
}
}
}
// If a managed domain was given then serve that from its origin or a proxy.
if let Some(domain) = maybe_host {
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(),
)
});
}
return self.serve_origin(domain, req).await;
}
// Serve main domani site
let (req, body) = req.into_parts(); let (req, body) = req.into_parts();
let path = req.uri.path(); let path = req.uri.path();
@ -488,6 +432,86 @@ impl Service {
), ),
} }
} }
fn domain_from_req(req: &Request<Body>) -> Option<domain::Name> {
let host_header = req
.headers()
.get("Host")
.and_then(|v| v.to_str().ok())
.map(strip_port);
host_header
// if host_header isn't present, try the host from the URI
.or_else(|| req.uri().host().map(strip_port))
.and_then(|h| h.parse().ok())
}
async fn handle_request(
&self,
client_ip: net::IpAddr,
req: Request<Body>,
req_is_https: bool,
) -> Response<Body> {
let domain = match Self::domain_from_req(&req) {
Some(domain) => domain,
None => return self.render_error_page(400, "Cannot serve page without domain"),
};
let method = req.method();
let path = req.uri().path();
// Serving acme challenges always takes priority. We serve them from the same store no
// matter the domain, presumably they are cryptographically random enough that it doesn't
// matter.
if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
let token = path.trim_start_matches("/.well-known/acme-challenge/");
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
return self.serve(200, "token.txt", key.into());
}
}
// Serving domani challenges similarly takes priority.
if method == Method::GET && path == "/.well-known/domani-challenge" {
match self
.domain_manager
.get_domain_checker_challenge_token(&domain)
{
Ok(Some(token)) => return self.serve(200, "token.txt", token.into()),
Ok(None) => return self.render_error_page(404, "Token not found"),
Err(e) => {
return self.internal_error(
format!("failed to get token for domain {}: {e}", domain).as_str(),
)
}
}
}
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 Some(&domain) == self.config.interface_domain.as_ref() {
return self.serve_interface(req).await;
}
self.serve_origin(domain, req).await
}
} }
fn strip_port(host: &str) -> &str { fn strip_port(host: &str) -> &str {

View File

@ -13,7 +13,13 @@ pub async fn listen_http(
canceller: CancellationToken, canceller: CancellationToken,
) -> Result<(), unexpected::Error> { ) -> Result<(), unexpected::Error> {
let addr = service.config.http.http_addr.clone(); let addr = service.config.http.http_addr.clone();
let primary_domain = service.config.primary_domain.clone();
// only used for logging
let listen_host = service
.config
.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();
@ -31,11 +37,7 @@ pub async fn listen_http(
async move { Ok::<_, convert::Infallible>(hyper_service) } async move { Ok::<_, convert::Infallible>(hyper_service) }
}); });
log::info!( log::info!("Listening on http://{}:{}", listen_host, addr.port(),);
"Listening on http://{}:{}",
primary_domain.as_str(),
addr.port(),
);
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 {
@ -51,7 +53,13 @@ pub async fn listen_https(
) -> Result<(), unexpected::Error> { ) -> Result<(), unexpected::Error> {
let cert_resolver = service.cert_resolver.clone(); let cert_resolver = service.cert_resolver.clone();
let addr = service.config.http.https_addr.unwrap().clone(); let addr = service.config.http.https_addr.unwrap().clone();
let primary_domain = service.config.primary_domain.clone();
// only used for logging
let listen_host = service
.config
.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();
@ -91,11 +99,7 @@ pub async fn listen_https(
let incoming = hyper::server::accept::from_stream(incoming); let incoming = hyper::server::accept::from_stream(incoming);
log::info!( log::info!("Listening on https://{}:{}", listen_host, addr.port());
"Listening on https://{}:{}",
primary_domain.as_str(),
addr.port()
);
let server = hyper::Server::builder(incoming).serve(make_service); let server = hyper::Server::builder(incoming).serve(make_service);