use std::{borrow::Cow, fmt::Debug};
use trillium::Conn;
use url::Url;
#[cfg(feature = "upstream-connection-counting")]
mod connection_counting;
#[cfg(feature = "upstream-random")]
mod random;
mod round_robin;
#[cfg(feature = "upstream-connection-counting")]
pub use connection_counting::ConnectionCounting;
#[cfg(feature = "upstream-random")]
pub use random::RandomSelector;
pub use round_robin::RoundRobin;
pub trait UpstreamSelector: Debug + Send + Sync + 'static {
fn determine_upstream(&self, conn: &mut Conn) -> Option<Url>;
fn boxed(self) -> Box<dyn UpstreamSelector>
where
Self: Sized,
{
Box::new(self.into_upstream())
}
}
impl UpstreamSelector for Box<dyn UpstreamSelector> {
fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
UpstreamSelector::determine_upstream(&**self, conn)
}
fn boxed(self) -> Box<dyn UpstreamSelector> {
self
}
}
pub trait IntoUpstreamSelector {
type UpstreamSelector: UpstreamSelector;
fn into_upstream(self) -> Self::UpstreamSelector;
}
impl<U: UpstreamSelector> IntoUpstreamSelector for U {
type UpstreamSelector = Self;
fn into_upstream(self) -> Self {
self
}
}
impl IntoUpstreamSelector for &str {
type UpstreamSelector = Url;
fn into_upstream(self) -> Url {
let url = match Url::try_from(self) {
Ok(url) => url,
Err(_) => panic!("could not convert proxy target into a url"),
};
assert!(!url.cannot_be_a_base(), "{url} cannot be a base");
url
}
}
impl IntoUpstreamSelector for String {
type UpstreamSelector = Url;
fn into_upstream(self) -> Url {
(&*self).into_upstream()
}
}
#[derive(Debug, Copy, Clone)]
pub struct ForwardProxy;
impl UpstreamSelector for ForwardProxy {
fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
conn.path_and_query().parse().ok()
}
}
impl UpstreamSelector for Url {
fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
join_within_base(self, conn.path_and_query())
}
}
fn join_within_base(base: &Url, path_and_query: &str) -> Option<Url> {
let base = if base.path().ends_with('/') {
Cow::Borrowed(base)
} else {
let mut owned = base.clone();
owned.set_path(&format!("{}/", base.path()));
Cow::Owned(owned)
};
let upstream = base
.join(&format!("./{}", path_and_query.trim_start_matches('/')))
.ok()?;
let base_path = base.path();
let base_dir = &base_path[..=base_path.rfind('/').unwrap_or(0)];
upstream.path().starts_with(base_dir).then_some(upstream)
}
impl<F> UpstreamSelector for F
where
F: Fn(&mut Conn) -> Option<Url> + Debug + Send + Sync + 'static,
{
fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
self(conn)
}
}
#[cfg(test)]
mod tests {
use super::join_within_base;
use url::Url;
fn base(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn dot_segments_cannot_escape_a_base_path_prefix() {
let base = base("http://upstream.example/app/");
for escape in [
"/../secret",
"/../../etc/passwd",
"/a/../../secret",
"/%2e%2e/secret",
] {
assert_eq!(
join_within_base(&base, escape),
None,
"{escape} escaped the base prefix"
);
}
assert_eq!(join_within_base(&base, "/foo").unwrap().path(), "/app/foo");
assert_eq!(join_within_base(&base, "/a/../b").unwrap().path(), "/app/b");
}
#[test]
fn resolution_never_leaves_the_base_host() {
let base = base("http://upstream.example/");
for input in [
"/https://sneaky.com",
"/../secret",
"/a/../b",
"/trillium::Handler",
"//sneaky.com/path",
] {
if let Some(url) = join_within_base(&base, input) {
assert_eq!(
url.host_str(),
Some("upstream.example"),
"{input} left the base host -> {url}"
);
}
}
}
#[test]
fn base_without_trailing_slash_keeps_its_prefix() {
let base = base("http://upstream.example/api");
assert_eq!(
join_within_base(&base, "/users").unwrap().as_str(),
"http://upstream.example/api/users"
);
assert_eq!(join_within_base(&base, "/../secret"), None);
}
#[test]
fn query_and_first_segment_colons_survive_containment() {
let base = base("http://upstream.example/");
let url = join_within_base(&base, "/trillium::Handler?x=1&y=2").unwrap();
assert_eq!(url.path(), "/trillium::Handler");
assert_eq!(url.query(), Some("x=1&y=2"));
}
}