use crate::context::Environment;
use ureq::http::{Request, Response, Uri};
use ureq::middleware::{Middleware, MiddlewareNext};
use ureq::{Body, ProxyProtocol, SendBody};
pub(crate) const NAMING: [&str; 6] = [
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
];
pub(crate) const EXEMPTING: [&str; 2] = ["NO_PROXY", "no_proxy"];
#[derive(Clone, Default)]
pub(crate) struct Proxies {
named: Option<(&'static str, ureq::Proxy)>,
passed_over: Vec<&'static str>,
exempting: Option<&'static str>,
set: Vec<(&'static str, String)>,
}
impl Proxies {
pub(crate) fn read(env: &Environment) -> Proxies {
let exempting = EXEMPTING.into_iter().find(|name| env.text(name).is_some());
let entries: Vec<&str> = exempting
.and_then(|name| env.text(name))
.map_or_else(Vec::new, |list| list.split(',').collect());
let mut read = Proxies {
exempting,
set: NAMING
.iter()
.chain(&EXEMPTING)
.filter_map(|name| env.text(name).map(|value| (*name, value.to_string())))
.collect(),
..Proxies::default()
};
for name in NAMING {
let Some(value) = env.text(name) else {
continue;
};
match ureq::Proxy::new(value) {
Ok(proxy) => {
read.named = Some((name, exempting_from(&proxy, &entries)));
break;
}
Err(_) if value.is_empty() => {}
Err(_) => read.passed_over.push(name),
}
}
read
}
pub(crate) fn for_ureq(&self) -> Option<ureq::Proxy> {
self.named
.as_ref()
.map(|(_, proxy)| proxy)
.filter(|proxy| tunnels(proxy))
.cloned()
}
pub(crate) fn socks(&self) -> bool {
self.named
.as_ref()
.is_some_and(|(_, proxy)| !tunnels(proxy))
}
pub(crate) fn refusal(&self) -> Option<Refusal> {
self.named
.as_ref()
.filter(|(_, proxy)| !tunnels(proxy))
.map(|(variable, proxy)| Refusal {
variable,
proxy: proxy.clone(),
})
}
pub(crate) fn variable(&self) -> Option<&'static str> {
self.named.as_ref().map(|(name, _)| *name)
}
pub(crate) fn address(&self) -> Option<String> {
self.named.as_ref().map(|(_, proxy)| shown(proxy))
}
pub(crate) fn server(&self) -> Option<String> {
self.named.as_ref().map(|(_, proxy)| server(proxy))
}
pub(crate) fn on_loopback(&self) -> bool {
self.named.as_ref().is_some_and(|(_, proxy)| {
let host = proxy.host();
host.eq_ignore_ascii_case("localhost")
|| host
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
})
}
pub(crate) fn passed_over(&self) -> &[&'static str] {
&self.passed_over
}
pub(crate) fn exempting(&self) -> Option<&'static str> {
self.exempting
}
pub(crate) fn variables(&self) -> &[(&'static str, String)] {
&self.set
}
pub(crate) fn exempts(&self, url: &Uri) -> bool {
self.named
.as_ref()
.is_some_and(|(_, proxy)| proxy.is_no_proxy(url))
}
pub(crate) fn exempt_hosts(&self) -> Vec<String> {
called()
.iter()
.filter_map(|url| url.parse::<Uri>().ok())
.filter(|url| self.exempts(url))
.filter_map(|url| url.host().map(str::to_string))
.collect()
}
pub(crate) fn same_way(&self, other: &Proxies) -> bool {
match (&self.named, &other.named) {
(None, None) => true,
(Some((_, mine)), Some((_, theirs))) => {
mine.protocol() == theirs.protocol()
&& mine.host().eq_ignore_ascii_case(theirs.host())
&& mine.port() == theirs.port()
&& mine.username() == theirs.username()
&& mine.password() == theirs.password()
&& mine.resolve_target() == theirs.resolve_target()
&& self.exempt_hosts() == other.exempt_hosts()
}
_ => false,
}
}
}
impl std::fmt::Debug for Proxies {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Proxies")
.field("variable", &self.variable())
.field("address", &self.address())
.field("passed_over", &self.passed_over)
.field("exempting", &self.exempting)
.field(
"set",
&self.set.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
)
.finish()
}
}
fn tunnels(proxy: &ureq::Proxy) -> bool {
matches!(proxy.protocol(), ProxyProtocol::Http | ProxyProtocol::Https)
}
pub(crate) fn refused(variable: &str) -> String {
format!("Pitboard does not use SOCKS proxies yet, and {variable} names one")
}
pub(crate) struct Refusal {
variable: &'static str,
proxy: ureq::Proxy,
}
impl Middleware for Refusal {
fn handle(
&self,
request: Request<SendBody>,
next: MiddlewareNext,
) -> Result<Response<Body>, ureq::Error> {
if self.proxy.is_no_proxy(request.uri()) {
return next.handle(request);
}
Err(ureq::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
refused(self.variable),
)))
}
}
fn exempting_from(proxy: &ureq::Proxy, entries: &[&str]) -> ureq::Proxy {
let mut builder = ureq::Proxy::builder(proxy.protocol())
.host(proxy.host())
.port(proxy.port())
.resolve_target(proxy.resolve_target());
if let Some(user) = proxy.username() {
builder = builder.username(user);
}
if let Some(password) = proxy.password() {
builder = builder.password(password);
}
for entry in entries {
builder = builder.no_proxy(entry);
}
builder.build().unwrap_or_else(|_| proxy.clone())
}
pub(crate) fn shown(proxy: &ureq::Proxy) -> String {
let hidden = match (proxy.username(), proxy.password()) {
(Some(_), Some(_)) => "***:***@",
(Some(_), None) => "***@",
(None, _) => "",
};
format!(
"{}://{hidden}{}",
proxy.protocol().to_string().to_ascii_lowercase(),
server(proxy)
)
}
fn server(proxy: &ureq::Proxy) -> String {
format!("{}:{}", proxy.host(), proxy.port())
}
pub(crate) fn called() -> [&'static str; 4] {
[
crate::api::BASE,
crate::api::AUTH_BASE,
crate::provider::codex::api::TOKEN_URL,
crate::provider::codex::api::USAGE_BASE,
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::Context;
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
struct Row {
env: &'static [(&'static str, &'static str)],
named: Option<(&'static str, &'static str)>,
passed_over: &'static [&'static str],
exempting: Option<&'static str>,
exempt: &'static [&'static str],
}
const NONE: Row = Row {
env: &[],
named: None,
passed_over: &[],
exempting: None,
exempt: &[],
};
const TABLE: &[Row] = &[
NONE,
Row {
env: &[("HTTPS_PROXY", "http://p.example:8080")],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
..NONE
},
Row {
env: &[("https_proxy", "http://p.example:8080")],
named: Some(("https_proxy", "http://p.example:8080")),
..NONE
},
Row {
env: &[
("https_proxy", "http://lower.example:2"),
("HTTPS_PROXY", "http://upper.example:1"),
],
named: Some(("HTTPS_PROXY", "http://upper.example:1")),
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://https.example:2"),
("ALL_PROXY", "http://all.example:1"),
],
named: Some(("ALL_PROXY", "http://all.example:1")),
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://https.example:2"),
("all_proxy", "http://all.example:1"),
],
named: Some(("all_proxy", "http://all.example:1")),
..NONE
},
Row {
env: &[("HTTP_PROXY", "http://http.example:3")],
named: Some(("HTTP_PROXY", "http://http.example:3")),
..NONE
},
Row {
env: &[
("HTTP_PROXY", "http://http.example:3"),
("https_proxy", "http://https.example:2"),
],
named: Some(("https_proxy", "http://https.example:2")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", ""), ("HTTP_PROXY", "http://http.example:3")],
named: Some(("HTTP_PROXY", "http://http.example:3")),
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "not a proxy"),
("http_proxy", "http://http.example:3"),
],
named: Some(("http_proxy", "http://http.example:3")),
passed_over: &["HTTPS_PROXY"],
..NONE
},
Row {
env: &[("ALL_PROXY", "ftp://ftp.example"), ("https_proxy", "::")],
passed_over: &["ALL_PROXY", "https_proxy"],
..NONE
},
Row {
env: &[("HTTPS_PROXY", "proxy.example:3128")],
named: Some(("HTTPS_PROXY", "http://proxy.example:3128")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", "https://proxy.example")],
named: Some(("HTTPS_PROXY", "https://proxy.example:443")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", "http://[::1]:8080/")],
named: Some(("HTTPS_PROXY", "http://[::1]:8080")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", "http://alice:s3cret@p.example:8080")],
named: Some(("HTTPS_PROXY", "http://***:***@p.example:8080")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", "alice@p.example:8080")],
named: Some(("HTTPS_PROXY", "http://***@p.example:8080")),
..NONE
},
Row {
env: &[
("all_proxy", "socks5://127.0.0.1:7890"),
("https_proxy", "http://127.0.0.1:7890"),
("http_proxy", "http://127.0.0.1:7890"),
],
named: Some(("all_proxy", "socks5://127.0.0.1:7890")),
..NONE
},
Row {
env: &[("HTTPS_PROXY", "socks5h://p.example")],
named: Some(("HTTPS_PROXY", "socks5h://p.example:1080")),
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("NO_PROXY", ".anthropic.com,chatgpt.com"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("NO_PROXY"),
exempt: &["api.anthropic.com", "chatgpt.com"],
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("no_proxy", "*.OpenAI.com,Platform.Claude.com"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("no_proxy"),
exempt: &["platform.claude.com", "auth.openai.com"],
..NONE
},
Row {
env: &[("HTTPS_PROXY", "http://p.example:8080"), ("no_proxy", "*")],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("no_proxy"),
exempt: &[
"api.anthropic.com",
"platform.claude.com",
"auth.openai.com",
"chatgpt.com",
],
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("NO_PROXY", "api.,chat*"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("NO_PROXY"),
exempt: &["api.anthropic.com", "chatgpt.com"],
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("no_proxy", "*claude.com,*gpt.com"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("no_proxy"),
exempt: &["platform.claude.com", "chatgpt.com"],
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("NO_PROXY", ""),
("no_proxy", "*"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("NO_PROXY"),
..NONE
},
Row {
env: &[
("HTTPS_PROXY", "http://p.example:8080"),
("NO_PROXY", "anthropic.com, chatgpt.com"),
],
named: Some(("HTTPS_PROXY", "http://p.example:8080")),
exempting: Some("NO_PROXY"),
..NONE
},
];
fn environment(row: &Row) -> Environment {
row.env.iter().copied().collect()
}
fn described(proxy: &ureq::Proxy) -> String {
format!(
"{:?} {} {} {:?} {:?} {}",
proxy.protocol(),
proxy.host(),
proxy.port(),
proxy.username(),
proxy.password(),
proxy.resolve_target()
)
}
#[test]
fn each_environment_reads_as_ureq_reads_it() {
for (index, row) in TABLE.iter().enumerate() {
let read = Proxies::read(&environment(row));
let said = format!("row {index}: {:?}", row.env);
assert_eq!(
read.variable().zip(read.address()),
row.named.map(|(name, address)| (name, address.to_string())),
"{said}"
);
assert_eq!(read.passed_over(), row.passed_over, "{said}");
assert_eq!(read.exempting(), row.exempting, "{said}");
assert_eq!(read.exempt_hosts(), row.exempt, "{said}");
}
}
const ROW: &str = "PITBOARD_TEST_PROXY_ROW";
const AGREES: &str = "proxy::tests::every_row_agrees_with_what_ureq_reads_from_a_process";
#[test]
#[cfg_attr(
windows,
ignore = "W14: a Windows environment, whose names are one in any case"
)]
#[allow(
clippy::disallowed_methods,
reason = "a child is told which row it checks through its environment"
)]
fn every_row_agrees_with_what_ureq_reads_from_a_process() {
if let Some(row) = std::env::var_os(ROW) {
let index: usize = row.to_str().and_then(|r| r.parse().ok()).expect("a row");
return agrees_in_this_process(&TABLE[index]);
}
for (index, row) in TABLE.iter().enumerate() {
let mut child = std::process::Command::new(std::env::current_exe().expect("me"));
child
.args([AGREES, "--exact", "--test-threads=1"])
.env(ROW, index.to_string());
for name in NAMING.iter().chain(&EXEMPTING) {
child.env_remove(name);
}
for (name, value) in row.env {
child.env(name, value);
}
let out = child.output().expect("this test binary runs");
let said = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.status.success() && said.contains("1 passed"),
"row {index}, {:?}:\n{said}",
row.env
);
}
}
#[allow(
clippy::disallowed_methods,
reason = "ureq's own reading of this process's environment, which Pitboard's is held to"
)]
fn agrees_in_this_process(row: &Row) {
let ours = Proxies::read(&environment(row));
let theirs = ureq::Proxy::try_from_env();
assert_eq!(
ours.named.as_ref().map(|(_, proxy)| described(proxy)),
theirs.as_ref().map(described),
"the proxy"
);
let urls = called()
.iter()
.chain(&["http://127.0.0.1:8080/", "https://API.Anthropic.com/"])
.map(|url| url.parse::<Uri>().expect("a URL"))
.collect::<Vec<_>>();
for url in &urls {
assert_eq!(
ours.exempts(url),
theirs.as_ref().is_some_and(|proxy| proxy.is_no_proxy(url)),
"{url}"
);
}
let given: Environment = row
.env
.iter()
.copied()
.chain([("HOME", "/nowhere")])
.collect();
let ctx = Context::for_command_line(&given);
assert_eq!(
crate::api::agent(&ctx).config().proxy().map(described),
ours.for_ureq().as_ref().map(described),
"the agent"
);
assert!(
crate::api::agent(&Context::for_unit_test())
.config()
.proxy()
.is_none(),
"a unit test's agent goes through no proxy"
);
}
#[test]
fn ureq_is_given_an_http_proxy_and_never_a_socks_one() {
let read = |pairs: &[(&'static str, &str)]| Proxies::read(&pairs.iter().copied().collect());
let http = read(&[
("HTTPS_PROXY", "http://alice:s3cret@p.example:8080"),
("NO_PROXY", ".anthropic.com"),
]);
let proxy = http.for_ureq().expect("an HTTP proxy is given");
assert_eq!(
(
proxy.host(),
proxy.port(),
proxy.username(),
proxy.password()
),
("p.example", 8080, Some("alice"), Some("s3cret"))
);
let api = "https://api.anthropic.com/".parse::<Uri>().expect("a URL");
let auth = "https://auth.openai.com/".parse::<Uri>().expect("a URL");
assert!(proxy.is_no_proxy(&api) && !proxy.is_no_proxy(&auth));
assert!(!http.socks() && http.refusal().is_none());
let https = read(&[("HTTPS_PROXY", "https://p.example")]);
assert!(https.for_ureq().is_some() && !https.socks());
let ctx = read_from(&[("HTTPS_PROXY", "http://p.example:8080")]);
let agent = crate::api::agent(&ctx);
assert!(agent.config().proxy().is_some());
assert_eq!(agent.config().max_redirects(), 10, "ureq's own");
for address in [
"socks://alice:s3cret@p",
"socks4://p",
"socks4a://p",
"socks5://p",
"socks5h://p",
"SOCKS5H://p",
] {
let socks = read(&[("ALL_PROXY", address), ("NO_PROXY", ".anthropic.com")]);
assert!(socks.for_ureq().is_none(), "{address} is given");
assert!(socks.socks() && socks.refusal().is_some(), "{address}");
assert_eq!(socks.variable(), Some("ALL_PROXY"), "{address}");
assert!(socks.exempts(&api) && !socks.exempts(&auth), "{address}");
let ctx = read_from(&[("ALL_PROXY", address)]);
let agent = crate::api::agent(&ctx);
assert!(agent.config().proxy().is_none(), "{address}");
assert_eq!(agent.config().max_redirects(), 0, "{address}");
}
let none = read(&[]);
assert!(none.for_ureq().is_none() && !none.socks() && none.refusal().is_none());
}
#[test]
fn a_contexts_debug_form_never_shows_a_proxys_password() {
let env: Environment = [
("HOME", "/nowhere"),
("HTTPS_PROXY", "http://alice:s3cret@p.example:8080"),
]
.into_iter()
.collect();
let ctx = Context::for_command_line(&env);
let _ = crate::api::agent(&ctx);
let debug = format!("{ctx:?}");
assert!(debug.contains("HTTPS_PROXY"), "{debug}");
assert!(debug.contains("http://***:***@p.example:8080"), "{debug}");
for secret in ["alice", "s3cret"] {
assert!(!debug.contains(secret), "{secret}: {debug}");
}
}
#[test]
fn a_context_made_without_an_environment_names_no_proxy() {
let ctx = Context::new(std::path::PathBuf::from("/nowhere"));
assert_eq!(ctx.proxy.variable(), None);
assert!(crate::api::agent(&ctx).config().proxy().is_none());
}
fn read_from(pairs: &[(&'static str, &str)]) -> Context {
let env: Environment = pairs
.iter()
.copied()
.chain([("HOME", "/nowhere")])
.collect();
Context::for_command_line(&env)
}
const SOCKS: [(&str, &str); 5] = [
("socks4", "HTTPS_PROXY"),
("socks4a", "all_proxy"),
("socks5", "ALL_PROXY"),
("socks5h", "http_proxy"),
("socks", "HTTP_PROXY"),
];
fn failing_with(variable: &str) -> String {
format!("io: Pitboard does not use SOCKS proxies yet, and {variable} names one")
}
fn untouched() -> (TcpListener, u16) {
let listener = TcpListener::bind("127.0.0.1:0").expect("a port on loopback");
listener.set_nonblocking(true).expect("never waits");
let port = listener.local_addr().expect("its address").port();
(listener, port)
}
fn reached(listener: &TcpListener) -> bool {
listener.accept().is_ok()
}
fn host_answering(answer: String) -> (u16, Arc<Mutex<usize>>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("a port on loopback");
let port = listener.local_addr().expect("its address").port();
let asked = Arc::new(Mutex::new(0));
let counted = Arc::clone(&asked);
std::thread::spawn(move || {
use std::io::{BufRead, BufReader, Write};
for mut client in listener.incoming().flatten() {
let mut head = BufReader::new(client.try_clone().expect("a second handle"));
let mut line = String::new();
while head.read_line(&mut line).is_ok_and(|n| n > 0) && line != "\r\n" {
line.clear();
}
*counted.lock().expect("not poisoned") += 1;
let _ = client.write_all(answer.as_bytes());
}
});
(port, asked)
}
const NO_CONTENT: &str =
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
fn got(agent: &ureq::Agent, url: &str) -> Result<u16, String> {
agent
.get(url)
.call()
.map(|response| response.status().as_u16())
.map_err(|e| e.to_string())
}
#[test]
fn every_socks_proxy_is_refused_before_anything_is_sent() {
let (listener, port) = untouched();
for (scheme, variable) in SOCKS {
let proxy = format!("{scheme}://127.0.0.1:{port}");
let api_base = format!("http://127.0.0.1:{port}");
let ctx = read_from(&[
(variable, proxy.as_str()),
("PITBOARD_API_BASE", api_base.as_str()),
]);
assert_eq!(
got(crate::api::agent(&ctx), &format!("{api_base}/")),
Err(failing_with(variable)),
"{scheme}"
);
let asked = crate::api::owner(&ctx, "a-token").expect_err("refused");
assert_eq!(
(asked.to_string(), crate::error::Cause::of(&asked).code()),
(
format!("could not reach Anthropic: {}", failing_with(variable)),
"unreachable"
),
"{scheme}"
);
assert!(!reached(&listener), "{scheme}: something was sent");
}
}
#[derive(Debug, Default)]
struct Recording(Arc<Mutex<Vec<String>>>);
const UNKNOWN_PROXY: &str = "socks.pitboard-test.invalid";
impl ureq::unversioned::resolver::Resolver for Recording {
#[allow(
clippy::disallowed_types,
reason = "ureq hands a resolver the agent's configuration, made in api::made"
)]
fn resolve(
&self,
uri: &Uri,
config: &ureq::config::Config,
timeout: ureq::unversioned::transport::NextTimeout,
) -> Result<ureq::unversioned::resolver::ResolvedSocketAddrs, ureq::Error> {
let host = uri.host().unwrap_or_default().to_string();
self.0.lock().expect("not poisoned").push(host.clone());
if host == UNKNOWN_PROXY {
return Err(ureq::Error::HostNotFound);
}
ureq::unversioned::resolver::DefaultResolver::default().resolve(uri, config, timeout)
}
}
#[test]
fn an_exempt_host_is_reached_without_looking_the_socks_proxy_up() {
let (port, asked) = host_answering(NO_CONTENT.to_string());
let url = format!("http://127.0.0.1:{port}/");
for (scheme, variable) in SOCKS {
let proxy = format!("{scheme}://{UNKNOWN_PROXY}:1080");
let looked_up = Recording::default();
let lookups = Arc::clone(&looked_up.0);
let exempt = read_from(&[(variable, proxy.as_str()), ("NO_PROXY", "127.0.0.1")]);
let agent = crate::api::made(&exempt, looked_up);
assert_eq!(got(&agent, &url), Ok(204), "{scheme}");
assert_eq!(
*lookups.lock().expect("not poisoned"),
["127.0.0.1"],
"{scheme}: only the host was looked up"
);
let looked_up = Recording::default();
let lookups = Arc::clone(&looked_up.0);
let not_exempt = read_from(&[(variable, proxy.as_str())]);
let agent = crate::api::made(¬_exempt, looked_up);
assert_eq!(got(&agent, &url), Err(failing_with(variable)), "{scheme}");
assert!(
lookups.lock().expect("not poisoned").is_empty(),
"{scheme}: something was looked up"
);
}
assert_eq!(
*asked.lock().expect("not poisoned"),
SOCKS.len(),
"the host was asked once for each exempt request, and for nothing else"
);
}
#[test]
fn a_redirect_is_not_followed_past_a_socks_proxy() {
let (elsewhere, port) = untouched();
let (proxy, proxy_port) = untouched();
let (exempt_port, asked) = host_answering(format!(
"HTTP/1.1 302 Found\r\nLocation: http://localhost:{port}/\r\n\
Content-Length: 0\r\nConnection: close\r\n\r\n"
));
for (scheme, variable) in SOCKS {
let address = format!("{scheme}://127.0.0.1:{proxy_port}");
let ctx = read_from(&[(variable, address.as_str()), ("NO_PROXY", "127.0.0.1")]);
assert_eq!(
got(
crate::api::agent(&ctx),
&format!("http://127.0.0.1:{exempt_port}/")
),
Ok(302),
"{scheme}"
);
assert!(!reached(&elsewhere), "{scheme}: the redirect was followed");
assert!(!reached(&proxy), "{scheme}: the proxy was reached");
}
assert_eq!(*asked.lock().expect("not poisoned"), SOCKS.len());
}
}