use std::{fmt, net::{IpAddr, SocketAddr}, path::PathBuf, str::FromStr};
use hyper::{Uri, http::uri};
use crate::{Controller, Server};
pub const DEFAULT_CONTROL_PATH: &str = "/~~penguin";
#[derive(Debug, Clone)]
pub struct Config {
pub(crate) bind_addr: SocketAddr,
pub(crate) proxy: Option<ProxyTarget>,
pub(crate) mounts: Vec<Mount>,
pub(crate) control_path: String,
}
impl Config {
pub fn proxy(&self) -> Option<&ProxyTarget> {
self.proxy.as_ref()
}
pub fn mounts(&self) -> &[Mount] {
&self.mounts
}
pub fn control_path(&self) -> &str {
&self.control_path
}
}
#[derive(Debug, Clone)]
pub struct Builder(Config);
impl Builder {
pub(crate) fn new(bind_addr: SocketAddr) -> Self {
Self(Config {
bind_addr,
proxy: None,
control_path: DEFAULT_CONTROL_PATH.into(),
mounts: Vec::new(),
})
}
pub fn proxy(mut self, target: ProxyTarget) -> Self {
if let Some(prev) = self.0.proxy {
panic!(
"`Builder::proxy` called a second time: is called with '{}' now \
but was previously called with '{}'",
target,
prev,
);
}
self.0.proxy = Some(target);
self
}
pub fn add_mount(
mut self,
uri_path: impl Into<String>,
fs_path: impl Into<PathBuf>,
) -> Result<Self, ConfigError> {
let mut uri_path = uri_path.into();
normalize_path(&mut uri_path);
if self.0.mounts.iter().any(|other| other.uri_path == uri_path) {
return Err(ConfigError::DuplicateUriPath(uri_path));
}
self.0.mounts.push(Mount {
uri_path,
fs_path: fs_path.into(),
});
Ok(self)
}
pub fn set_control_path(mut self, path: impl Into<String>) -> Self {
self.0.control_path = path.into();
normalize_path(&mut self.0.control_path);
self
}
pub fn build(self) -> Result<(Server, Controller), ConfigError> {
self.validate().map(Server::build)
}
pub fn validate(self) -> Result<Config, ConfigError> {
if self.0.proxy.is_none() && self.0.mounts.is_empty() {
return Err(ConfigError::NoProxyOrMount)
}
if self.0.proxy.is_some() && self.0.mounts.iter().any(|other| other.uri_path == "/") {
return Err(ConfigError::ProxyAndRootMount);
}
Ok(self.0)
}
}
fn normalize_path(path: &mut String) {
if path.len() > 1 && path.ends_with('/') {
path.pop();
}
if !path.starts_with('/') {
path.insert(0, '/');
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("URI path '{0}' was added as mount twice")]
DuplicateUriPath(String),
#[error("a proxy was configured but a mount on '/' was added as well (in \
that case, the proxy is would be ignored)")]
ProxyAndRootMount,
#[error("neither a proxy nor a mount was specified: server would always \
respond 404 in this case")]
NoProxyOrMount,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProxyTarget {
pub(crate) scheme: uri::Scheme,
pub(crate) authority: uri::Authority,
}
impl From<(uri::Scheme, uri::Authority)> for ProxyTarget {
fn from((scheme, authority): (uri::Scheme, uri::Authority)) -> Self {
Self { scheme, authority }
}
}
impl fmt::Display for ProxyTarget {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}://{}", self.scheme, self.authority)
}
}
impl FromStr for ProxyTarget {
type Err = ProxyTargetParseError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let parts = src.parse::<Uri>()?.into_parts();
let has_real_path = parts.path_and_query.as_ref()
.map_or(false, |pq| !pq.as_str().is_empty() && pq.as_str() != "/");
if has_real_path {
return Err(ProxyTargetParseError::HasPath);
}
let authority = parts.authority.ok_or(ProxyTargetParseError::MissingAuthority)?;
let scheme = parts.scheme
.or_else(|| {
let ip = authority.host().parse::<IpAddr>();
if authority.host() == "localhost" || ip.map_or(false, |ip| ip.is_loopback()) {
Some(uri::Scheme::HTTP)
} else {
None
}
})
.ok_or(ProxyTargetParseError::MissingScheme)?;
Ok(Self { scheme, authority })
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProxyTargetParseError {
#[error("invalid URI: {0}")]
InvalidUri(#[from] uri::InvalidUri),
#[error("proxy target has path which is not allowed")]
HasPath,
#[error("proxy target has no scheme ('http' or 'https') specified, but a \
scheme must be specified for non-local targets")]
MissingScheme,
#[error("proxy target has no authority (\"host\") specified")]
MissingAuthority,
}
#[derive(Debug, Clone)]
pub struct Mount {
pub uri_path: String,
pub fs_path: PathBuf,
}