Compare commits
5 Commits
6941ceec8e
...
506037dcd0
Author | SHA1 | Date | |
---|---|---|---|
|
506037dcd0 | ||
|
7ea97b2617 | ||
|
1f9ae0038f | ||
|
dbc912a9d3 | ||
|
6da68dc042 |
@ -125,6 +125,11 @@ pub trait Manager: Sync + Send {
|
|||||||
domain: &domain::Name,
|
domain: &domain::Name,
|
||||||
) -> Result<sync::Arc<dyn origin::Origin>, GetOriginError>;
|
) -> Result<sync::Arc<dyn origin::Origin>, GetOriginError>;
|
||||||
|
|
||||||
|
fn sync_cert<'mgr>(
|
||||||
|
&'mgr self,
|
||||||
|
domain: domain::Name,
|
||||||
|
) -> pin::Pin<Box<dyn future::Future<Output = Result<(), unexpected::Error>> + Send + 'mgr>>;
|
||||||
|
|
||||||
fn sync_with_config<'mgr>(
|
fn sync_with_config<'mgr>(
|
||||||
&'mgr self,
|
&'mgr self,
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
@ -139,7 +144,7 @@ pub trait Manager: Sync + Send {
|
|||||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ManagerImpl {
|
pub struct ManagerImpl {
|
||||||
origin_store: sync::Arc<dyn origin::store::Store>,
|
origin_store: sync::Arc<dyn origin::store::Store>,
|
||||||
domain_config_store: sync::Arc<dyn config::Store>,
|
domain_config_store: sync::Arc<dyn config::Store>,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
@ -170,7 +175,7 @@ pub fn new(
|
|||||||
domain_config_store: sync::Arc<dyn config::Store>,
|
domain_config_store: sync::Arc<dyn config::Store>,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<sync::Arc<dyn acme::manager::Manager>>,
|
acme_manager: Option<sync::Arc<dyn acme::manager::Manager>>,
|
||||||
) -> sync::Arc<dyn Manager> {
|
) -> ManagerImpl {
|
||||||
let canceller = CancellationToken::new();
|
let canceller = CancellationToken::new();
|
||||||
|
|
||||||
let origin_sync_handler = {
|
let origin_sync_handler = {
|
||||||
@ -188,22 +193,20 @@ pub fn new(
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
sync::Arc::new(ManagerImpl {
|
ManagerImpl {
|
||||||
origin_store,
|
origin_store,
|
||||||
domain_config_store,
|
domain_config_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
acme_manager,
|
acme_manager,
|
||||||
canceller,
|
canceller,
|
||||||
origin_sync_handler,
|
origin_sync_handler,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagerImpl {
|
impl ManagerImpl {
|
||||||
pub async fn stop(self) {
|
pub fn stop(self) -> tokio::task::JoinHandle<()> {
|
||||||
self.canceller.cancel();
|
self.canceller.cancel();
|
||||||
self.origin_sync_handler
|
self.origin_sync_handler
|
||||||
.await
|
|
||||||
.expect("origin_sync_handler errored");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +228,20 @@ impl Manager for ManagerImpl {
|
|||||||
Ok(origin)
|
Ok(origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_cert<'mgr>(
|
||||||
|
&'mgr self,
|
||||||
|
domain: domain::Name,
|
||||||
|
) -> pin::Pin<Box<dyn future::Future<Output = Result<(), unexpected::Error>> + Send + 'mgr>>
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(ref acme_manager) = self.acme_manager {
|
||||||
|
acme_manager.sync_domain(domain.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_with_config<'mgr>(
|
fn sync_with_config<'mgr>(
|
||||||
&'mgr self,
|
&'mgr self,
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
@ -245,9 +262,7 @@ impl Manager for ManagerImpl {
|
|||||||
|
|
||||||
self.domain_config_store.set(&domain, &config)?;
|
self.domain_config_store.set(&domain, &config)?;
|
||||||
|
|
||||||
if let Some(ref acme_manager) = self.acme_manager {
|
self.sync_cert(domain).await?;
|
||||||
acme_manager.sync_domain(domain.clone()).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#![feature(result_option_inspect)]
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
|
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
|
176
src/main.rs
176
src/main.rs
@ -1,16 +1,10 @@
|
|||||||
#![feature(result_option_inspect)]
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::stream::futures_unordered::FuturesUnordered;
|
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use signal_hook_tokio::Signals;
|
use signal_hook_tokio::Signals;
|
||||||
use tokio::select;
|
|
||||||
use tokio::time;
|
|
||||||
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{future, path, sync};
|
use std::{path, sync};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
@ -84,7 +78,6 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let mut wait_group = FuturesUnordered::new();
|
|
||||||
let canceller = tokio_util::sync::CancellationToken::new();
|
let canceller = tokio_util::sync::CancellationToken::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -152,170 +145,31 @@ async fn main() {
|
|||||||
https_params.as_ref().map(|p| p.domain_acme_manager.clone()),
|
https_params.as_ref().map(|p| p.domain_acme_manager.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let service = domiply::service::new(
|
let domain_manager = sync::Arc::new(domain_manager);
|
||||||
|
|
||||||
|
{
|
||||||
|
let http_service = domiply::service::http::new(
|
||||||
domain_manager.clone(),
|
domain_manager.clone(),
|
||||||
config.domain_checker_target_a,
|
config.domain_checker_target_a,
|
||||||
config.passphrase,
|
config.passphrase,
|
||||||
|
config.http_listen_addr.clone(),
|
||||||
config.http_domain.clone(),
|
config.http_domain.clone(),
|
||||||
|
https_params.map(|p| domiply::service::http::HTTPSParams {
|
||||||
|
listen_addr: p.https_listen_addr,
|
||||||
|
cert_resolver: domiply::domain::acme::resolver::new(p.domain_acme_store),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let service = sync::Arc::new(service);
|
|
||||||
|
|
||||||
wait_group.push({
|
|
||||||
let http_domain = config.http_domain.clone();
|
|
||||||
let canceller = canceller.clone();
|
|
||||||
let service = service.clone();
|
|
||||||
|
|
||||||
let make_service = hyper::service::make_service_fn(move |_| {
|
|
||||||
let service = service.clone();
|
|
||||||
|
|
||||||
// Create a `Service` for responding to the request.
|
|
||||||
let service = hyper::service::service_fn(move |req| {
|
|
||||||
domiply::service::handle_request(service.clone(), req)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the service to hyper.
|
|
||||||
async move { Ok::<_, Infallible>(service) }
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let addr = config.http_listen_addr;
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Listening on http://{}:{}",
|
|
||||||
http_domain.as_str(),
|
|
||||||
addr.port()
|
|
||||||
);
|
|
||||||
let server = hyper::Server::bind(&addr).serve(make_service);
|
|
||||||
|
|
||||||
let graceful = server.with_graceful_shutdown(async {
|
|
||||||
canceller.cancelled().await;
|
canceller.cancelled().await;
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = graceful.await {
|
sync::Arc::into_inner(http_service).unwrap().stop().await;
|
||||||
panic!("server error: {}", e);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(https_params) = https_params {
|
|
||||||
// Periodically refresh all domain certs, including the http_domain passed in the Cli opts
|
|
||||||
wait_group.push({
|
|
||||||
let https_params = https_params.clone();
|
|
||||||
let domain_manager = domain_manager.clone();
|
|
||||||
let http_domain = config.http_domain.clone();
|
|
||||||
let canceller = canceller.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut interval = time::interval(time::Duration::from_secs(60 * 60));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
_ = interval.tick() => (),
|
|
||||||
_ = canceller.cancelled() => return,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = https_params
|
sync::Arc::into_inner(domain_manager)
|
||||||
.domain_acme_manager
|
.unwrap()
|
||||||
.sync_domain(http_domain.clone())
|
.stop()
|
||||||
.await
|
.await
|
||||||
.inspect_err(|err| {
|
.expect("domain manager failed to shutdown cleanly");
|
||||||
log::error!(
|
|
||||||
"Error while getting cert for {}: {err}",
|
|
||||||
http_domain.as_str()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let domains_iter = domain_manager.all_domains();
|
|
||||||
|
|
||||||
if let Err(err) = domains_iter {
|
|
||||||
log::error!("Got error calling all_domains: {err}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for domain in domains_iter.unwrap().into_iter() {
|
|
||||||
let _ = https_params
|
|
||||||
.domain_acme_manager
|
|
||||||
.sync_domain(domain.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
log::error!(
|
|
||||||
"Error while getting cert for {}: {err}",
|
|
||||||
domain.as_str(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// HTTPS server
|
|
||||||
wait_group.push({
|
|
||||||
let http_domain = config.http_domain.clone();
|
|
||||||
let canceller = canceller.clone();
|
|
||||||
let service = service.clone();
|
|
||||||
|
|
||||||
let make_service = hyper::service::make_service_fn(move |_| {
|
|
||||||
let service = service.clone();
|
|
||||||
|
|
||||||
// Create a `Service` for responding to the request.
|
|
||||||
let service = hyper::service::service_fn(move |req| {
|
|
||||||
domiply::service::handle_request(service.clone(), req)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the service to hyper.
|
|
||||||
async move { Ok::<_, Infallible>(service) }
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let cert_resolver =
|
|
||||||
domiply::domain::acme::resolver::new(https_params.domain_acme_store);
|
|
||||||
let canceller = canceller.clone();
|
|
||||||
|
|
||||||
let server_config: tokio_rustls::TlsAcceptor = sync::Arc::new(
|
|
||||||
rustls::server::ServerConfig::builder()
|
|
||||||
.with_safe_defaults()
|
|
||||||
.with_no_client_auth()
|
|
||||||
.with_cert_resolver(cert_resolver),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let addr = https_params.https_listen_addr;
|
|
||||||
let addr_incoming = hyper::server::conn::AddrIncoming::bind(&addr)
|
|
||||||
.expect("https listen socket creation failed");
|
|
||||||
|
|
||||||
let incoming =
|
|
||||||
tls_listener::TlsListener::new(server_config, addr_incoming).filter(|conn| {
|
|
||||||
if let Err(err) = conn {
|
|
||||||
log::error!("Error accepting TLS connection: {:?}", err);
|
|
||||||
future::ready(false)
|
|
||||||
} else {
|
|
||||||
future::ready(true)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let incoming = hyper::server::accept::from_stream(incoming);
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Listening on https://{}:{}",
|
|
||||||
http_domain.as_str(),
|
|
||||||
addr.port()
|
|
||||||
);
|
|
||||||
|
|
||||||
let server = hyper::Server::builder(incoming).serve(make_service);
|
|
||||||
|
|
||||||
let graceful = server.with_graceful_shutdown(async {
|
|
||||||
canceller.cancelled().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = graceful.await {
|
|
||||||
panic!("server error: {}", e);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
while wait_group.next().await.is_some() {}
|
|
||||||
|
|
||||||
log::info!("Graceful shutdown complete");
|
log::info!("Graceful shutdown complete");
|
||||||
}
|
}
|
||||||
|
395
src/service.rs
395
src/service.rs
@ -1,395 +1,2 @@
|
|||||||
use hyper::{Body, Method, Request, Response};
|
pub mod http;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::net;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync;
|
|
||||||
|
|
||||||
use crate::{domain, origin};
|
|
||||||
|
|
||||||
pub mod http_tpl;
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
type SvcResponse = Result<Response<hyper::body::Body>, String>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Service<'svc> {
|
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
|
||||||
target_a: net::Ipv4Addr,
|
|
||||||
passphrase: String,
|
|
||||||
http_domain: domain::Name,
|
|
||||||
handlebars: handlebars::Handlebars<'svc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<'svc>(
|
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
|
||||||
target_a: net::Ipv4Addr,
|
|
||||||
passphrase: String,
|
|
||||||
http_domain: domain::Name,
|
|
||||||
) -> Service<'svc> {
|
|
||||||
Service {
|
|
||||||
domain_manager,
|
|
||||||
target_a,
|
|
||||||
passphrase,
|
|
||||||
http_domain,
|
|
||||||
handlebars: self::http_tpl::get(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct BasePresenter<'a, T> {
|
|
||||||
page_name: &'a str,
|
|
||||||
data: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct DomainGetArgs {
|
|
||||||
domain: domain::Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct DomainInitArgs {
|
|
||||||
domain: domain::Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct DomainSyncArgs {
|
|
||||||
domain: domain::Name,
|
|
||||||
passphrase: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'svc> Service<'svc> {
|
|
||||||
fn serve_string(&self, status_code: u16, path: &'_ str, body: Vec<u8>) -> SvcResponse {
|
|
||||||
let content_type = mime_guess::from_path(path)
|
|
||||||
.first_or_octet_stream()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
match Response::builder()
|
|
||||||
.status(status_code)
|
|
||||||
.header("Content-Type", content_type)
|
|
||||||
.body(body.into())
|
|
||||||
{
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(format!("failed to build {}: {}", path, err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//// TODO make this use an io::Write, rather than SvcResponse
|
|
||||||
fn render<T>(&self, status_code: u16, name: &'_ str, value: T) -> SvcResponse
|
|
||||||
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_string(status_code, name, rendered.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_error_page(&'svc self, status_code: u16, e: &'_ str) -> SvcResponse {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Response<'a> {
|
|
||||||
error_msg: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.render(
|
|
||||||
status_code,
|
|
||||||
"/base.html",
|
|
||||||
BasePresenter {
|
|
||||||
page_name: "/error.html",
|
|
||||||
data: &Response { error_msg: e },
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_page<T>(&self, name: &'_ str, data: T) -> SvcResponse
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
self.render(
|
|
||||||
200,
|
|
||||||
"/base.html",
|
|
||||||
BasePresenter {
|
|
||||||
page_name: name,
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve_origin(&self, domain: domain::Name, path: &'_ str) -> SvcResponse {
|
|
||||||
let mut path_owned;
|
|
||||||
|
|
||||||
let path = match path.ends_with('/') {
|
|
||||||
true => {
|
|
||||||
path_owned = String::from(path);
|
|
||||||
path_owned.push_str("index.html");
|
|
||||||
path_owned.as_str()
|
|
||||||
}
|
|
||||||
false => path,
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin = match self.domain_manager.get_origin(&domain) {
|
|
||||||
Ok(o) => o,
|
|
||||||
Err(domain::manager::GetOriginError::NotFound) => {
|
|
||||||
return self.render_error_page(404, "Domain not found")
|
|
||||||
}
|
|
||||||
Err(domain::manager::GetOriginError::Unexpected(e)) => {
|
|
||||||
return self.render_error_page(500, format!("failed to fetch origin: {e}").as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = Vec::<u8>::new();
|
|
||||||
match origin.read_file_into(path, &mut buf) {
|
|
||||||
Ok(_) => self.serve_string(200, path, buf),
|
|
||||||
Err(origin::ReadFileIntoError::FileNotFound) => {
|
|
||||||
self.render_error_page(404, "File not found")
|
|
||||||
}
|
|
||||||
Err(origin::ReadFileIntoError::Unexpected(e)) => {
|
|
||||||
self.render_error_page(500, format!("failed to fetch file {path}: {e}").as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn with_query_req<'a, F, In, Out>(&self, req: &'a Request<Body>, f: F) -> SvcResponse
|
|
||||||
where
|
|
||||||
In: Deserialize<'a>,
|
|
||||||
F: FnOnce(In) -> Out,
|
|
||||||
Out: Future<Output = SvcResponse>,
|
|
||||||
{
|
|
||||||
let query = req.uri().query().unwrap_or("");
|
|
||||||
match serde_urlencoded::from_str::<In>(query) {
|
|
||||||
Ok(args) => f(args).await,
|
|
||||||
Err(err) => Err(format!("failed to parse query args: {}", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn domain_get(&self, args: DomainGetArgs) -> SvcResponse {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Response {
|
|
||||||
domain: domain::Name,
|
|
||||||
config: Option<domain::config::Config>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = match self.domain_manager.get_config(&args.domain) {
|
|
||||||
Ok(config) => Some(config),
|
|
||||||
Err(domain::manager::GetConfigError::NotFound) => None,
|
|
||||||
Err(domain::manager::GetConfigError::Unexpected(e)) => {
|
|
||||||
return self
|
|
||||||
.render_error_page(500, format!("retrieving configuration: {}", e).as_str());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.render_page(
|
|
||||||
"/domain.html",
|
|
||||||
Response {
|
|
||||||
domain: args.domain,
|
|
||||||
config,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn domain_init(&self, args: DomainInitArgs, domain_config: util::FlatConfig) -> SvcResponse {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Response {
|
|
||||||
domain: domain::Name,
|
|
||||||
flat_config: util::FlatConfig,
|
|
||||||
target_a: net::Ipv4Addr,
|
|
||||||
challenge_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let config: domain::config::Config = match domain_config.try_into() {
|
|
||||||
Ok(Some(config)) => config,
|
|
||||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
|
||||||
Err(e) => {
|
|
||||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let config_hash = match config.hash() {
|
|
||||||
Ok(hash) => hash,
|
|
||||||
Err(e) => {
|
|
||||||
return self
|
|
||||||
.render_error_page(500, format!("failed to hash domain config: {e}").as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.render_page(
|
|
||||||
"/domain_init.html",
|
|
||||||
Response {
|
|
||||||
domain: args.domain,
|
|
||||||
flat_config: config.into(),
|
|
||||||
target_a: self.target_a,
|
|
||||||
challenge_token: config_hash,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn domain_sync(
|
|
||||||
&self,
|
|
||||||
args: DomainSyncArgs,
|
|
||||||
domain_config: util::FlatConfig,
|
|
||||||
) -> SvcResponse {
|
|
||||||
if args.passphrase != self.passphrase.as_str() {
|
|
||||||
return self.render_error_page(401, "Incorrect passphrase");
|
|
||||||
}
|
|
||||||
|
|
||||||
let config: domain::config::Config = match domain_config.try_into() {
|
|
||||||
Ok(Some(config)) => config,
|
|
||||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
|
||||||
Err(e) => {
|
|
||||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sync_result = self
|
|
||||||
.domain_manager
|
|
||||||
.sync_with_config(args.domain.clone(), config)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Response {
|
|
||||||
domain: domain::Name,
|
|
||||||
error_msg: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let error_msg = match sync_result {
|
|
||||||
Ok(_) => None,
|
|
||||||
Err(domain::manager::SyncWithConfigError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
|
||||||
Err(domain::manager::SyncWithConfigError::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()),
|
|
||||||
Err(domain::manager::SyncWithConfigError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()),
|
|
||||||
Err(domain::manager::SyncWithConfigError::TargetANotSet) => Some("The A record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
|
||||||
Err(domain::manager::SyncWithConfigError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
|
||||||
Err(domain::manager::SyncWithConfigError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = Response {
|
|
||||||
domain: args.domain,
|
|
||||||
error_msg,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.render_page("/domain_sync.html", response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn domains(&self) -> SvcResponse {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Response {
|
|
||||||
domains: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let domains = match self.domain_manager.all_domains() {
|
|
||||||
Ok(domains) => domains,
|
|
||||||
Err(e) => {
|
|
||||||
return self.render_error_page(500, format!("failed get all domains: {e}").as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut domains: Vec<String> = domains
|
|
||||||
.into_iter()
|
|
||||||
.map(|domain| domain.as_str().to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
domains.sort();
|
|
||||||
|
|
||||||
self.render_page("/domains.html", Response { domains })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_request(
|
|
||||||
svc: sync::Arc<Service<'_>>,
|
|
||||||
req: Request<Body>,
|
|
||||||
) -> Result<Response<Body>, Infallible> {
|
|
||||||
match handle_request_inner(svc, req).await {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => panic!("unexpected error {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_port(host: &str) -> &str {
|
|
||||||
match host.rfind(':') {
|
|
||||||
None => host,
|
|
||||||
Some(i) => &host[..i],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_request_inner(svc: sync::Arc<Service<'_>>, req: Request<Body>) -> SvcResponse {
|
|
||||||
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 != svc.http_domain.as_str() => Some(h),
|
|
||||||
(_, Some(h)) if h != svc.http_domain.as_str() => Some(h),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
.and_then(|h| domain::Name::from_str(h).ok());
|
|
||||||
|
|
||||||
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) = svc.domain_manager.get_acme_http01_challenge_key(token) {
|
|
||||||
let body: hyper::Body = key.into();
|
|
||||||
return match Response::builder().status(200).body(body) {
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => Err(format!(
|
|
||||||
"failed to write acme http-01 challenge key: {}",
|
|
||||||
err
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a managed domain was given then serve that from its origin
|
|
||||||
if let Some(domain) = maybe_host {
|
|
||||||
return svc.serve_origin(domain, req.uri().path());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve main domiply site
|
|
||||||
|
|
||||||
if method == Method::GET && path.starts_with("/static/") {
|
|
||||||
return svc.render(200, path, ());
|
|
||||||
}
|
|
||||||
|
|
||||||
match (method, path) {
|
|
||||||
(&Method::GET, "/") | (&Method::GET, "/index.html") => svc.render_page("/index.html", ()),
|
|
||||||
(&Method::GET, "/domain.html") => {
|
|
||||||
svc.with_query_req(&req, |args: DomainGetArgs| async { svc.domain_get(args) })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
(&Method::GET, "/domain_init.html") => {
|
|
||||||
svc.with_query_req(&req, |args: DomainInitArgs| async {
|
|
||||||
svc.with_query_req(&req, |config: util::FlatConfig| async {
|
|
||||||
svc.domain_init(args, config)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
(&Method::GET, "/domain_sync.html") => {
|
|
||||||
svc.with_query_req(&req, |args: DomainSyncArgs| async {
|
|
||||||
svc.with_query_req(&req, |config: util::FlatConfig| async {
|
|
||||||
svc.domain_sync(args, config).await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
(&Method::GET, "/domains.html") => svc.domains(),
|
|
||||||
_ => svc.render_error_page(404, "Page not found!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
444
src/service/http.rs
Normal file
444
src/service/http.rs
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
mod tasks;
|
||||||
|
mod tpl;
|
||||||
|
|
||||||
|
use futures::stream::futures_unordered::FuturesUnordered;
|
||||||
|
use hyper::{Body, Method, Request, Response};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{future, net, sync};
|
||||||
|
|
||||||
|
use crate::{domain, origin, service};
|
||||||
|
|
||||||
|
type SvcResponse = Result<Response<hyper::body::Body>, String>;
|
||||||
|
|
||||||
|
pub struct Service {
|
||||||
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
|
target_a: net::Ipv4Addr,
|
||||||
|
passphrase: String,
|
||||||
|
http_domain: domain::Name,
|
||||||
|
handlebars: handlebars::Handlebars<'static>,
|
||||||
|
|
||||||
|
canceller: CancellationToken,
|
||||||
|
wait_group: FuturesUnordered<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HTTPSParams {
|
||||||
|
pub listen_addr: net::SocketAddr,
|
||||||
|
pub cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
|
target_a: net::Ipv4Addr,
|
||||||
|
passphrase: String,
|
||||||
|
http_listen_addr: net::SocketAddr,
|
||||||
|
http_domain: domain::Name,
|
||||||
|
https_params: Option<HTTPSParams>,
|
||||||
|
) -> sync::Arc<Service> {
|
||||||
|
let service = sync::Arc::new(Service {
|
||||||
|
domain_manager: domain_manager.clone(),
|
||||||
|
target_a,
|
||||||
|
passphrase,
|
||||||
|
http_domain: http_domain.clone(),
|
||||||
|
handlebars: tpl::get(),
|
||||||
|
canceller: CancellationToken::new(),
|
||||||
|
wait_group: FuturesUnordered::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
service.wait_group.push(tasks::listen_http(
|
||||||
|
service.clone(),
|
||||||
|
service.canceller.clone(),
|
||||||
|
http_listen_addr,
|
||||||
|
http_domain.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(https_params) = https_params {
|
||||||
|
service.wait_group.push(tasks::listen_https(
|
||||||
|
service.clone(),
|
||||||
|
service.canceller.clone(),
|
||||||
|
https_params.cert_resolver,
|
||||||
|
https_params.listen_addr,
|
||||||
|
http_domain.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
service.wait_group.push(tasks::cert_refresher(
|
||||||
|
domain_manager,
|
||||||
|
service.canceller.clone(),
|
||||||
|
http_domain,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
pub async fn stop(self) {
|
||||||
|
self.canceller.cancel();
|
||||||
|
for f in self.wait_group {
|
||||||
|
f.await.expect("task failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct BasePresenter<'a, T> {
|
||||||
|
page_name: &'a str,
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DomainGetArgs {
|
||||||
|
domain: domain::Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DomainInitArgs {
|
||||||
|
domain: domain::Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DomainSyncArgs {
|
||||||
|
domain: domain::Name,
|
||||||
|
passphrase: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'svc> Service {
|
||||||
|
fn serve_string(&self, status_code: u16, path: &'_ str, body: Vec<u8>) -> SvcResponse {
|
||||||
|
let content_type = mime_guess::from_path(path)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
match Response::builder()
|
||||||
|
.status(status_code)
|
||||||
|
.header("Content-Type", content_type)
|
||||||
|
.body(body.into())
|
||||||
|
{
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(format!("failed to build {}: {}", path, err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//// TODO make this use an io::Write, rather than SvcResponse
|
||||||
|
fn render<T>(&self, status_code: u16, name: &'_ str, value: T) -> SvcResponse
|
||||||
|
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_string(status_code, name, rendered.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_error_page(&'svc self, status_code: u16, e: &'_ str) -> SvcResponse {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response<'a> {
|
||||||
|
error_msg: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render(
|
||||||
|
status_code,
|
||||||
|
"/base.html",
|
||||||
|
BasePresenter {
|
||||||
|
page_name: "/error.html",
|
||||||
|
data: &Response { error_msg: e },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_page<T>(&self, name: &'_ str, data: T) -> SvcResponse
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
self.render(
|
||||||
|
200,
|
||||||
|
"/base.html",
|
||||||
|
BasePresenter {
|
||||||
|
page_name: name,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_origin(&self, domain: domain::Name, path: &'_ str) -> SvcResponse {
|
||||||
|
let mut path_owned;
|
||||||
|
|
||||||
|
let path = match path.ends_with('/') {
|
||||||
|
true => {
|
||||||
|
path_owned = String::from(path);
|
||||||
|
path_owned.push_str("index.html");
|
||||||
|
path_owned.as_str()
|
||||||
|
}
|
||||||
|
false => path,
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin = match self.domain_manager.get_origin(&domain) {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(domain::manager::GetOriginError::NotFound) => {
|
||||||
|
return self.render_error_page(404, "Domain not found")
|
||||||
|
}
|
||||||
|
Err(domain::manager::GetOriginError::Unexpected(e)) => {
|
||||||
|
return self.render_error_page(500, format!("failed to fetch origin: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
match origin.read_file_into(path, &mut buf) {
|
||||||
|
Ok(_) => self.serve_string(200, path, buf),
|
||||||
|
Err(origin::ReadFileIntoError::FileNotFound) => {
|
||||||
|
self.render_error_page(404, "File not found")
|
||||||
|
}
|
||||||
|
Err(origin::ReadFileIntoError::Unexpected(e)) => {
|
||||||
|
self.render_error_page(500, format!("failed to fetch file {path}: {e}").as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn with_query_req<'a, F, In, Out>(&self, req: &'a Request<Body>, f: F) -> SvcResponse
|
||||||
|
where
|
||||||
|
In: Deserialize<'a>,
|
||||||
|
F: FnOnce(In) -> Out,
|
||||||
|
Out: future::Future<Output = SvcResponse>,
|
||||||
|
{
|
||||||
|
let query = req.uri().query().unwrap_or("");
|
||||||
|
match serde_urlencoded::from_str::<In>(query) {
|
||||||
|
Ok(args) => f(args).await,
|
||||||
|
Err(err) => Err(format!("failed to parse query args: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_get(&self, args: DomainGetArgs) -> SvcResponse {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
domain: domain::Name,
|
||||||
|
config: Option<domain::config::Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = match self.domain_manager.get_config(&args.domain) {
|
||||||
|
Ok(config) => Some(config),
|
||||||
|
Err(domain::manager::GetConfigError::NotFound) => None,
|
||||||
|
Err(domain::manager::GetConfigError::Unexpected(e)) => {
|
||||||
|
return self
|
||||||
|
.render_error_page(500, format!("retrieving configuration: {}", e).as_str());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_page(
|
||||||
|
"/domain.html",
|
||||||
|
Response {
|
||||||
|
domain: args.domain,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_init(
|
||||||
|
&self,
|
||||||
|
args: DomainInitArgs,
|
||||||
|
domain_config: service::util::FlatConfig,
|
||||||
|
) -> SvcResponse {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
domain: domain::Name,
|
||||||
|
flat_config: service::util::FlatConfig,
|
||||||
|
target_a: net::Ipv4Addr,
|
||||||
|
challenge_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: domain::config::Config = match domain_config.try_into() {
|
||||||
|
Ok(Some(config)) => config,
|
||||||
|
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
||||||
|
Err(e) => {
|
||||||
|
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config_hash = match config.hash() {
|
||||||
|
Ok(hash) => hash,
|
||||||
|
Err(e) => {
|
||||||
|
return self
|
||||||
|
.render_error_page(500, format!("failed to hash domain config: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_page(
|
||||||
|
"/domain_init.html",
|
||||||
|
Response {
|
||||||
|
domain: args.domain,
|
||||||
|
flat_config: config.into(),
|
||||||
|
target_a: self.target_a,
|
||||||
|
challenge_token: config_hash,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn domain_sync(
|
||||||
|
&self,
|
||||||
|
args: DomainSyncArgs,
|
||||||
|
domain_config: service::util::FlatConfig,
|
||||||
|
) -> SvcResponse {
|
||||||
|
if args.passphrase != self.passphrase.as_str() {
|
||||||
|
return self.render_error_page(401, "Incorrect passphrase");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: domain::config::Config = match domain_config.try_into() {
|
||||||
|
Ok(Some(config)) => config,
|
||||||
|
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
||||||
|
Err(e) => {
|
||||||
|
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sync_result = self
|
||||||
|
.domain_manager
|
||||||
|
.sync_with_config(args.domain.clone(), config)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
domain: domain::Name,
|
||||||
|
error_msg: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_msg = match sync_result {
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(domain::manager::SyncWithConfigError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
||||||
|
Err(domain::manager::SyncWithConfigError::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()),
|
||||||
|
Err(domain::manager::SyncWithConfigError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()),
|
||||||
|
Err(domain::manager::SyncWithConfigError::TargetANotSet) => Some("The A record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||||
|
Err(domain::manager::SyncWithConfigError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||||
|
Err(domain::manager::SyncWithConfigError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = Response {
|
||||||
|
domain: args.domain,
|
||||||
|
error_msg,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_page("/domain_sync.html", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domains(&self) -> SvcResponse {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
domains: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let domains = match self.domain_manager.all_domains() {
|
||||||
|
Ok(domains) => domains,
|
||||||
|
Err(e) => {
|
||||||
|
return self.render_error_page(500, format!("failed get all domains: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut domains: Vec<String> = domains
|
||||||
|
.into_iter()
|
||||||
|
.map(|domain| domain.as_str().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
domains.sort();
|
||||||
|
|
||||||
|
self.render_page("/domains.html", Response { domains })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(
|
||||||
|
svc: sync::Arc<Service>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, Infallible> {
|
||||||
|
match handle_request_inner(svc, req).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => panic!("unexpected error {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_port(host: &str) -> &str {
|
||||||
|
match host.rfind(':') {
|
||||||
|
None => host,
|
||||||
|
Some(i) => &host[..i],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request_inner(svc: sync::Arc<Service>, req: Request<Body>) -> SvcResponse {
|
||||||
|
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 != svc.http_domain.as_str() => Some(h),
|
||||||
|
(_, Some(h)) if h != svc.http_domain.as_str() => Some(h),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
.and_then(|h| domain::Name::from_str(h).ok());
|
||||||
|
|
||||||
|
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) = svc.domain_manager.get_acme_http01_challenge_key(token) {
|
||||||
|
let body: hyper::Body = key.into();
|
||||||
|
return match Response::builder().status(200).body(body) {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(format!(
|
||||||
|
"failed to write acme http-01 challenge key: {}",
|
||||||
|
err
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a managed domain was given then serve that from its origin
|
||||||
|
if let Some(domain) = maybe_host {
|
||||||
|
return svc.serve_origin(domain, req.uri().path());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve main domiply site
|
||||||
|
|
||||||
|
if method == Method::GET && path.starts_with("/static/") {
|
||||||
|
return svc.render(200, path, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
match (method, path) {
|
||||||
|
(&Method::GET, "/") | (&Method::GET, "/index.html") => svc.render_page("/index.html", ()),
|
||||||
|
(&Method::GET, "/domain.html") => {
|
||||||
|
svc.with_query_req(&req, |args: DomainGetArgs| async { svc.domain_get(args) })
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
(&Method::GET, "/domain_init.html") => {
|
||||||
|
svc.with_query_req(&req, |args: DomainInitArgs| async {
|
||||||
|
svc.with_query_req(&req, |config: service::util::FlatConfig| async {
|
||||||
|
svc.domain_init(args, config)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
(&Method::GET, "/domain_sync.html") => {
|
||||||
|
svc.with_query_req(&req, |args: DomainSyncArgs| async {
|
||||||
|
svc.with_query_req(&req, |config: service::util::FlatConfig| async {
|
||||||
|
svc.domain_sync(args, config).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
(&Method::GET, "/domains.html") => svc.domains(),
|
||||||
|
_ => svc.render_error_page(404, "Page not found!"),
|
||||||
|
}
|
||||||
|
}
|
140
src/service/http/tasks.rs
Normal file
140
src/service/http/tasks.rs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
use crate::{domain, service};
|
||||||
|
|
||||||
|
use std::{convert, future, net, sync};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
pub fn listen_http(
|
||||||
|
service: sync::Arc<service::http::Service>,
|
||||||
|
canceller: CancellationToken,
|
||||||
|
addr: net::SocketAddr,
|
||||||
|
domain: domain::Name,
|
||||||
|
) -> tokio::task::JoinHandle<()> {
|
||||||
|
let make_service = hyper::service::make_service_fn(move |_| {
|
||||||
|
let service = service.clone();
|
||||||
|
|
||||||
|
// Create a `Service` for responding to the request.
|
||||||
|
let hyper_service = hyper::service::service_fn(move |req| {
|
||||||
|
service::http::handle_request(service.clone(), req)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the service to hyper.
|
||||||
|
async move { Ok::<_, convert::Infallible>(hyper_service) }
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
log::info!("Listening on http://{}:{}", domain.as_str(), addr.port());
|
||||||
|
let server = hyper::Server::bind(&addr).serve(make_service);
|
||||||
|
|
||||||
|
let graceful = server.with_graceful_shutdown(async {
|
||||||
|
canceller.cancelled().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = graceful.await {
|
||||||
|
panic!("server error: {}", e);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen_https(
|
||||||
|
service: sync::Arc<service::http::Service>,
|
||||||
|
canceller: CancellationToken,
|
||||||
|
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
||||||
|
addr: net::SocketAddr,
|
||||||
|
domain: domain::Name,
|
||||||
|
) -> tokio::task::JoinHandle<()> {
|
||||||
|
let make_service = hyper::service::make_service_fn(move |_| {
|
||||||
|
let service = service.clone();
|
||||||
|
|
||||||
|
// Create a `Service` for responding to the request.
|
||||||
|
let hyper_service = hyper::service::service_fn(move |req| {
|
||||||
|
service::http::handle_request(service.clone(), req)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the service to hyper.
|
||||||
|
async move { Ok::<_, convert::Infallible>(hyper_service) }
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let server_config: tokio_rustls::TlsAcceptor = sync::Arc::new(
|
||||||
|
rustls::server::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_cert_resolver(cert_resolver),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let addr_incoming = hyper::server::conn::AddrIncoming::bind(&addr)
|
||||||
|
.expect("https listen socket creation failed");
|
||||||
|
|
||||||
|
let incoming =
|
||||||
|
tls_listener::TlsListener::new(server_config, addr_incoming).filter(|conn| {
|
||||||
|
if let Err(err) = conn {
|
||||||
|
log::error!("Error accepting TLS connection: {:?}", err);
|
||||||
|
future::ready(false)
|
||||||
|
} else {
|
||||||
|
future::ready(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let incoming = hyper::server::accept::from_stream(incoming);
|
||||||
|
|
||||||
|
log::info!("Listening on https://{}:{}", domain.as_str(), addr.port());
|
||||||
|
|
||||||
|
let server = hyper::Server::builder(incoming).serve(make_service);
|
||||||
|
|
||||||
|
let graceful = server.with_graceful_shutdown(async {
|
||||||
|
canceller.cancelled().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = graceful.await {
|
||||||
|
panic!("server error: {}", e);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cert_refresher(
|
||||||
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
|
canceller: CancellationToken,
|
||||||
|
http_domain: domain::Name,
|
||||||
|
) -> tokio::task::JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
let mut interval = time::interval(time::Duration::from_secs(60 * 60));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => (),
|
||||||
|
_ = canceller.cancelled() => return,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = domain_manager
|
||||||
|
.sync_cert(http_domain.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(|err| {
|
||||||
|
log::error!(
|
||||||
|
"Error while getting cert for {}: {err}",
|
||||||
|
http_domain.as_str()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let domains_iter = domain_manager.all_domains();
|
||||||
|
|
||||||
|
if let Err(err) = domains_iter {
|
||||||
|
log::error!("Got error calling all_domains: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for domain in domains_iter.unwrap().into_iter() {
|
||||||
|
let _ = domain_manager
|
||||||
|
.sync_cert(domain.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(|err| {
|
||||||
|
log::error!("Error while getting cert for {}: {err}", domain.as_str(),)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
|
|
||||||
#[derive(rust_embed::RustEmbed)]
|
#[derive(rust_embed::RustEmbed)]
|
||||||
#[folder = "src/service/http_tpl"]
|
#[folder = "src/service/http/tpl"]
|
||||||
#[prefix = "/"]
|
#[prefix = "/"]
|
||||||
struct Dir;
|
struct Dir;
|
||||||
|
|
||||||
pub fn get<'hbs>() -> Handlebars<'hbs> {
|
pub fn get() -> Handlebars<'static> {
|
||||||
let mut reg = Handlebars::new();
|
let mut reg = Handlebars::new();
|
||||||
reg.register_embed_templates::<Dir>()
|
reg.register_embed_templates::<Dir>()
|
||||||
.expect("registered embedded templates");
|
.expect("registered embedded templates");
|
Loading…
Reference in New Issue
Block a user