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>; struct Renderer<'a, DM> where DM: domain::manager::Manager, { domain_manager: sync::Arc, target_cname: sync::Arc, passphrase: sync::Arc, handlebars: Handlebars<'a>, query_args: HashMap, } #[derive(Serialize)] struct BasePresenter<'a, T> { page_name: &'a str, query_args: &'a HashMap, data: &'a T, } impl<'a, DM> Renderer<'a, DM> where DM: domain::manager::Manager, { // TODO make this use an io::Write, rather than warp::Reply fn render(&self, name: &'_ str, value: &'_ T) -> Box 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 { #[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(), )) } fn render_page(&self, name: &'_ str, data: &'_ T) -> Box where T: Serialize, { self.render( "/base.html", &BasePresenter { page_name: name, query_args: &self.query_args, data, }, ) } } pub fn new( manager: DM, target_cname: domain::Name, passphrase: String, ) -> Result< impl warp::Filter + Clone + 'static, Box, > 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()?); let with_renderer = warp::any() .and(warp::query::>()) .map(move |query_args: HashMap| Renderer { domain_manager: manager.clone(), target_cname: target_cname.clone(), passphrase: passphrase.clone(), handlebars: hbs.clone(), query_args, }); let static_dir = warp::get() .and(with_renderer.clone()) .and(warp::path("static")) .and(warp::path::full()) .map(|renderer: Renderer<'_, DM>, full: warp::path::FullPath| { renderer.render(full.as_str(), &()) }); let index = warp::get() .and(with_renderer.clone()) .and(warp::path::end()) .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, } let domain_get = warp::get() .and(with_renderer.clone()) .and(warp::path!("domain.html")) .and(warp::query::()) .and(warp::query::()) .map( |renderer: Renderer<'_, DM>, req: DomainGetNewRequest, domain_config: util::ConfigFromURL| { match renderer.domain_manager.get_config(&req.domain) { Ok(_config) => renderer.render_error_page(500, "TODO not yet implemented"), Err(domain::manager::GetConfigError::NotFound) => { let domain_config = match domain_config.try_into() { Ok(domain_config) => domain_config, Err(e) => { return renderer.render_error_page( 400, format!("parsing domain configuration: {}", e).as_str(), ) } }; renderer.render_page( "/domain_get_new.html", &DomainGetNewResponse { domain: req.domain, config: domain_config, }, ) } Err(domain::manager::GetConfigError::Unexpected(e)) => renderer .render_error_page( 500, format!("retrieving configuration: {}", e).as_str(), ), } }, ); #[derive(Deserialize)] struct DomainPostRequest { _init: bool, domain: domain::Name, passphrase: String, } let domain_post = warp::post() .and(with_renderer.clone()) .and(warp::path!("domain.html")) .and(warp::query::()) .and(warp::query::()) .map( |renderer: Renderer<'_, DM>, req: DomainPostRequest, domain_config: util::ConfigFromURL| { if req.passphrase != renderer.passphrase.as_str() { return renderer.render_error_page(401, "Incorrect passphrase"); } //if req.init { #[derive(Serialize)] struct Response { domain: domain::Name, config: domain::config::Config, target_cname: domain::Name, challenge_token: String, } 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 config_hash = match config.hash() { Ok(hash) => hash, Err(e) => { return renderer.render_error_page( 500, format!("failed to hash domain config: {e}").as_str(), ) } }; let target_cname = (*renderer.target_cname).clone(); return renderer.render_page( "/domain_post_init.html", &Response { domain: req.domain, config: config, target_cname: target_cname, challenge_token: config_hash, }, ); //} }, ); let not_found = warp::any() .and(with_renderer.clone()) .map(|renderer: Renderer<'_, DM>| renderer.render_error_page(404, "Page not found")); Ok(static_dir .or(index) .or(domain_get) .or(domain_post) .or(not_found)) }