domani/src/service.rs

352 lines
11 KiB
Rust
Raw Normal View History

use hyper::{Body, Method, Request, Response};
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
use std::future::Future;
2023-05-15 16:23:53 +00:00
use std::net;
2023-05-15 18:25:07 +00:00
use std::str::FromStr;
use std::sync;
2023-05-15 18:25:07 +00:00
use crate::{domain, origin};
pub mod http_tpl;
mod util;
2023-05-15 18:25:07 +00:00
type SvcResponse = Result<Response<hyper::body::Body>, String>;
#[derive(Clone)]
pub struct Service<'svc> {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
2023-05-15 16:23:53 +00:00
target_aaaa: net::Ipv6Addr,
passphrase: String,
2023-05-15 18:25:07 +00:00
http_domain: String,
handlebars: handlebars::Handlebars<'svc>,
}
pub fn new<'svc, 'mgr>(
domain_manager: sync::Arc<dyn domain::manager::Manager>,
2023-05-15 16:23:53 +00:00
target_aaaa: net::Ipv6Addr,
passphrase: String,
2023-05-15 18:25:07 +00:00
http_domain: String,
) -> Service<'svc> {
Service {
domain_manager,
2023-05-15 16:23:53 +00:00
target_aaaa,
passphrase,
2023-05-15 18:25:07 +00:00
http_domain,
handlebars: self::http_tpl::get().expect("Retrieved Handlebars templates"),
}
}
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,
data: T,
2023-05-13 13:22:47 +00:00
}
#[derive(Deserialize)]
struct DomainGetArgs {
domain: domain::Name,
}
#[derive(Deserialize)]
struct DomainInitArgs {
domain: domain::Name,
passphrase: String,
}
#[derive(Deserialize)]
struct DomainSyncArgs {
domain: domain::Name,
}
impl<'svc> Service<'svc> {
2023-05-15 18:25:07 +00:00
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
2023-05-13 13:37:24 +00:00
where
T: Serialize,
{
let rendered = match self.handlebars.render(name, &value) {
2023-05-13 13:37:24 +00:00
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())
}
};
2023-05-15 18:25:07 +00:00
self.serve_string(status_code, name, rendered.into())
2023-05-13 13:37:24 +00:00
}
fn render_error_page(&'svc self, status_code: u16, e: &'_ str) -> SvcResponse {
2023-05-13 13:37:24 +00:00
#[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 },
},
)
2023-05-13 13:37:24 +00:00
}
fn render_page<T>(&self, name: &'_ str, data: T) -> SvcResponse
2023-05-13 13:37:24 +00:00
where
T: Serialize,
{
self.render(
200,
2023-05-13 13:37:24 +00:00
"/base.html",
BasePresenter {
2023-05-13 13:37:24 +00:00
page_name: name,
data,
},
)
}
2023-05-15 18:25:07 +00:00
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>,
}
match self.domain_manager.get_config(&args.domain) {
Ok(_config) => self.render_error_page(500, "TODO not yet implemented"),
Err(domain::manager::GetConfigError::NotFound) => self.render_page(
"/domain.html",
&Response {
domain: args.domain,
config: None,
},
),
Err(domain::manager::GetConfigError::Unexpected(e)) => {
self.render_error_page(500, format!("retrieving configuration: {}", e).as_str())
}
}
}
fn domain_init(&self, args: DomainInitArgs, domain_config: util::FlatConfig) -> SvcResponse {
if args.passphrase != self.passphrase.as_str() {
return self.render_error_page(401, "Incorrect passphrase");
}
#[derive(Serialize)]
struct Response {
domain: domain::Name,
flat_config: util::FlatConfig,
2023-05-15 16:23:53 +00:00
target_aaaa: net::Ipv6Addr,
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())
}
};
return self.render_page(
"/domain_init.html",
&Response {
domain: args.domain,
flat_config: config.into(),
2023-05-15 16:23:53 +00:00
target_aaaa: self.target_aaaa,
challenge_token: config_hash,
2023-05-13 14:34:51 +00:00
},
);
}
2023-05-13 14:34:51 +00:00
async fn domain_sync(
&self,
args: DomainSyncArgs,
domain_config: util::FlatConfig,
) -> SvcResponse {
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()),
2023-05-15 16:23:53 +00:00
Err(domain::manager::SyncWithConfigError::TargetAAAANotSet) => Some("The AAAA 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,
};
return self.render_page("/domain_sync.html", response);
2023-05-13 14:34:51 +00:00
}
}
2023-05-13 14:34:51 +00:00
pub async fn handle_request<'svc>(
svc: sync::Arc<Service<'svc>>,
req: Request<Body>,
2023-05-15 18:25:07 +00:00
) -> Result<Response<Body>, Infallible> {
match handle_request_inner(svc, req).await {
Ok(res) => Ok(res),
2023-05-15 18:25:07 +00:00
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>(
svc: sync::Arc<Service<'svc>>,
req: Request<Body>,
) -> SvcResponse {
2023-05-15 18:25:07 +00:00
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 => Some(h),
(_, Some(h)) if h != svc.http_domain => Some(h),
_ => None,
}
.and_then(|h| domain::Name::from_str(h).ok());
if let Some(domain) = maybe_host {
return svc.serve_origin(domain, req.uri().path());
}
let method = req.method();
let path = req.uri().path();
if method == &Method::GET && path.starts_with("/static/") {
return svc.render(200, path, ());
}
2023-05-13 13:22:47 +00:00
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
}
_ => svc.render_error_page(404, "Page not found!"),
}
}