domani/src/service.rs

322 lines
11 KiB
Rust
Raw Normal View History

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::sync;
use warp::Filter;
use crate::domain;
pub mod http_tpl;
mod util;
type Handlebars<'a> = sync::Arc<handlebars::Handlebars<'a>>;
2023-05-13 13:37:24 +00:00
struct Renderer<'a, DM>
where
DM: domain::manager::Manager,
{
domain_manager: sync::Arc<DM>,
target_cname: sync::Arc<domain::Name>,
passphrase: sync::Arc<String>,
handlebars: Handlebars<'a>,
query_args: HashMap<String, String>,
}
2023-05-13 13:22:47 +00:00
#[derive(Serialize)]
2023-05-13 13:37:24 +00:00
struct BasePresenter<'a, T> {
page_name: &'a str,
query_args: &'a HashMap<String, String>,
data: &'a T,
2023-05-13 13:22:47 +00:00
}
2023-05-13 13:37:24 +00:00
impl<'a, DM> Renderer<'a, DM>
where
DM: domain::manager::Manager,
{
2023-05-13 13:37:24 +00:00
// TODO make this use an io::Write, rather than warp::Reply
fn render<T>(&self, name: &'_ str, value: &'_ T) -> Box<dyn warp::Reply>
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())
}
};
let content_type = mime_guess::from_path(name)
.first_or_octet_stream()
.to_string();
let reply = warp::reply::html(rendered);
Box::from(warp::reply::with_header(
reply,
"Content-Type",
content_type,
))
}
fn render_error_page(&self, status_code: u16, e: &'_ str) -> Box<dyn warp::Reply> {
#[derive(Serialize)]
struct Response<'a> {
error_msg: &'a str,
}
Box::from(warp::reply::with_status(
self.render(
"/base.html",
&BasePresenter {
page_name: "/error.html",
query_args: &HashMap::default(),
data: &Response { error_msg: e },
},
),
status_code.try_into().unwrap(),
))
}
2023-05-13 13:37:24 +00:00
fn render_page<T>(&self, name: &'_ str, data: &'_ T) -> Box<dyn warp::Reply>
where
T: Serialize,
{
self.render(
"/base.html",
&BasePresenter {
page_name: name,
query_args: &self.query_args,
data,
},
)
}
}
pub fn new<DM>(
manager: DM,
target_cname: domain::Name,
passphrase: String,
) -> Result<
impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone + 'static,
Box<dyn Error>,
>
where
DM: domain::manager::Manager + 'static,
{
let manager = sync::Arc::new(manager);
let target_cname = sync::Arc::new(target_cname);
let passphrase = sync::Arc::new(passphrase);
let hbs = sync::Arc::new(self::http_tpl::get()?);
2023-05-13 13:37:24 +00:00
let with_renderer = warp::any()
.and(warp::query::<HashMap<String, String>>())
2023-05-13 13:37:24 +00:00
.map(move |query_args: HashMap<String, String>| Renderer {
domain_manager: manager.clone(),
target_cname: target_cname.clone(),
passphrase: passphrase.clone(),
handlebars: hbs.clone(),
query_args,
});
let static_dir = warp::get()
2023-05-13 13:37:24 +00:00
.and(with_renderer.clone())
.and(warp::path("static"))
.and(warp::path::full())
2023-05-13 13:37:24 +00:00
.map(|renderer: Renderer<'_, DM>, full: warp::path::FullPath| {
renderer.render(full.as_str(), &())
});
let index = warp::get()
2023-05-13 13:37:24 +00:00
.and(with_renderer.clone())
.and(warp::path::end())
2023-05-13 13:37:24 +00:00
.map(|renderer: Renderer<'_, DM>| renderer.render_page("/index.html", &()));
#[derive(Deserialize)]
struct DomainGetNewRequest {
domain: domain::Name,
}
#[derive(Serialize)]
struct DomainGetNewResponse {
domain: domain::Name,
config: Option<domain::config::Config>,
}
2023-05-13 14:34:51 +00:00
let domain = warp::get()
2023-05-13 13:37:24 +00:00
.and(with_renderer.clone())
.and(warp::path!("domain.html"))
.and(warp::query::<DomainGetNewRequest>())
2023-05-13 14:34:51 +00:00
.and(warp::query::<util::FlatConfig>())
.map(
2023-05-13 13:37:24 +00:00
|renderer: Renderer<'_, DM>,
req: DomainGetNewRequest,
2023-05-13 14:34:51 +00:00
domain_config: util::FlatConfig| {
2023-05-13 13:37:24 +00:00
match renderer.domain_manager.get_config(&req.domain) {
Ok(_config) => renderer.render_error_page(500, "TODO not yet implemented"),
2023-05-13 13:22:47 +00:00
Err(domain::manager::GetConfigError::NotFound) => {
let domain_config = match domain_config.try_into() {
Ok(domain_config) => domain_config,
Err(e) => {
2023-05-13 13:37:24 +00:00
return renderer.render_error_page(
2023-05-13 13:22:47 +00:00
400,
2023-05-13 13:37:24 +00:00
format!("parsing domain configuration: {}", e).as_str(),
2023-05-13 13:22:47 +00:00
)
}
};
2023-05-13 13:37:24 +00:00
renderer.render_page(
2023-05-13 14:34:51 +00:00
"/domain.html",
2023-05-13 13:37:24 +00:00
&DomainGetNewResponse {
2023-05-13 13:22:47 +00:00
domain: req.domain,
config: domain_config,
},
)
}
2023-05-13 13:37:24 +00:00
Err(domain::manager::GetConfigError::Unexpected(e)) => renderer
.render_error_page(
500,
format!("retrieving configuration: {}", e).as_str(),
),
}
},
);
#[derive(Deserialize)]
2023-05-13 14:34:51 +00:00
struct DomainInitRequest {
domain: domain::Name,
passphrase: String,
}
2023-05-13 14:34:51 +00:00
let domain_init = warp::get()
2023-05-13 13:37:24 +00:00
.and(with_renderer.clone())
2023-05-13 14:34:51 +00:00
.and(warp::path!("domain_init.html"))
.and(warp::query::<DomainInitRequest>())
.and(warp::query::<util::FlatConfig>())
.map(
2023-05-13 13:37:24 +00:00
|renderer: Renderer<'_, DM>,
2023-05-13 14:34:51 +00:00
req: DomainInitRequest,
domain_config: util::FlatConfig| {
2023-05-13 13:37:24 +00:00
if req.passphrase != renderer.passphrase.as_str() {
return renderer.render_error_page(401, "Incorrect passphrase");
}
#[derive(Serialize)]
struct Response {
domain: domain::Name,
2023-05-13 14:34:51 +00:00
flat_config: util::FlatConfig,
target_cname: domain::Name,
challenge_token: String,
}
2023-05-13 13:22:47 +00:00
let config: domain::config::Config = match domain_config.try_into() {
Ok(Some(config)) => config,
Ok(None) => {
2023-05-13 13:37:24 +00:00
return renderer.render_error_page(400, "domain config is required")
2023-05-13 13:22:47 +00:00
}
Err(e) => {
2023-05-13 13:37:24 +00:00
return renderer
.render_error_page(400, format!("invalid domain config: {e}").as_str())
2023-05-13 13:22:47 +00:00
}
};
let config_hash = match config.hash() {
Ok(hash) => hash,
Err(e) => {
2023-05-13 13:37:24 +00:00
return renderer.render_error_page(
2023-05-13 13:22:47 +00:00
500,
2023-05-13 13:37:24 +00:00
format!("failed to hash domain config: {e}").as_str(),
2023-05-13 13:22:47 +00:00
)
}
};
2023-05-13 13:37:24 +00:00
let target_cname = (*renderer.target_cname).clone();
2023-05-13 13:37:24 +00:00
return renderer.render_page(
2023-05-13 14:34:51 +00:00
"/domain_init.html",
2023-05-13 13:37:24 +00:00
&Response {
domain: req.domain,
2023-05-13 14:34:51 +00:00
flat_config: config.into(),
target_cname: target_cname,
challenge_token: config_hash,
},
);
2023-05-13 14:34:51 +00:00
},
);
#[derive(Deserialize)]
struct DomainSyncRequest {
domain: domain::Name,
}
let domain_sync = warp::get()
.and(with_renderer.clone())
.and(warp::path!("domain_sync.html"))
.and(warp::query::<DomainSyncRequest>())
.and(warp::query::<util::FlatConfig>())
.map(
|renderer: Renderer<'_, DM>,
req: DomainSyncRequest,
domain_config: util::FlatConfig| {
let config: domain::config::Config = match domain_config.try_into() {
Ok(Some(config)) => config,
Ok(None) => {
return renderer.render_error_page(400, "domain config is required")
}
Err(e) => {
return renderer
.render_error_page(400, format!("invalid domain config: {e}").as_str())
}
};
let sync_result = renderer
.domain_manager
.sync_with_config(&req.domain, &config);
#[derive(Serialize)]
struct Response {
domain: domain::Name,
flat_config: util::FlatConfig,
error_msg: Option<String>,
}
let mut response = Response {
domain: req.domain,
flat_config: config.into(),
error_msg: None,
};
response.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::TargetCNAMENotSet) => Some("The CNAME 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}")),
};
return renderer.render_page(
"/domain_sync.html",
&response,
)
},
);
2023-05-13 13:37:24 +00:00
let not_found = warp::any()
.and(with_renderer.clone())
.map(|renderer: Renderer<'_, DM>| renderer.render_error_page(404, "Page not found"));
2023-05-13 13:22:47 +00:00
Ok(static_dir
.or(index)
2023-05-13 14:34:51 +00:00
.or(domain)
.or(domain_init)
.or(domain_sync)
2023-05-13 13:22:47 +00:00
.or(not_found))
}