Skip to main content

subx_cli/services/ai/
security.rs

1//! Shared security helpers for AI provider clients.
2//!
3//! These helpers centralize precautionary checks that every AI client should
4//! perform before sending credentials over the wire.
5
6use url::Url;
7
8/// Emit a warning if the parsed `url` uses plaintext HTTP against a non-local
9/// host while an API key is configured, because the API key would be sent
10/// unencrypted.
11///
12/// Loopback hosts (`127.0.0.1`, `::1`, `localhost`) are exempt because they
13/// never leave the machine.
14pub 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
32/// Convenience wrapper that parses `url_str` and forwards to
33/// [`warn_on_insecure_http`]. Parse errors are silently ignored because the
34/// caller's existing URL validation will have already rejected malformed
35/// URLs.
36pub 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        // Log output cannot be asserted without a test logger crate; we just
74        // make sure the branch is exercised.
75        let url = Url::parse("http://api.example.com/v1").unwrap();
76        warn_on_insecure_http(&url, "sk-secret");
77    }
78}