1use axum::http::{HeaderMap, header};
4
5pub fn origin(headers: &HeaderMap, public_url: Option<&str>) -> Option<String> {
9 if let Some(url) = public_url.map(str::trim).filter(|u| !u.is_empty()) {
10 return Some(url.trim_end_matches('/').to_owned());
11 }
12 let value = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
13 let host = value("x-forwarded-host")
14 .or_else(|| value(header::HOST.as_str()))?
15 .split(',')
16 .next()?
17 .trim();
18 let scheme = value("x-forwarded-proto")
19 .and_then(|p| p.split(',').next())
20 .map(str::trim)
21 .filter(|p| matches!(*p, "http" | "https"))
22 .unwrap_or("http");
23 (!host.is_empty()).then(|| format!("{scheme}://{host}"))
24}
25
26#[cfg(test)]
27mod tests {
28 use super::*;
29
30 fn headers(pairs: &[(&'static str, &'static str)]) -> HeaderMap {
31 let mut h = HeaderMap::new();
32 for (k, v) in pairs {
33 h.insert(*k, v.parse().unwrap());
34 }
35 h
36 }
37
38 #[test]
39 fn the_configured_address_wins() {
40 let h = headers(&[("host", "10.0.0.2:4000")]);
41 assert_eq!(
42 origin(&h, Some("https://music.example.com/")).as_deref(),
43 Some("https://music.example.com")
44 );
45 }
46
47 #[test]
48 fn a_proxy_supplies_the_scheme_and_host() {
49 let h = headers(&[
50 ("host", "koan:4000"),
51 ("x-forwarded-host", "music.example.com"),
52 ("x-forwarded-proto", "https"),
53 ]);
54 assert_eq!(
55 origin(&h, None).as_deref(),
56 Some("https://music.example.com")
57 );
58 }
59
60 #[test]
61 fn a_direct_request_is_plain_http() {
62 let h = headers(&[("host", "192.168.1.5:4000")]);
63 assert_eq!(
64 origin(&h, Some("")).as_deref(),
65 Some("http://192.168.1.5:4000")
66 );
67 assert_eq!(origin(&HeaderMap::new(), None), None);
68 }
69}