use crate::{
errors::{ConfigError, FileError, VetisError, VirtualHostError},
security::SecurityConfig,
Request, Response,
};
use radix_trie::Trie;
use serde::Deserialize;
use std::{collections::HashMap, future::Future, path::Path, pin::Pin, sync::Arc};
pub mod path;
pub type BoxedHandlerClosure = Box<
dyn Fn(Request) -> Pin<Box<dyn Future<Output = Result<Response, VetisError>> + Send + Sync>>
+ Send
+ Sync,
>;
pub fn handler_fn<F, Fut>(f: F) -> BoxedHandlerClosure
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response, VetisError>> + Send + Sync + 'static,
{
Box::new(move |req| Box::pin(f(req)))
}
pub struct VirtualHostConfigBuilder {
hostname: String,
port: u16,
root_directory: String,
default_headers: Option<Vec<(String, String)>>,
security: Option<SecurityConfig>,
status_pages: Option<HashMap<u16, String>>,
enable_logging: bool,
paths: Option<Vec<Box<dyn path::PathConfig>>>,
}
impl VirtualHostConfigBuilder {
pub fn hostname(mut self, hostname: &str) -> Self {
self.hostname = hostname.to_string();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn root_directory(mut self, root_directory: &str) -> Self {
self.root_directory = root_directory.to_string();
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
match self.default_headers {
None => {
let vec = vec![(key.to_string(), value.to_string())];
self.default_headers = Some(vec);
}
Some(ref mut headers) => {
headers.push((key.to_string(), value.to_string()));
}
}
self
}
pub fn security(mut self, security: SecurityConfig) -> Self {
self.security = Some(security);
self
}
pub fn status_pages(mut self, status_pages: HashMap<u16, String>) -> Self {
self.status_pages = Some(status_pages);
self
}
pub fn enable_logging(mut self, logging: bool) -> Self {
self.enable_logging = logging;
self
}
pub fn build(self) -> Result<VirtualHostConfig, VetisError> {
if self
.hostname
.is_empty()
{
return Err(VetisError::Config(ConfigError::VirtualHost(
"Missing hostname".to_string(),
)));
}
if self
.root_directory
.is_empty()
{
return Err(VetisError::Config(ConfigError::VirtualHost(
"Missing root directory".to_string(),
)));
} else {
let root_path = Path::new(&self.root_directory);
if !root_path.exists() {
return Err(VetisError::Config(ConfigError::VirtualHost(format!(
"root_directory does not exist: {}",
self.root_directory
))));
}
}
Ok(VirtualHostConfig {
hostname: self.hostname,
port: self.port,
root_directory: self.root_directory,
default_headers: self.default_headers,
security: self.security,
status_pages: self.status_pages,
enable_logging: self.enable_logging,
paths: self.paths,
})
}
}
#[derive(Deserialize)]
pub struct VirtualHostConfig {
hostname: String,
port: u16,
root_directory: String,
default_headers: Option<Vec<(String, String)>>,
#[serde(deserialize_with = "crate::security::deserialize_security_from_file")]
security: Option<SecurityConfig>,
status_pages: Option<HashMap<u16, String>>,
enable_logging: bool,
paths: Option<Vec<Box<dyn path::PathConfig>>>,
}
impl VirtualHostConfig {
pub fn builder() -> VirtualHostConfigBuilder {
VirtualHostConfigBuilder {
hostname: "localhost".to_string(),
port: 80,
root_directory: "/var/vetis/www".to_string(),
default_headers: None,
security: None,
status_pages: None,
enable_logging: true,
paths: None,
}
}
pub fn hostname(&self) -> &str {
&self.hostname
}
pub fn port(&self) -> u16 {
self.port
}
pub fn root_directory(&self) -> &str {
&self.root_directory
}
pub fn default_headers(&self) -> &Option<Vec<(String, String)>> {
&self.default_headers
}
pub fn security(&self) -> &Option<SecurityConfig> {
&self.security
}
pub fn status_pages(&self) -> &Option<HashMap<u16, String>> {
&self.status_pages
}
pub fn enable_logging(&self) -> bool {
self.enable_logging
}
pub fn paths(&self) -> &Option<Vec<Box<dyn path::PathConfig>>> {
&self.paths
}
}
pub trait VirtualHost {
fn paths(&self) -> Trie<String, Arc<Box<dyn path::Path>>>;
fn config(&self) -> &VirtualHostConfig;
fn hostname(&self) -> &str {
self.config()
.hostname()
}
fn port(&self) -> u16 {
self.config().port()
}
fn is_secure(&self) -> bool {
self.config()
.security()
.is_some()
}
fn serve_status_page(
&self,
status: u16,
) -> Pin<Box<dyn Future<Output = Result<Response, VetisError>> + Send + Sync + '_>>;
fn route(
&self,
request: Request,
) -> Pin<Box<dyn Future<Output = Result<Response, VetisError>> + Send + Sync + '_>>
where
Self: Sync,
{
let uri_path: String = request
.uri()
.path()
.into();
if uri_path.starts_with("..") {
return Box::pin(async move {
self.serve_status_page(http::StatusCode::FORBIDDEN.as_u16())
.await
});
}
let paths = self.paths();
let matches = paths.get_ancestor_value(&uri_path);
let Some(path) = matches else {
return Box::pin(async move {
self.serve_status_page(http::StatusCode::NOT_FOUND.as_u16())
.await
});
};
let path = path.clone();
let target_path: String = uri_path
.strip_prefix(path.uri())
.unwrap_or(&uri_path)
.into();
Box::pin(async move {
let result = path.handle(request, Arc::from(target_path));
match result.await {
Ok(response) => Ok(response),
Err(error) => {
match error {
VetisError::VirtualHost(VirtualHostError::File(FileError::NotFound)) => {
log::error!("Invalid path: {}", error);
return self
.serve_status_page(http::StatusCode::NOT_FOUND.as_u16())
.await;
}
VetisError::VirtualHost(VirtualHostError::Proxy(ref error)) => {
log::error!("Proxy error: {}", error);
return self
.serve_status_page(http::StatusCode::BAD_GATEWAY.as_u16())
.await;
}
VetisError::VirtualHost(VirtualHostError::Auth(e)) => {
log::error!("Auth error: {}", e);
return self
.serve_status_page(http::StatusCode::UNAUTHORIZED.as_u16())
.await;
}
_ => {}
}
Err(error)
}
}
})
}
}