mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-21 20:16:50 +00:00
Replace the WinHTTP client with a ureq agent that keeps the same Client/Response surface: status errors come back as responses, gzip, 60 s connect / 30 s receive / 120 s total timeouts, rustls with the platform verifier and a per-request User-Agent override. Environment proxies come from ureq. On Windows, when none is set, the static Internet Settings proxy is read from HKCU and parsed by a pure core module (per-scheme ProxyServer, ProxyOverride globs and <local>); bypassed hosts go through a direct agent. PAC/WPAD is not evaluated. The usage providers and credential sources compile again on every platform. Refresh keeps the Windows candidates and CREATE_NO_WINDOW; Unix spawns claude/codex from PATH plus the usual per-user install directories and reaps the child on a detached thread. An empty CODEX_HOME now counts as unset and a float expiresAt is truncated instead of ignored. Fixture tests cover the OAuth usage body, rate-limit headers (including a 429 over loopback), the ChatGPT envelope, ISO 8601 edge cases, credential JSON and proxy strings. An ignored live_poll test polls the real providers.
222 lines
7.0 KiB
Rust
222 lines
7.0 KiB
Rust
//! Parsing of the Windows "Internet Settings" static proxy.
|
|
//!
|
|
//! The registry reader lives in `platform::windows`; everything here is pure
|
|
//! so it can be tested on any host. PAC/WPAD scripts are not evaluated.
|
|
|
|
/// A static system proxy and the hosts that bypass it.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SystemProxy {
|
|
/// Proxy URI such as `http://proxy.corp:8080`, ready for `ureq::Proxy::new`.
|
|
pub url: String,
|
|
/// `ProxyOverride` entries, trimmed and non-empty.
|
|
pub bypass: Vec<String>,
|
|
}
|
|
|
|
impl SystemProxy {
|
|
/// Whether a request to `host` must skip the proxy.
|
|
pub fn bypasses(&self, host: &str) -> bool {
|
|
bypass_matches(&self.bypass, host)
|
|
}
|
|
}
|
|
|
|
/// Build the proxy from the raw `ProxyEnable`, `ProxyServer` and
|
|
/// `ProxyOverride` registry values. Returns `None` unless the proxy is
|
|
/// enabled and the server string names a usable entry.
|
|
pub fn from_registry_values(
|
|
enable: Option<u32>,
|
|
server: Option<&str>,
|
|
proxy_override: Option<&str>,
|
|
) -> Option<SystemProxy> {
|
|
if enable != Some(1) {
|
|
return None;
|
|
}
|
|
let url = parse_proxy_server(server?)?;
|
|
Some(SystemProxy {
|
|
url,
|
|
bypass: parse_override(proxy_override.unwrap_or_default()),
|
|
})
|
|
}
|
|
|
|
/// Pick the proxy for HTTPS traffic out of a `ProxyServer` value.
|
|
///
|
|
/// A per-scheme value (`http=h:p;https=h:p;…`) uses the `https=` entry, else
|
|
/// the `http=` entry. A bare `h:p` applies to every scheme. The result always
|
|
/// carries a scheme, defaulting to `http://` (a CONNECT proxy).
|
|
pub fn parse_proxy_server(value: &str) -> Option<String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
return None;
|
|
}
|
|
let target = if value.contains('=') {
|
|
let entries: Vec<(String, &str)> = value
|
|
.split(';')
|
|
.filter_map(|entry| entry.split_once('='))
|
|
.map(|(scheme, addr)| (scheme.trim().to_ascii_lowercase(), addr.trim()))
|
|
.filter(|(_, addr)| !addr.is_empty())
|
|
.collect();
|
|
let pick = |scheme: &str| {
|
|
entries
|
|
.iter()
|
|
.find(|(s, _)| s == scheme)
|
|
.map(|(_, addr)| *addr)
|
|
};
|
|
pick("https").or_else(|| pick("http"))?
|
|
} else {
|
|
value
|
|
};
|
|
if target.contains("://") {
|
|
Some(target.to_string())
|
|
} else {
|
|
Some(format!("http://{target}"))
|
|
}
|
|
}
|
|
|
|
/// Split a `ProxyOverride` value on `;` into trimmed, non-empty entries.
|
|
pub fn parse_override(value: &str) -> Vec<String> {
|
|
value
|
|
.split(';')
|
|
.map(str::trim)
|
|
.filter(|entry| !entry.is_empty())
|
|
.map(str::to_string)
|
|
.collect()
|
|
}
|
|
|
|
/// Whether `host` matches any bypass entry. Entries are case-insensitive
|
|
/// globs where `*` matches any run of characters; `<local>` matches every
|
|
/// host without a dot.
|
|
pub fn bypass_matches(entries: &[String], host: &str) -> bool {
|
|
let host = host.to_ascii_lowercase();
|
|
entries.iter().any(|entry| {
|
|
if entry.eq_ignore_ascii_case("<local>") {
|
|
!host.contains('.')
|
|
} else {
|
|
glob_match(&entry.to_ascii_lowercase(), &host)
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Iterative `*`-only glob match with single-star backtracking.
|
|
fn glob_match(pattern: &str, text: &str) -> bool {
|
|
let p = pattern.as_bytes();
|
|
let t = text.as_bytes();
|
|
let (mut pi, mut ti) = (0, 0);
|
|
let mut star: Option<(usize, usize)> = None;
|
|
while ti < t.len() {
|
|
if pi < p.len() && p[pi] == b'*' {
|
|
star = Some((pi, ti));
|
|
pi += 1;
|
|
} else if pi < p.len() && p[pi] == t[ti] {
|
|
pi += 1;
|
|
ti += 1;
|
|
} else if let Some((sp, st)) = star {
|
|
pi = sp + 1;
|
|
ti = st + 1;
|
|
star = Some((sp, st + 1));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
p[pi..].iter().all(|&c| c == b'*')
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn entries(value: &str) -> Vec<String> {
|
|
parse_override(value)
|
|
}
|
|
|
|
#[test]
|
|
fn per_scheme_server_prefers_https_entry() {
|
|
assert_eq!(
|
|
parse_proxy_server("http=web:80;https=secure:8443;ftp=f:21").as_deref(),
|
|
Some("http://secure:8443")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn per_scheme_server_without_https_uses_http_entry() {
|
|
assert_eq!(
|
|
parse_proxy_server("ftp=f:21; HTTP=web:3128").as_deref(),
|
|
Some("http://web:3128")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn per_scheme_server_without_http_entries_is_none() {
|
|
assert_eq!(parse_proxy_server("socks=s:1080"), None);
|
|
assert_eq!(parse_proxy_server("https=;http="), None);
|
|
}
|
|
|
|
#[test]
|
|
fn bare_server_applies_to_all_schemes() {
|
|
assert_eq!(
|
|
parse_proxy_server(" proxy.corp:8080 ").as_deref(),
|
|
Some("http://proxy.corp:8080")
|
|
);
|
|
assert_eq!(
|
|
parse_proxy_server("http://proxy.corp:8080").as_deref(),
|
|
Some("http://proxy.corp:8080")
|
|
);
|
|
assert_eq!(parse_proxy_server(" "), None);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_values_need_enable_flag_and_server() {
|
|
assert_eq!(
|
|
from_registry_values(Some(0), Some("p:1"), Some("<local>")),
|
|
None
|
|
);
|
|
assert_eq!(from_registry_values(None, Some("p:1"), None), None);
|
|
assert_eq!(from_registry_values(Some(1), None, None), None);
|
|
assert_eq!(
|
|
from_registry_values(Some(1), Some("p:1"), Some("*.corp; <local> ;")),
|
|
Some(SystemProxy {
|
|
url: "http://p:1".into(),
|
|
bypass: vec!["*.corp".into(), "<local>".into()],
|
|
})
|
|
);
|
|
assert_eq!(
|
|
from_registry_values(Some(1), Some("p:1"), None).map(|p| p.bypass),
|
|
Some(Vec::new())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn override_wildcard_matches_subdomains_case_insensitively() {
|
|
let list = entries("*.anthropic.com");
|
|
assert!(bypass_matches(&list, "api.anthropic.com"));
|
|
assert!(bypass_matches(&list, "API.Anthropic.COM"));
|
|
assert!(!bypass_matches(&list, "anthropic.com"));
|
|
assert!(!bypass_matches(&list, "chatgpt.com"));
|
|
}
|
|
|
|
#[test]
|
|
fn override_exact_host_matches_only_that_host() {
|
|
let list = entries("chatgpt.com");
|
|
assert!(bypass_matches(&list, "chatgpt.com"));
|
|
assert!(!bypass_matches(&list, "api.chatgpt.com"));
|
|
assert!(!bypass_matches(&list, "chatgpt.com.evil"));
|
|
}
|
|
|
|
#[test]
|
|
fn override_local_matches_dotless_hosts() {
|
|
let list = entries("<LOCAL>");
|
|
assert!(bypass_matches(&list, "intranet"));
|
|
assert!(bypass_matches(&list, "localhost"));
|
|
assert!(!bypass_matches(&list, "api.anthropic.com"));
|
|
assert!(!bypass_matches(&list, "127.0.0.1"));
|
|
}
|
|
|
|
#[test]
|
|
fn override_globs_cover_prefixes_and_inner_stars() {
|
|
let list = entries("10.*;api.*.com");
|
|
assert!(bypass_matches(&list, "10.1.2.3"));
|
|
assert!(bypass_matches(&list, "api.anthropic.com"));
|
|
assert!(!bypass_matches(&list, "chatgpt.com"));
|
|
assert!(bypass_matches(&entries("*"), "anything.example"));
|
|
assert!(!bypass_matches(&[], "chatgpt.com"));
|
|
}
|
|
}
|