Domani connects your domain to whatever you want to host on it, all with no account needed
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
domani/src/service/http.rs

632 lines
22 KiB

mod config;
mod proxy;
mod tasks;
mod tpl;
mod util;
pub use config::*;
use http::request::Parts;
use hyper::{Body, Method, Request, Response};
use serde::{Deserialize, Serialize};
use std::{future, net, sync};
use crate::error::unexpected::{self, Mappable};
use crate::{domain, service, task_stack};
#[derive(Serialize)]
struct BasePresenter<'a, T> {
page_name: &'a str,
form_method: &'a str,
http_scheme: &'a str,
data: T,
}
#[derive(Deserialize)]
struct DomainArgs {
domain: domain::Name,
}
#[derive(Deserialize)]
struct DomainInitArgs {
domain: domain::Name,
#[serde(flatten)]
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
}
#[derive(Deserialize)]
struct DomainSyncArgs {
domain: domain::Name,
passphrase: String,
#[serde(flatten)]
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,
}
impl Service {
pub fn new<CertResolver>(
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: CertResolver,
config: service::Config,
) -> sync::Arc<Service>
where
CertResolver: rustls::server::ResolvesServerCert + 'static,
{
let service = sync::Arc::new(Service {
domain_manager: domain_manager.clone(),
cert_resolver: sync::Arc::from(cert_resolver),
handlebars: tpl::get(),
config,
});
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
if service.https_enabled() {
task_stack.push_spawn(|canceller| tasks::listen_https(service.clone(), canceller));
}
service
}
fn https_enabled(&self) -> bool {
self.config.http.https_addr.is_some()
}
fn serve(&self, status_code: u16, path: &str, body: Body) -> Response<Body> {
match Response::builder()
.status(status_code)
.header("Content-Type", service::guess_mime(path))
.body(body)
{
Ok(res) => res,
Err(err) => {
// if the status code was already a 500, don't try to render _another_ 500, it'll
// probably fail for the same reason. At this point something is incredibly wrong,
// just panic.
if status_code == 500 {
panic!("failed to build {}: {}", path, err);
}
self.internal_error(format!("failed to build {}: {}", path, err).as_str())
}
}
}
fn render<T>(&self, status_code: u16, name: &str, value: T) -> Response<Body>
where
T: Serialize,
{
let rendered = match self.handlebars.render(name, &value) {
Ok(res) => res,
Err(handlebars::RenderError {
template_name: None,
..
}) => return self.render_error_page(404, "Static asset not found"),
Err(err) => {
return self.render_error_page(500, format!("template error: {err}").as_str())
}
};
self.serve(status_code, name, rendered.into())
}
fn presenter_http_scheme(&self) -> &str {
if self.https_enabled() {
return "https";
}
"http"
}
fn render_error_page(&self, status_code: u16, e: &str) -> Response<Body> {
#[derive(Serialize)]
struct Response<'a> {
error_msg: &'a str,
}
self.render(
status_code,
"/base.html",
BasePresenter {
page_name: "/error.html",
form_method: self.config.http.form_method.as_str(),
http_scheme: self.presenter_http_scheme(),
data: &Response { error_msg: e },
},
)
}
fn internal_error(&self, e: &str) -> Response<Body> {
log::error!("Internal error: {e}");
self.render_error_page(
500,
"An unexpected error occurred. The server administrator will be able to help.",
)
}
fn render_redirect(&self, status_code: u16, target_uri: &str) -> Response<Body> {
Response::builder()
.status(status_code)
.header("Location", target_uri.to_string())
.body(Body::empty())
.unwrap_or_else(|err| {
self.internal_error(
format!("failed to render {status_code} redirect to {target_uri}: {err}",)
.as_str(),
)
})
}
fn https_redirect(&self, domain: domain::Name, req: Request<Body>) -> Response<Body> {
let https_addr = self.config.http.https_addr.unwrap();
(|| {
let mut uri_parts = http::uri::Parts::default();
uri_parts.scheme = Some(http::uri::Scheme::HTTPS);
uri_parts.authority = Some(
http::uri::Authority::from_maybe_shared(format!(
"{}:{}",
&domain,
https_addr.port()
))
.or_unexpected_while("constructing authority")?,
);
uri_parts.path_and_query = req.uri().path_and_query().cloned();
let uri: http::uri::Uri = uri_parts
.try_into()
.or_unexpected_while("constructing new URI")?;
Ok(self.render_redirect(
http::status::StatusCode::PERMANENT_REDIRECT.into(),
uri.to_string().as_str(),
))
})()
.unwrap_or_else(|err: unexpected::Error| {
self.internal_error(
format!("failed to redirect from {} to https: {}", req.uri(), err).as_str(),
)
})
}
fn render_page<T>(&self, name: &str, data: T) -> Response<Body>
where
T: Serialize,
{
self.render(
200,
"/base.html",
BasePresenter {
page_name: name,
form_method: self.config.http.form_method.as_str(),
http_scheme: self.presenter_http_scheme(),
data,
},
)
}
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");
use domain::manager::GetFileError;
match self.domain_manager.get_file(&settings, &path).await {
Ok(f) => self.serve(200, &path, Body::wrap_stream(f)),
Err(GetFileError::FileNotFound) => self.render_error_page(404, "File not found"),
Err(GetFileError::Unavailable) => self.render_error_page(502, "Content unavailable"),
Err(GetFileError::DescrNotSynced) => self.internal_error(
format!(
"Backend for {:?} has not yet been synced",
settings.origin_descr
)
.as_str(),
),
Err(GetFileError::PathIsDirectory) => {
// redirect so that the path has '/' appended to it, which will cause the server to
// check index.html within the path on the new page load.
let mut path = path.into_owned();
path.push('/');
self.render_redirect(
http::status::StatusCode::TEMPORARY_REDIRECT.into(),
path.as_str(),
)
}
Err(GetFileError::Unexpected(e)) => {
self.internal_error(format!("failed to fetch file {path}: {e}").as_str())
}
}
}
async fn with_query_req<'a, F, In, Out>(
&self,
req: &'a Parts,
body: Body,
f: F,
) -> Response<Body>
where
In: for<'d> Deserialize<'d>,
F: FnOnce(In) -> Out,
Out: future::Future<Output = Response<Body>>,
{
let res = match self.config.http.form_method {
ConfigFormMethod::GET => {
serde_urlencoded::from_str::<In>(req.uri.query().unwrap_or(""))
}
ConfigFormMethod::POST => {
let body = match hyper::body::to_bytes(body).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
return self.internal_error(format!("failed to read body: {e}").as_str())
}
};
serde_urlencoded::from_bytes::<In>(body.as_ref())
}
};
match res {
Ok(args) => f(args).await,
Err(err) => {
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
}
}
}
fn domain(&self, args: DomainArgs) -> Response<Body> {
#[derive(Serialize)]
struct Data {
domain: domain::Name,
settings: Option<domain::Settings>,
}
let settings = match self.domain_manager.get_settings(&args.domain) {
Ok(domain::manager::GetSettingsResult::Stored(settings)) => Some(settings),
Ok(domain::manager::GetSettingsResult::Builtin(config)) => {
config.public.then_some(config.settings)
}
Ok(domain::manager::GetSettingsResult::Proxied(_)) => None,
Ok(domain::manager::GetSettingsResult::Interface) => None,
Ok(domain::manager::GetSettingsResult::External(_)) => None,
Err(domain::manager::GetSettingsError::NotFound) => None,
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
return self.internal_error(
format!("retrieving settings for domain {}: {}", &args.domain, e).as_str(),
);
}
};
self.render_page(
"/domain.html",
Data {
domain: args.domain,
settings,
},
)
}
fn domain_init(&self, args: DomainInitArgs) -> Response<Body> {
#[derive(Serialize)]
struct Data<'a> {
domain: domain::Name,
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
dns_records: &'a [service::ConfigDNSRecord],
challenge_token: String,
dns_records_have_more_than_one: bool,
dns_records_have_cname: bool,
}
let settings: domain::Settings = match args.url_encoded_domain_settings.try_into() {
Ok(settings) => settings,
Err(e) => {
return self
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
}
};
let settings_hash = match settings.hash() {
Ok(hash) => hash,
Err(e) => {
return self.internal_error(
format!("failed to hash domain settings {settings:?}: {e}").as_str(),
)
}
};
let dns_records_have_cname = self
.config
.dns_records
.iter()
.any(|r| matches!(r, service::ConfigDNSRecord::CNAME { .. }));
let url_encoded_domain_settings = match settings.try_into() {
Ok(s) => s,
Err(e) => {
return self
.internal_error(format!("failed to flatten domains settings: {e}").as_str())
}
};
self.render_page(
"/domain_init.html",
Data {
domain: args.domain,
url_encoded_domain_settings,
dns_records: &self.config.dns_records,
challenge_token: settings_hash,
dns_records_have_more_than_one: self.config.dns_records.len() > 1,
dns_records_have_cname,
},
)
}
async fn domain_sync(&self, args: DomainSyncArgs) -> Response<Body> {
if args.passphrase != self.config.passphrase.as_str() {
return self.render_error_page(401, "Incorrect passphrase");
}
let settings: domain::Settings = match args.url_encoded_domain_settings.clone().try_into() {
Ok(settings) => settings,
Err(e) => {
return self
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
}
};
let sync_result = self
.domain_manager
.sync_with_settings(args.domain.clone(), settings)
.await;
#[derive(Serialize)]
struct Data {
domain: domain::Name,
url_encoded_domain_settings: util::UrlEncodedDomainSettings,
passphrase: String,
error_msg: Option<String>,
retryable: bool,
}
let (error_msg, retryable) = match sync_result {
Ok(_) => (None, false),
Err(domain::manager::SyncWithSettingsError::NotModifiable) => {
(Some("This domain is not allowed to be configured.".to_string()), false)
}
Err(domain::manager::SyncWithSettingsError::InvalidURL) => (Some(
"Fetching the git repository failed; please double check that you input the correct
URL."
.to_string(),
), false),
Err(domain::manager::SyncWithSettingsError::Unavailable) => (Some(
"Fetching the git repository failed; the server is not available or is not corectly serving the repository."
.to_string(),
), false),
Err(domain::manager::SyncWithSettingsError::InvalidBranchName) => (Some(
"The git repository does not have a branch of the given name; please double check
that you input the correct name."
.to_string(),
), false),
Err(domain::manager::SyncWithSettingsError::AlreadyInProgress) => {
(Some("The configuration of your domain is still in progress.".to_string()), true)
}
Err(domain::manager::SyncWithSettingsError::ServiceDNSRecordsNotSet) => (Some(
"No routing record (A, AAAA, CNAME, etc...) was present on the nameserver."
.to_string(),
), true),
Err(domain::manager::SyncWithSettingsError::ChallengeTokenNotSet) => (Some(
"The challenge record (TXT) was not present on the nameserver."
.to_string(),
), true),
Err(domain::manager::SyncWithSettingsError::Unexpected(e)) => {
(Some(format!("An unexpected error occurred: {e}")), false)
}
};
self.render_page(
"/domain_sync.html",
Data {
domain: args.domain,
url_encoded_domain_settings: args.url_encoded_domain_settings,
passphrase: args.passphrase,
error_msg,
retryable,
},
)
}
fn domains(&self) -> Response<Body> {
#[derive(Serialize)]
struct Response {
domains: Vec<String>,
}
let domains = match self.domain_manager.all_domains() {
Ok(domains) => domains,
Err(e) => return self.internal_error(format!("failed get all domains: {e}").as_str()),
};
let mut domains: Vec<String> = domains
.into_iter()
.filter(|d| d.public)
.map(|d| d.domain.as_str().to_string())
.collect();
domains.sort();
self.render_page("/domains.html", Response { domains })
}
async fn serve_interface(&self, req: Request<Body>) -> Response<Body> {
let (req, body) = req.into_parts();
let path = req.uri.path();
if req.method == Method::GET && path.starts_with("/static/") {
return self.render(200, path, ());
}
let config_form_method = self.config.http.form_method.as_ref();
match (&req.method, path) {
(&Method::GET, "/") | (&Method::GET, "/index.html") => {
self.render_page("/index.html", ())
}
(form_method, "/domain.html") if form_method == config_form_method => {
self.with_query_req(&req, body, |args: DomainArgs| async { self.domain(args) })
.await
}
(form_method, "/domain_init.html") if form_method == config_form_method => {
self.with_query_req(&req, body, |args: DomainInitArgs| async {
self.domain_init(args)
})
.await
}
(form_method, "/domain_sync.html") if form_method == config_form_method => {
self.with_query_req(&req, body, |args: DomainSyncArgs| async {
self.domain_sync(args).await
})
.await
}
(&Method::GET, "/domains.html") => self.domains(),
_ => self.render_error_page(
404,
"This is not the page you're looking for. This page doesn't even exist!",
),
}
}
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(),
)
}
}
}
// We only allow HTTP requests when HTTPS is enabled in specific cases:
// - /.well-known urls
// - proxied domains with https_disabled set on them
// everything else must use https if possible.
let https_upgradable = self.https_enabled() && !req_is_https;
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(),
)
}
}
};
self.serve_origin(settings, req).await
}
}
fn strip_port(host: &str) -> &str {
match host.rfind(':') {
None => host,
Some(i) => &host[..i],
}
}