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; /* * POST /domain/config (domain, config, secret, init?) -> token? * GET /domain/config (domain) -> config * GET /domains */ type Handlebars<'a> = sync::Arc>; struct RenderContext<'a, DM> where DM: domain::manager::Manager, { domain_manager: sync::Arc, target_cname: sync::Arc, passphrase: sync::Arc, handlebars: Handlebars<'a>, query_args: HashMap, } // TODO make this use an io::Write, rather than warp::Reply fn render<'a, T>(handlebars: Handlebars<'a>, name: &'a str, value: T) -> Box where T: Serialize, { let rendered = match handlebars.render(name, &value) { Ok(res) => res, Err(handlebars::RenderError { template_name: None, .. }) => return render_error_page(handlebars, 404, "Static asset not found".to_string()), Err(err) => return render_error_page(handlebars, 500, format!("template error: {err}")), }; 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, )) } #[derive(Serialize)] struct BasePresenter { page_name: String, query_args: HashMap, data: T, } fn render_error_page<'a>( handlebars: Handlebars<'a>, status_code: u16, e: String, ) -> Box { #[derive(Serialize)] struct Response { error_msg: String, } Box::from(warp::reply::with_status( render( handlebars, "/base.html", BasePresenter { page_name: "/error.html".to_string(), query_args: HashMap::default(), data: Response { error_msg: e }, }, ), status_code.try_into().unwrap(), )) } fn render_page<'a, T, DM>( render_ctx: RenderContext<'a, DM>, name: String, data: T, ) -> Box where T: Serialize, DM: domain::manager::Manager, { let presenter = BasePresenter { page_name: name, query_args: render_ctx.query_args, data, }; render(render_ctx.handlebars, "/base.html", presenter) } 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_render_ctx = warp::any() .and(warp::query::>()) .map(move |query_args: HashMap| RenderContext { domain_manager: manager.clone(), target_cname: target_cname.clone(), passphrase: passphrase.clone(), handlebars: hbs.clone(), query_args, }); let static_dir = warp::get() .and(with_render_ctx.clone()) .and(warp::path("static")) .and(warp::path::full()) .map( |render_ctx: RenderContext<'_, DM>, full: warp::path::FullPath| { render(render_ctx.handlebars, full.as_str(), ()) }, ); let index = warp::get() .and(with_render_ctx.clone()) .and(warp::path::end()) .map(|render_ctx: RenderContext<'_, DM>| { render_page(render_ctx, String::from("/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_render_ctx.clone()) .and(warp::path!("domain.html")) .and(warp::query::()) .and(warp::query::()) .map( |render_ctx: RenderContext<'_, DM>, req: DomainGetNewRequest, domain_config: util::ConfigFromURL| { match render_ctx.domain_manager.get_config(&req.domain) { Ok(_config) => render_error_page( render_ctx.handlebars, 500, "TODO not yet implemented".to_string(), ), Err(domain::manager::GetConfigError::NotFound) => { let domain_config = match domain_config.try_into() { Ok(domain_config) => domain_config, Err(e) => { return render_error_page( render_ctx.handlebars, 400, format!("parsing domain configuration: {}", e), ) } }; render_page( render_ctx, String::from("/domain_get_new.html"), DomainGetNewResponse { domain: req.domain, config: domain_config, }, ) } Err(domain::manager::GetConfigError::Unexpected(e)) => render_error_page( render_ctx.handlebars, 500, format!("retrieving configuration: {}", e), ), } }, ); #[derive(Deserialize)] struct DomainPostRequest { _init: bool, domain: domain::Name, passphrase: String, } let domain_post = warp::post() .and(with_render_ctx.clone()) .and(warp::path!("domain.html")) .and(warp::query::()) .and(warp::query::()) .map( |render_ctx: RenderContext<'_, DM>, req: DomainPostRequest, domain_config: util::ConfigFromURL| { if req.passphrase != render_ctx.passphrase.as_str() { return render_error_page( render_ctx.handlebars, 401, "Incorrect passphrase".to_string(), ); } //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 render_error_page( render_ctx.handlebars, 400, "domain config is required".to_string(), ) } Err(e) => { return render_error_page( render_ctx.handlebars, 400, format!("invalid domain config: {e}"), ) } }; let config_hash = match config.hash() { Ok(hash) => hash, Err(e) => { return render_error_page( render_ctx.handlebars, 500, format!("failed to hash domain config: {e}"), ) } }; let target_cname = (*render_ctx.target_cname).clone(); return render_page( render_ctx, String::from("/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_render_ctx.clone()) .map(|render_ctx: RenderContext<'_, DM>| { render_error_page(render_ctx.handlebars, 404, "Page not found".to_string()) }); Ok(static_dir .or(index) .or(domain_get) .or(domain_post) .or(not_found)) }