subx_cli/services/ai/
security.rs1use url::Url;
7
8pub fn warn_on_insecure_http(url: &Url, api_key: &str) {
15 if url.scheme() != "http" {
16 return;
17 }
18 if api_key.trim().is_empty() {
19 return;
20 }
21 let host = url.host_str().unwrap_or("");
22 let is_loopback = matches!(host, "127.0.0.1" | "::1" | "localhost");
23 if is_loopback {
24 return;
25 }
26 log::warn!(
27 "AI endpoint uses plaintext HTTP ({}). API key will be transmitted unencrypted; consider using HTTPS.",
28 host
29 );
30}
31
32pub fn warn_on_insecure_http_str(url_str: &str, api_key: &str) {
37 if let Ok(url) = Url::parse(url_str) {
38 warn_on_insecure_http(&url, api_key);
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn https_never_warns() {
48 let url = Url::parse("https://api.example.com/v1").unwrap();
49 warn_on_insecure_http(&url, "sk-secret");
50 }
51
52 #[test]
53 fn http_loopback_does_not_warn() {
54 for host in [
55 "http://127.0.0.1:8080",
56 "http://localhost/v1",
57 "http://[::1]/",
58 ] {
59 let url = Url::parse(host).unwrap();
60 warn_on_insecure_http(&url, "sk-secret");
61 }
62 }
63
64 #[test]
65 fn http_public_with_empty_key_does_not_warn() {
66 let url = Url::parse("http://api.example.com/v1").unwrap();
67 warn_on_insecure_http(&url, "");
68 warn_on_insecure_http(&url, " ");
69 }
70
71 #[test]
72 fn http_public_with_key_runs_without_panic() {
73 let url = Url::parse("http://api.example.com/v1").unwrap();
76 warn_on_insecure_http(&url, "sk-secret");
77 }
78}