diff --git a/.env.dev b/.env.dev index baaa036..123b75e 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,4 @@ +export DOMIPLY_HTTP_DOMAIN=localhost export DOMIPLY_PASSPHRASE=foobar export DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domiply_dev_env/origin/git export DOMIPLY_DOMAIN_CHECKER_TARGET_AAAA=::1 diff --git a/src/main.rs b/src/main.rs index 786ca1a..44d7df1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,9 @@ struct Cli { #[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")] http_listen_addr: SocketAddr, + #[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")] + http_domain: String, + #[arg(long, required = true, env = "DOMIPLY_PASSPHRASE")] passphrase: String, @@ -82,6 +85,7 @@ fn main() { manager, config.domain_checker_target_aaaa, config.passphrase, + config.http_domain, ); let service = sync::Arc::new(service); diff --git a/src/service.rs b/src/service.rs index d04fb52..22f25dd 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,24 +1,25 @@ -use http::status::StatusCode; use hyper::{Body, Method, Request, Response}; 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; +use crate::{domain, origin}; pub mod http_tpl; mod util; -type SvcResponse = Result, String>; +type SvcResponse = Result, String>; #[derive(Clone)] pub struct Service<'svc> { domain_manager: sync::Arc, target_aaaa: net::Ipv6Addr, passphrase: String, + http_domain: String, handlebars: handlebars::Handlebars<'svc>, } @@ -26,11 +27,13 @@ pub fn new<'svc, 'mgr>( domain_manager: sync::Arc, target_aaaa: net::Ipv6Addr, passphrase: String, + http_domain: String, ) -> Service<'svc> { Service { domain_manager, target_aaaa, passphrase, + http_domain, handlebars: self::http_tpl::get().expect("Retrieved Handlebars templates"), } } @@ -58,6 +61,21 @@ struct DomainSyncArgs { } impl<'svc> Service<'svc> { + fn serve_string(&self, status_code: u16, path: &'_ str, body: Vec) -> 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(&self, status_code: u16, name: &'_ str, value: T) -> SvcResponse where @@ -74,18 +92,7 @@ impl<'svc> Service<'svc> { } }; - let content_type = mime_guess::from_path(name) - .first_or_octet_stream() - .to_string(); - - match Response::builder() - .status(status_code) - .header("Content-Type", content_type) - .body(rendered) - { - Ok(res) => Ok(res), - Err(err) => Err(format!("failed to build {}: {}", name, err)), - } + self.serve_string(status_code, name, rendered.into()) } fn render_error_page(&'svc self, status_code: u16, e: &'_ str) -> SvcResponse { @@ -118,6 +125,40 @@ impl<'svc> Service<'svc> { ) } + 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::::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, f: F) -> SvcResponse where In: Deserialize<'a>, @@ -239,14 +280,17 @@ impl<'svc> Service<'svc> { pub async fn handle_request<'svc>( svc: sync::Arc>, req: Request, -) -> Result, Infallible> { +) -> Result, Infallible> { match handle_request_inner(svc, req).await { Ok(res) => Ok(res), - Err(err) => { - let mut res = Response::new(format!("failed to serve request: {}", err)); - *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - Ok(res) - } + Err(err) => panic!("unexpected error {err}"), + } +} + +fn strip_port(host: &str) -> &str { + match host.rfind(":") { + None => host, + Some(i) => &host[..i], } } @@ -254,6 +298,23 @@ pub async fn handle_request_inner<'svc>( svc: sync::Arc>, req: Request, ) -> 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 => 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();