Implement serving of origins

This commit is contained in:
Brian Picciano 2023-05-15 20:25:07 +02:00
parent c1b6d982ef
commit 8f1c0cce22
3 changed files with 87 additions and 21 deletions

View File

@ -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

View File

@ -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);

View File

@ -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<Response<String>, String>;
type SvcResponse = Result<Response<hyper::body::Body>, String>;
#[derive(Clone)]
pub struct Service<'svc> {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
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<dyn domain::manager::Manager>,
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<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
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::<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>,
@ -239,14 +280,17 @@ impl<'svc> Service<'svc> {
pub async fn handle_request<'svc>(
svc: sync::Arc<Service<'svc>>,
req: Request<Body>,
) -> Result<Response<String>, Infallible> {
) -> Result<Response<Body>, 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<Service<'svc>>,
req: Request<Body>,
) -> 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();