diff --git a/README.md b/README.md index a58add3..018a64d 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ service: # Domani can serve the domains. All records given must route to this Domani # instance. # - # A CNAME record with the primary_domain of this server is automatically - # included. + # A CNAME record with the interface_domain of this server is automatically + # included, if it's not null itself. dns_records: #- kind: A # 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 # service.http.https_addr is enabled then an HTTPS certificate for this domain # will be retrieved automatically. - #primary_domain: "localhost" + # + # This can be set to null to disable the web interface entirely. + #interface_domain: "localhost" #http: diff --git a/config.yml.tpl b/config.yml.tpl index e518a7d..ba58274 100644 --- a/config.yml.tpl +++ b/config.yml.tpl @@ -13,7 +13,6 @@ domain: public: true service: passphrase: foobar - primary_domain: localhost dns_records: - kind: A addr: 127.0.0.1 diff --git a/src/domain/checker.rs b/src/domain/checker.rs index 002dab4..9e00ff9 100644 --- a/src/domain/checker.rs +++ b/src/domain/checker.rs @@ -32,14 +32,12 @@ pub struct DNSChecker { // TODO we should use some kind of connection pool here, I suppose client: tokio::sync::Mutex, token_store: Box, - service_primary_domain: domain::Name, } impl DNSChecker { pub async fn new( token_store: TokenStore, config: &domain::ConfigDNS, - service_primary_domain: domain::Name, ) -> Result where TokenStore: token::Store + Send + Sync + 'static, @@ -52,7 +50,6 @@ impl DNSChecker { Ok(Self { token_store: Box::from(token_store), client: tokio::sync::Mutex::new(client), - service_primary_domain, }) } @@ -111,7 +108,7 @@ impl DNSChecker { let body = match reqwest::get(format!( "http://{}/.well-known/domani-challenge", - self.service_primary_domain.as_str() + domain.as_str(), )) .await { diff --git a/src/main.rs b/src/main.rs index ab34d16..bd15032 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,26 +56,25 @@ async fn main() { }) }; - // primary_cname is a CNAME record which points to the primary domain of the service. Since - // the primary domain _must_ point to the service (otherwise HTTPS wouldn't work) it's + // inteface_cname is a CNAME record which points to the interface domain of the service. + // 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 // the service. - let primary_cname = domani::service::ConfigDNSRecord::CNAME { - name: config.service.primary_domain.clone(), - }; + if let Some(ref interface_domain) = config.service.interface_domain { + let interface_cname = domani::service::ConfigDNSRecord::CNAME { + name: interface_domain.clone(), + }; - let dns_records_have_primary_cname = config - .service - .dns_records - .iter() - .any(|r| r == &primary_cname); + let dns_records_have_interface_cname = config + .service + .dns_records + .iter() + .any(|r| r == &interface_cname); - if !dns_records_have_primary_cname { - log::info!( - "Adding 'CNAME {}' to service.dns_records", - &config.service.primary_domain - ); - config.service.dns_records.push(primary_cname); + if !dns_records_have_interface_cname { + log::info!("Adding 'CNAME {interface_domain}' to service.dns_records"); + config.service.dns_records.push(interface_cname); + } } config @@ -95,7 +94,6 @@ async fn main() { let domain_checker = domani::domain::checker::DNSChecker::new( domani::token::MemStore::new(), &config.domain.dns, - config.service.primary_domain.clone(), ) .await .expect("domain checker initialization failed"); diff --git a/src/service/config.rs b/src/service/config.rs index 1de8a2f..6552a6c 100644 --- a/src/service/config.rs +++ b/src/service/config.rs @@ -2,8 +2,8 @@ use crate::{domain, service}; use serde::{Deserialize, Serialize}; use std::{net, str::FromStr}; -fn default_primary_domain() -> domain::Name { - domain::Name::from_str("localhost").unwrap() +fn default_interface_domain() -> Option { + Some(domain::Name::from_str("localhost").unwrap()) } #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -28,10 +28,13 @@ impl From for domain::checker::DNSRecord { pub struct Config { pub passphrase: String, pub dns_records: Vec, - #[serde(default = "default_primary_domain")] - pub primary_domain: domain::Name, + + #[serde(default = "default_interface_domain")] + pub interface_domain: Option, + #[serde(default)] pub http: service::http::Config, + #[serde(default)] pub gemini: service::gemini::Config, } diff --git a/src/service/gemini.rs b/src/service/gemini.rs index 904bc0f..7a2b605 100644 --- a/src/service/gemini.rs +++ b/src/service/gemini.rs @@ -196,11 +196,7 @@ async fn listen( .with_cert_resolver(service.cert_resolver.clone()), ); - log::info!( - "Listening on gemini://{}:{}", - &service.config.primary_domain.clone(), - addr, - ); + log::info!("Listening on gemini://{}", addr); let listener = tokio::net::TcpListener::bind(addr) .await diff --git a/src/service/http.rs b/src/service/http.rs index b61f774..725166a 100644 --- a/src/service/http.rs +++ b/src/service/http.rs @@ -10,7 +10,6 @@ use http::request::Parts; use hyper::{Body, Method, Request, Response}; use serde::{Deserialize, Serialize}; -use std::str::FromStr; use std::{future, net, sync}; use crate::error::unexpected; @@ -164,7 +163,7 @@ impl Service { match self.domain_manager.get_file(&domain, &path) { Ok(f) => self.serve(200, &path, Body::wrap_stream(f)), 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) => { self.render_error_page(404, "File not found") @@ -396,83 +395,7 @@ impl Service { self.render_page("/domains.html", Response { domains }) } - async fn handle_request( - &self, - client_ip: net::IpAddr, - req: Request, - req_is_https: bool, - ) -> Response { - 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 + async fn serve_interface(&self, req: Request) -> Response { let (req, body) = req.into_parts(); let path = req.uri.path(); @@ -509,6 +432,86 @@ impl Service { ), } } + + fn domain_from_req(req: &Request) -> Option { + 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, + req_is_https: bool, + ) -> Response { + 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 { diff --git a/src/service/http/tasks.rs b/src/service/http/tasks.rs index 2fb7328..e266bcf 100644 --- a/src/service/http/tasks.rs +++ b/src/service/http/tasks.rs @@ -13,7 +13,13 @@ pub async fn listen_http( canceller: CancellationToken, ) -> Result<(), unexpected::Error> { 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 service = service.clone(); @@ -31,11 +37,7 @@ pub async fn listen_http( async move { Ok::<_, convert::Infallible>(hyper_service) } }); - log::info!( - "Listening on http://{}:{}", - primary_domain.as_str(), - addr.port(), - ); + log::info!("Listening on http://{}:{}", listen_host, addr.port(),); let server = hyper::Server::bind(&addr).serve(make_service); let graceful = server.with_graceful_shutdown(async { @@ -51,7 +53,13 @@ pub async fn listen_https( ) -> Result<(), unexpected::Error> { let cert_resolver = service.cert_resolver.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| { let service = service.clone(); @@ -91,11 +99,7 @@ pub async fn listen_https( let incoming = hyper::server::accept::from_stream(incoming); - log::info!( - "Listening on https://{}:{}", - primary_domain.as_str(), - addr.port() - ); + log::info!("Listening on https://{}:{}", listen_host, addr.port()); let server = hyper::Server::builder(incoming).serve(make_service);