1use std::net::ToSocketAddrs;
2
3use crate::ProxyError;
4
5pub fn parse_backend(backend: &str) -> Result<(String, u16), ProxyError> {
12 if backend.contains("://") {
13 let url = url::Url::parse(backend)
14 .map_err(|e| ProxyError::InvalidConfig(format!("invalid backend URL: {e}")))?;
15 let host = url
16 .host_str()
17 .ok_or_else(|| ProxyError::InvalidConfig("backend URL missing host".to_string()))?
18 .to_string();
19 let port = url
20 .port_or_known_default()
21 .ok_or_else(|| ProxyError::InvalidConfig("backend URL missing port".to_string()))?;
22 return Ok((host, port));
23 }
24
25 let (host, port) = backend.rsplit_once(':').ok_or_else(|| {
26 ProxyError::InvalidConfig("backend must be host:port or a URL".to_string())
27 })?;
28 let port: u16 = port
29 .parse()
30 .map_err(|_| ProxyError::InvalidConfig(format!("invalid backend port: {port}")))?;
31 if host.is_empty() {
32 return Err(ProxyError::InvalidConfig(
33 "backend missing host".to_string(),
34 ));
35 }
36 Ok((host.to_string(), port))
37}
38
39pub fn ensure_backend_allowed(backend: &str, allow_remote: bool) -> Result<(), ProxyError> {
45 let (host, port) = parse_backend(backend)?;
46
47 if allow_remote {
48 return Ok(());
49 }
50
51 if host.eq_ignore_ascii_case("localhost") {
52 return Ok(());
53 }
54
55 let addrs = (host.as_str(), port)
56 .to_socket_addrs()
57 .map_err(|e| ProxyError::InvalidConfig(format!("backend resolution failed: {e}")))?;
58
59 let mut any = false;
60 for addr in addrs {
61 any = true;
62 if !addr.ip().is_loopback() {
63 return Err(ProxyError::InvalidConfig(
64 "backend is not loopback; use --backend-remote to allow".to_string(),
65 ));
66 }
67 }
68
69 if !any {
70 return Err(ProxyError::InvalidConfig(
71 "backend host did not resolve to any address".to_string(),
72 ));
73 }
74
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn parses_url_form_with_explicit_port() {
84 let (host, port) = parse_backend("http://127.0.0.1:3000").unwrap();
85 assert_eq!(host, "127.0.0.1");
86 assert_eq!(port, 3000);
87 }
88
89 #[test]
90 fn parses_url_form_with_default_port() {
91 let (host, port) = parse_backend("https://example.test").unwrap();
92 assert_eq!(host, "example.test");
93 assert_eq!(port, 443);
94 }
95
96 #[test]
97 fn parses_bare_host_port() {
98 let (host, port) = parse_backend("127.0.0.1:8080").unwrap();
99 assert_eq!(host, "127.0.0.1");
100 assert_eq!(port, 8080);
101 }
102
103 #[test]
104 fn rejects_missing_port() {
105 assert!(parse_backend("127.0.0.1").is_err());
106 }
107
108 #[test]
109 fn loopback_backend_allowed_without_remote() {
110 assert!(ensure_backend_allowed("127.0.0.1:3000", false).is_ok());
111 assert!(ensure_backend_allowed("http://localhost:3000", false).is_ok());
112 }
113
114 #[test]
115 fn non_loopback_backend_rejected_without_remote() {
116 assert!(ensure_backend_allowed("192.0.2.10:3000", false).is_err());
118 }
119
120 #[test]
121 fn non_loopback_backend_allowed_with_remote() {
122 assert!(ensure_backend_allowed("192.0.2.10:3000", true).is_ok());
123 }
124}