trillium_proxy/
upstream.rs1use std::{borrow::Cow, fmt::Debug};
3use trillium::Conn;
4use url::Url;
5
6#[cfg(feature = "upstream-connection-counting")]
7mod connection_counting;
8#[cfg(feature = "upstream-random")]
9mod random;
10mod round_robin;
11
12#[cfg(feature = "upstream-connection-counting")]
13pub use connection_counting::ConnectionCounting;
14#[cfg(feature = "upstream-random")]
15pub use random::RandomSelector;
16pub use round_robin::RoundRobin;
17
18pub trait UpstreamSelector: Debug + Send + Sync + 'static {
20 fn determine_upstream(&self, conn: &mut Conn) -> Option<Url>;
22
23 fn boxed(self) -> Box<dyn UpstreamSelector>
25 where
26 Self: Sized,
27 {
28 Box::new(self.into_upstream())
29 }
30}
31
32impl UpstreamSelector for Box<dyn UpstreamSelector> {
33 fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
34 UpstreamSelector::determine_upstream(&**self, conn)
35 }
36
37 fn boxed(self) -> Box<dyn UpstreamSelector> {
38 self
39 }
40}
41
42pub trait IntoUpstreamSelector {
47 type UpstreamSelector: UpstreamSelector;
49 fn into_upstream(self) -> Self::UpstreamSelector;
51}
52
53impl<U: UpstreamSelector> IntoUpstreamSelector for U {
54 type UpstreamSelector = Self;
55
56 fn into_upstream(self) -> Self {
57 self
58 }
59}
60
61impl IntoUpstreamSelector for &str {
62 type UpstreamSelector = Url;
63
64 fn into_upstream(self) -> Url {
65 let url = match Url::try_from(self) {
66 Ok(url) => url,
67 Err(_) => panic!("could not convert proxy target into a url"),
68 };
69
70 assert!(!url.cannot_be_a_base(), "{url} cannot be a base");
71 url
72 }
73}
74
75impl IntoUpstreamSelector for String {
76 type UpstreamSelector = Url;
77
78 fn into_upstream(self) -> Url {
79 (&*self).into_upstream()
80 }
81}
82
83#[derive(Debug, Copy, Clone)]
84pub struct ForwardProxy;
86impl UpstreamSelector for ForwardProxy {
87 fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
88 conn.path_and_query().parse().ok()
89 }
90}
91
92impl UpstreamSelector for Url {
93 fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
94 join_within_base(self, conn.path_and_query())
95 }
96}
97
98fn join_within_base(base: &Url, path_and_query: &str) -> Option<Url> {
110 let base = if base.path().ends_with('/') {
114 Cow::Borrowed(base)
115 } else {
116 let mut owned = base.clone();
117 owned.set_path(&format!("{}/", base.path()));
118 Cow::Owned(owned)
119 };
120
121 let upstream = base
126 .join(&format!("./{}", path_and_query.trim_start_matches('/')))
127 .ok()?;
128
129 let base_path = base.path();
130 let base_dir = &base_path[..=base_path.rfind('/').unwrap_or(0)];
131 upstream.path().starts_with(base_dir).then_some(upstream)
132}
133
134impl<F> UpstreamSelector for F
135where
136 F: Fn(&mut Conn) -> Option<Url> + Debug + Send + Sync + 'static,
137{
138 fn determine_upstream(&self, conn: &mut Conn) -> Option<Url> {
139 self(conn)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::join_within_base;
146 use url::Url;
147
148 fn base(s: &str) -> Url {
149 Url::parse(s).unwrap()
150 }
151
152 #[test]
153 fn dot_segments_cannot_escape_a_base_path_prefix() {
154 let base = base("http://upstream.example/app/");
155
156 for escape in [
161 "/../secret",
162 "/../../etc/passwd",
163 "/a/../../secret",
164 "/%2e%2e/secret",
165 ] {
166 assert_eq!(
167 join_within_base(&base, escape),
168 None,
169 "{escape} escaped the base prefix"
170 );
171 }
172
173 assert_eq!(join_within_base(&base, "/foo").unwrap().path(), "/app/foo");
175 assert_eq!(join_within_base(&base, "/a/../b").unwrap().path(), "/app/b");
176 }
177
178 #[test]
179 fn resolution_never_leaves_the_base_host() {
180 let base = base("http://upstream.example/");
181 for input in [
182 "/https://sneaky.com",
183 "/../secret",
184 "/a/../b",
185 "/trillium::Handler",
186 "//sneaky.com/path",
187 ] {
188 if let Some(url) = join_within_base(&base, input) {
189 assert_eq!(
190 url.host_str(),
191 Some("upstream.example"),
192 "{input} left the base host -> {url}"
193 );
194 }
195 }
196 }
197
198 #[test]
199 fn base_without_trailing_slash_keeps_its_prefix() {
200 let base = base("http://upstream.example/api");
204 assert_eq!(
205 join_within_base(&base, "/users").unwrap().as_str(),
206 "http://upstream.example/api/users"
207 );
208 assert_eq!(join_within_base(&base, "/../secret"), None);
210 }
211
212 #[test]
213 fn query_and_first_segment_colons_survive_containment() {
214 let base = base("http://upstream.example/");
215 let url = join_within_base(&base, "/trillium::Handler?x=1&y=2").unwrap();
216 assert_eq!(url.path(), "/trillium::Handler");
217 assert_eq!(url.query(), Some("x=1&y=2"));
218 }
219}