Skip to content
3 changes: 3 additions & 0 deletions src/server/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ use uuid::Uuid;
///
/// This enum is non-exhaustive, as users should only be constructing required
/// variants, not matching on it.
///
/// Proxy configuration is read from environment variables (HTTP_PROXY, HTTPS_PROXY,
/// and lowercase variants) and applied automatically during client creation.
#[non_exhaustive]
pub enum ServerConfig {
/// A local task database, for situations with a single replica.
Expand Down
177 changes: 175 additions & 2 deletions src/server/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! This contains some utilities to make using `reqwest` easier, including getting
//! the correct TLS certificate store.

use crate::errors::Result;

use crate::errors::{Error, Result};
#[cfg(not(target_arch = "wasm32"))]
use std::env;
#[cfg(all(
not(target_arch = "wasm32"),
not(any(feature = "tls-native-roots", feature = "tls-webpki-roots"))
Expand All @@ -19,12 +20,15 @@ static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_V
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn client() -> Result<reqwest::Client> {
use std::time::Duration;

let client = reqwest::Client::builder()
.use_rustls_tls()
.user_agent(USER_AGENT)
.connect_timeout(Duration::from_secs(10))
.read_timeout(Duration::from_secs(60));

// configure client proxy
let client = configure_proxy(client)?;
// Select native or webpki certs depending on features
let client = client.tls_built_in_root_certs(false);
#[cfg(feature = "tls-native-roots")]
Expand All @@ -34,6 +38,37 @@ pub(super) fn client() -> Result<reqwest::Client> {

Ok(client.build()?)
}
#[cfg(not(target_arch = "wasm32"))]
fn configure_proxy(mut client: reqwest::ClientBuilder) -> Result<reqwest::ClientBuilder> {
// Configure HTTP proxy if set
if let Ok(proxy_url) = env::var("HTTP_PROXY").or_else(|_| env::var("http_proxy")) {
match reqwest::Proxy::http(&proxy_url) {
Ok(proxy) => {
client = client.proxy(proxy);
}
Err(e) => {
return Err(Error::Server(format!(
"Invalid HTTP_PROXY '{proxy_url}': {e}"
)));
}
}
}

// Configure HTTPS proxy if set
if let Ok(proxy_url) = env::var("HTTPS_PROXY").or_else(|_| env::var("https_proxy")) {
match reqwest::Proxy::https(&proxy_url) {
Ok(proxy) => {
client = client.proxy(proxy);
}
Err(e) => {
return Err(Error::Server(format!(
"Invalid HTTPS_PROXY '{proxy_url}': {e}"
)));
}
}
}
Ok(client)
}

/// Create a new [`reqwest::Client`] with configuration appropriate to this library.
///
Expand All @@ -46,3 +81,141 @@ pub(super) fn client() -> Result<reqwest::Client> {

Ok(client.build()?)
}

#[cfg(test)]
mod tests {
use super::client;

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_client_proxy_configurations() {
// Helper function to test a scenario and cleanup
let test_scenario = |setup: fn(), description: &str, should_succeed: bool| {
setup();
let client = client();
if should_succeed {
assert!(client.is_ok(), "{}", description);
} else {
assert!(client.is_err(), "{}", description);
}
// Cleanup all possible env vars
std::env::remove_var("HTTP_PROXY");
std::env::remove_var("http_proxy");
std::env::remove_var("HTTPS_PROXY");
std::env::remove_var("https_proxy");
};

// Test 1: No proxy
test_scenario(
|| {
std::env::remove_var("HTTP_PROXY");
std::env::remove_var("http_proxy");
std::env::remove_var("HTTPS_PROXY");
std::env::remove_var("https_proxy");
},
"Client should build without proxy settings",
true,
);

// Test 2: HTTP proxy
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "http://proxy.example.com:8080");
},
"Client should build with HTTP_PROXY set",
true,
);

// Test 3: HTTPS proxy
test_scenario(
|| {
std::env::set_var("HTTPS_PROXY", "http://proxy.example.com:8443");
},
"Client should build with HTTPS_PROXY set",
true,
);

// Test 4: Both proxies
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "http://http-proxy.example.com:8080");
std::env::set_var("HTTPS_PROXY", "http://https-proxy.example.com:8443");
},
"Client should build with both proxies set",
true,
);

// Test 5: Lowercase proxy vars
test_scenario(
|| {
std::env::set_var("http_proxy", "http://proxy.example.com:8080");
std::env::set_var("https_proxy", "http://proxy.example.com:8443");
},
"Client should build with lowercase proxy vars",
true,
);

// Test 6: Proxy with authentication
test_scenario(
|| {
std::env::set_var("HTTPS_PROXY", "http://user:password@proxy.example.com:8443");
},
"Client should build with authenticated proxy",
true,
);

// Test 7: Invalid proxy URL
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "http://[invalid-bracket-usage");
},
"Client should fail with invalid proxy URL",
false,
);

// Test 8: Uppercase takes precedence
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "http://uppercase.example.com:8080");
std::env::set_var("http_proxy", "http://lowercase.example.com:8080");
},
"Client should build with precedence rules",
true,
);

// Test 9: Mixed case proxies
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "http://http-upper.example.com:8080");
std::env::set_var("https_proxy", "http://https-lower.example.com:8443");
},
"Client should build with mixed case proxy vars",
true,
);

// Test 10: Empty proxy value
test_scenario(
|| {
std::env::set_var("HTTP_PROXY", "");
},
"Client should fail with empty proxy value",
false,
);

// Test 11: SOCKS proxy
test_scenario(
|| {
std::env::set_var("HTTPS_PROXY", "socks5://proxy.example.com:1080");
},
"Client should build with SOCKS proxy",
true,
);
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test::wasm_bindgen_test]
fn test_client_wasm() {
let client = client();
assert!(client.is_ok(), "WASM client should build");
}
}
Loading