pitchfork_cli/proxy/
mod.rs1pub mod hostname;
14pub mod hosts;
15pub mod lan_ip;
16pub mod mdns;
17pub mod server;
18pub mod trust;
19pub mod worktree;
20
21pub(crate) fn ascii_case_collisions<'a>(
26 keys: impl Iterator<Item = &'a str>,
27) -> std::collections::HashSet<String> {
28 let mut seen = std::collections::HashSet::new();
29 let mut collisions = std::collections::HashSet::new();
30 for key in keys {
31 let folded = key.to_ascii_lowercase();
32 if !seen.insert(folded.clone()) {
33 collisions.insert(folded);
34 }
35 }
36 collisions
37}
38
39pub fn effective_tld(s: &crate::settings::Settings) -> &str {
43 if s.proxy.lan || !s.proxy.lan_ip.is_empty() {
44 "local"
45 } else {
46 &s.proxy.tld
47 }
48}
49
50pub fn build_proxy_url(host: Option<&str>, s: &crate::settings::Settings) -> Option<String> {
62 if !s.proxy.enable {
63 return None;
64 }
65 let host = host?;
66
67 let scheme = if s.proxy.https { "https" } else { "http" };
68 let tld = effective_tld(s);
69 let standard_port = if s.proxy.https { 443u16 } else { 80u16 };
70
71 let effective_port = u16::try_from(s.proxy.port).ok().filter(|&p| p > 0)?;
72
73 let authority = format!("{host}.{tld}");
74
75 Some(if effective_port == standard_port {
76 format!("{scheme}://{authority}")
77 } else {
78 format!("{scheme}://{authority}:{effective_port}")
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_ascii_case_collisions() {
88 let none = ascii_case_collisions(["myapp", "other", "third"].into_iter());
89 assert!(none.is_empty());
90
91 let folded = ascii_case_collisions(["MyApp", "myapp", "other"].into_iter());
92 assert_eq!(folded.len(), 1);
93 assert!(folded.contains("myapp"));
94
95 let exact = ascii_case_collisions(["dup", "dup"].into_iter());
97 assert!(exact.contains("dup"));
98
99 let unicode = ascii_case_collisions(["café", "CAFÉ"].into_iter());
101 assert!(unicode.is_empty());
102 }
103
104 fn proxy_settings() -> crate::settings::Settings {
105 let mut s = crate::settings::Settings::default();
106 s.proxy.enable = true;
107 s.proxy.https = true;
108 s.proxy.port = 443;
109 s.proxy.tld = "localhost".to_string();
110 s
111 }
112
113 #[test]
115 fn test_build_proxy_url_port_suffix() {
116 let mut s = proxy_settings();
117 assert_eq!(
118 build_proxy_url(Some("api.myproj"), &s).as_deref(),
119 Some("https://api.myproj.localhost")
120 );
121 s.proxy.port = 8088;
122 assert_eq!(
123 build_proxy_url(Some("api.myproj"), &s).as_deref(),
124 Some("https://api.myproj.localhost:8088")
125 );
126 s.proxy.https = false;
127 s.proxy.port = 80;
128 assert_eq!(
129 build_proxy_url(Some("api.myproj"), &s).as_deref(),
130 Some("http://api.myproj.localhost")
131 );
132 }
133
134 #[test]
137 fn test_build_proxy_url_requires_enabled_proxy_and_host() {
138 let mut s = proxy_settings();
139 assert_eq!(build_proxy_url(None, &s), None);
140 s.proxy.enable = false;
141 assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
142 }
143
144 #[test]
146 fn test_build_proxy_url_uses_lan_tld() {
147 let mut s = proxy_settings();
148 s.proxy.tld = "test".to_string();
149 assert_eq!(effective_tld(&s), "test");
150
151 s.proxy.lan = true;
152 assert_eq!(effective_tld(&s), "local");
153 assert_eq!(
154 build_proxy_url(Some("api.myproj"), &s).as_deref(),
155 Some("https://api.myproj.local")
156 );
157
158 s.proxy.lan = false;
159 s.proxy.lan_ip = "192.168.1.42".to_string();
160 assert_eq!(
161 build_proxy_url(Some("api.myproj"), &s).as_deref(),
162 Some("https://api.myproj.local")
163 );
164 }
165
166 #[test]
168 fn test_build_proxy_url_rejects_invalid_port() {
169 let mut s = proxy_settings();
170 s.proxy.port = 0;
171 assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
172 s.proxy.port = 70000;
173 assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
174 }
175}