use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::error::Result;
use crate::net::connector::ProxySpec;
use crate::net::stream::NetStream;
use crate::net::{Connector, DeadlineStream, DirectConnector};
use crate::url::Url;
pub(crate) struct NetConfig {
pub(crate) connector: Arc<dyn Connector>,
pub(crate) connect_timeout: Option<Duration>,
pub(crate) tls: crate::tls::TlsSettings,
pub(crate) read_timeout: Option<Duration>,
pub(crate) deadline: Option<Instant>,
pub(crate) ftp_use_epsv: bool,
pub(crate) ftp_create_dirs: bool,
pub(crate) ftp_active: bool,
pub(crate) require_tls: bool,
}
impl Default for NetConfig {
fn default() -> Self {
NetConfig {
connector: Arc::new(DirectConnector),
connect_timeout: Some(Duration::from_secs(30)),
tls: crate::tls::TlsSettings::default(),
read_timeout: Some(DEFAULT_READ_TIMEOUT),
deadline: None,
ftp_use_epsv: true,
ftp_create_dirs: false,
ftp_active: false,
require_tls: false,
}
}
}
pub(crate) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
impl NetConfig {
pub(crate) fn connect(&self, host: &str, port: u16) -> Result<Box<dyn NetStream>> {
let connect_timeout = match self.deadline {
None => self.connect_timeout,
Some(_) => self.op_timeout(self.connect_timeout)?,
};
let stream = self.connector.connect(host, port, connect_timeout)?;
Ok(DeadlineStream::wrap(stream, self.deadline))
}
pub(crate) fn op_timeout(&self, idle: Option<Duration>) -> std::io::Result<Option<Duration>> {
super::deadline::op_timeout(self.deadline, idle)
}
pub(crate) fn tls_connect<S: std::io::Read + std::io::Write>(
&self,
transport: S,
host: &str,
) -> Result<crate::tls::TlsStream<S>> {
self.tls.connect(transport, host)
}
pub(crate) fn io_timeout(&self) -> Option<Duration> {
self.read_timeout
}
}
#[derive(Clone)]
pub struct Client {
connector: Arc<dyn Connector>,
proxy: Option<ProxySpec>,
proxy_tls: crate::tls::TlsSettings,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
max_time: Option<Duration>,
tls: crate::tls::TlsSettings,
idn: bool,
no_proxy: Vec<String>,
ftp_use_epsv: bool,
ftp_create_dirs: bool,
ftp_active: bool,
require_tls: bool,
decompress: bool,
}
impl Default for Client {
fn default() -> Self {
Client {
connector: Arc::new(DirectConnector),
proxy: None,
proxy_tls: crate::tls::TlsSettings::default(),
connect_timeout: Some(Duration::from_secs(30)),
read_timeout: Some(DEFAULT_READ_TIMEOUT),
max_time: None,
tls: crate::tls::TlsSettings::default(),
idn: true,
no_proxy: Vec::new(),
ftp_use_epsv: true,
ftp_create_dirs: false,
ftp_active: false,
require_tls: false,
decompress: true,
}
}
}
impl Client {
pub fn new() -> Self {
Self::default()
}
pub fn proxy(mut self, spec: &str) -> Result<Self> {
self.proxy = Some(ProxySpec::parse(spec)?);
self.connector = Arc::new(DirectConnector);
Ok(self)
}
pub fn connector(mut self, connector: Arc<dyn Connector>) -> Self {
self.connector = connector;
self.proxy = None;
self
}
crate::tls::proxy_tls_builder_methods!();
pub fn connect_timeout(mut self, d: Option<Duration>) -> Self {
self.connect_timeout = d;
self
}
pub fn read_timeout(mut self, d: Option<Duration>) -> Self {
self.read_timeout = d;
self
}
pub fn max_time(mut self, d: Duration) -> Self {
self.max_time = Some(d);
self
}
pub fn verify_tls(mut self, on: bool) -> Self {
self.tls.verify = on;
self
}
pub fn ca_bundle(mut self, path: &str) -> Self {
self.tls.ca_bundle = Some(path.to_string());
self
}
pub fn ca_path(mut self, dir: &str) -> Self {
self.tls.ca_path = Some(dir.to_string());
self
}
pub fn crl_file(mut self, path: &str) -> Self {
self.tls.crl_file = Some(path.to_string());
self
}
pub fn client_cert(mut self, path: &str) -> Self {
self.tls.client_cert = Some(path.to_string());
self
}
pub fn client_key(mut self, path: &str) -> Self {
self.tls.client_key = Some(path.to_string());
self
}
pub fn client_key_pass(mut self, pass: &str) -> Self {
self.tls.client_key_pass = Some(pass.to_string());
self
}
pub fn cert_type_der(mut self, der: bool) -> Self {
self.tls.cert_is_der = der;
self
}
pub fn key_type_der(mut self, der: bool) -> Self {
self.tls.key_is_der = der;
self
}
pub fn pinned_pubkey(mut self, spec: &str) -> Self {
self.tls.pinned_pubkey = Some(spec.to_string());
self
}
pub fn ciphers(mut self, list: &str) -> Self {
self.tls.ciphers = Some(list.to_string());
self
}
pub fn tls13_ciphers(mut self, list: &str) -> Self {
self.tls.tls13_ciphers = Some(list.to_string());
self
}
pub fn tls_min_version(mut self, v: crate::tls::ProtocolVersion) -> Self {
self.tls.min_version = Some(v);
self
}
pub fn tls_max_version(mut self, v: crate::tls::ProtocolVersion) -> Self {
self.tls.max_version = Some(v);
self
}
pub fn idn(mut self, on: bool) -> Self {
self.idn = on;
self
}
pub fn decompress(mut self, on: bool) -> Self {
self.decompress = on;
self
}
pub fn ftp_use_epsv(mut self, on: bool) -> Self {
self.ftp_use_epsv = on;
self
}
pub fn ftp_create_dirs(mut self, on: bool) -> Self {
self.ftp_create_dirs = on;
self
}
pub fn ftp_active(mut self, on: bool) -> Self {
self.ftp_active = on;
self
}
pub fn require_tls(mut self, on: bool) -> Self {
self.require_tls = on;
self
}
pub fn no_proxy<I, S>(mut self, entries: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.no_proxy = entries.into_iter().map(Into::into).collect();
self
}
fn host_bypassed(&self, host: &str) -> bool {
crate::net::no_proxy_matches(self.no_proxy.iter().map(String::as_str), host)
}
fn effective_connector(&self, host: &str) -> Arc<dyn Connector> {
if self.host_bypassed(host) {
Arc::new(DirectConnector)
} else if let Some(p) = &self.proxy {
p.connector(&self.proxy_tls)
} else {
self.connector.clone()
}
}
fn net_config_for(&self, host: &str) -> NetConfig {
NetConfig {
connector: self.effective_connector(host),
connect_timeout: self.connect_timeout,
tls: self.tls.clone(),
read_timeout: self.read_timeout,
deadline: self.max_time.map(|d| Instant::now() + d),
ftp_use_epsv: self.ftp_use_epsv,
ftp_create_dirs: self.ftp_create_dirs,
ftp_active: self.ftp_active,
require_tls: self.require_tls,
}
}
pub fn request(&self, method: &str, url: &str) -> Result<crate::Request> {
let mut r = crate::Request::new(method, url)?
.with_tls_settings(&self.tls)
.idn(self.idn)
.decompress(self.decompress);
r.proxy_tls = self.proxy_tls.clone();
r = match &self.proxy {
Some(p) => {
r.set_proxy_spec(p.clone());
r.no_proxy(self.no_proxy.clone())
}
None => {
let host = r.url().host.clone();
r.connector(self.effective_connector(&host))
}
};
r = r.read_timeout(self.read_timeout);
if let Some(d) = self.max_time {
r = r.max_time(d);
}
if let Some(t) = self.connect_timeout {
r = r.connect_timeout(t);
}
Ok(r)
}
pub fn get(&self, url: &str) -> Result<crate::Response> {
self.request("GET", url)?.send()
}
pub fn websocket(&self, url: &str) -> Result<crate::websocket::WebSocket> {
let mut url = Url::parse(url)?;
url.set_idn(self.idn)?;
self.websocket_url(&url)
}
pub fn websocket_url(&self, url: &Url) -> Result<crate::websocket::WebSocket> {
let cfg = self.net_config_for(&url.host);
crate::websocket::WebSocket::open(url, &cfg, self.read_timeout, &[])
}
pub fn websocket_with_subprotocols(
&self,
url: &str,
subprotocols: &[&str],
) -> Result<crate::websocket::WebSocket> {
let mut url = Url::parse(url)?;
url.set_idn(self.idn)?;
let cfg = self.net_config_for(&url.host);
let protos: Vec<String> = subprotocols.iter().map(|s| s.to_string()).collect();
crate::websocket::WebSocket::open(&url, &cfg, self.read_timeout, &protos)
}
pub fn transfer(&self, url_str: &str) -> Result<Vec<u8>> {
let mut url = Url::parse(url_str)?;
url.set_idn(self.idn)?;
self.transfer_url(&url)
}
pub fn transfer_url(&self, url: &Url) -> Result<Vec<u8>> {
crate::transfer::transfer_url_with(url, &self.net_config_for(&url.host))
}
pub fn transfer_url_to(&self, url: &Url, sink: &mut dyn std::io::Write) -> Result<u64> {
crate::transfer::transfer_url_to_with(url, &self.net_config_for(&url.host), sink)
}
pub fn ftp_store(&self, url: &Url, body: &[u8], resume_at: Option<u64>) -> Result<()> {
crate::ftp::store_with(url, body, resume_at, &self.net_config_for(&url.host))
}
pub fn ftp_append(&self, url: &Url, body: &[u8]) -> Result<()> {
crate::ftp::append_with(url, body, &self.net_config_for(&url.host))
}
pub fn tftp_store(&self, url: &Url, body: &[u8]) -> Result<()> {
crate::tftp::store_with(url, body, &self.net_config_for(&url.host))
}
pub fn smtp_send(
&self,
url: &Url,
body: &[u8],
from: &str,
rcpts: &[String],
user: Option<&str>,
pass: Option<&str>,
) -> Result<()> {
let opts = crate::smtp::SmtpOptions {
from,
rcpts,
user,
pass,
};
crate::smtp::send(url, body, &opts, &self.net_config_for(&url.host))
}
pub fn mqtt_publish(&self, url: &Url, payload: &[u8], qos: u8) -> Result<()> {
crate::mqtt::publish_with(url, payload, qos, &self.net_config_for(&url.host))
}
pub fn rtsp(&self, url: &Url, method: &str) -> Result<Vec<u8>> {
crate::rtsp::run_method_with(url, method, &self.net_config_for(&url.host))
}
pub fn telnet(&self, url: &Url, input: &[u8]) -> Result<Vec<u8>> {
crate::telnet::run(url, input, &self.net_config_for(&url.host))
}
}
#[cfg(test)]
mod tests {
use super::{Client, NetConfig, DEFAULT_READ_TIMEOUT};
use std::time::Duration;
#[test]
fn client_is_send_sync_and_clone() {
fn assert_send_sync<T: Send + Sync + Clone>() {}
assert_send_sync::<Client>();
}
#[test]
fn no_proxy_matches_leading_dot_apex_suffix_and_wildcard() {
let c = Client::new().no_proxy([".example.com"]);
assert!(c.host_bypassed("foo.example.com"));
assert!(c.host_bypassed("example.com"));
assert!(c.host_bypassed("EXAMPLE.COM")); assert!(!c.host_bypassed("notexample.com"));
assert!(!c.host_bypassed("example.com.evil.com"));
let c = Client::new().no_proxy(["example.com"]);
assert!(c.host_bypassed("foo.example.com"));
assert!(c.host_bypassed("example.com"));
assert!(Client::new().no_proxy(["*"]).host_bypassed("anything.test"));
let c = Client::new().no_proxy(["2.3.4", "10.0.0.0/8", "::1"]);
assert!(!c.host_bypassed("1.2.3.4"));
assert!(c.host_bypassed("10.9.8.7"));
assert!(c.host_bypassed("[::1]"));
}
#[test]
fn http_proxy_bypass_is_decided_per_hop() {
let c = Client::new()
.proxy("http://proxy.example:3128")
.unwrap()
.no_proxy(["internal.example"]);
let r = c.request("GET", "http://internal.example/").unwrap();
assert!(r.connector.is_direct());
let p = r.proxy.as_ref().expect("proxy kept on the request");
assert_eq!((p.host.as_str(), p.port), ("proxy.example", 3128));
assert!(crate::http::proxy_bypassed(&r));
let mut hop = r.clone();
hop.url = crate::url::Url::parse("http://external.example/").unwrap();
assert!(!crate::http::proxy_bypassed(&hop));
let c = c.connector(std::sync::Arc::new(crate::net::DirectConnector));
assert!(c.request("GET", "http://x/").unwrap().proxy.is_none());
}
#[test]
fn socks_and_https_proxy_bypass_is_decided_per_hop() {
for spec in ["socks5h://proxy.example:1080", "https://proxy.example:443"] {
let c = Client::new()
.proxy(spec)
.unwrap()
.no_proxy(["internal.example"])
.verify_tls(false)
.proxy_ca_bundle("/proxy-ca.pem");
let mut r = c.request("GET", "http://internal.example/").unwrap();
assert!(r.proxy.is_none(), "{spec}");
assert!(r.proxy_spec.is_some(), "{spec}");
assert!(
!r.verify_tls && r.proxy_tls.verify,
"{spec}: -k is origin-only"
);
assert_eq!(r.proxy_tls.ca_bundle.as_deref(), Some("/proxy-ca.pem"));
r.select_hop_connector();
assert!(
r.connector.is_direct(),
"{spec}: no-proxy host dials direct"
);
r.url = crate::url::Url::parse("http://external.example/").unwrap();
r.select_hop_connector();
assert!(!r.connector.is_direct(), "{spec}: external host is proxied");
}
let c = Client::new()
.proxy("socks5://p:1080")
.unwrap()
.no_proxy(["internal.example"]);
assert!(c.net_config_for("internal.example").connector.is_direct());
assert!(!c.net_config_for("external.example").connector.is_direct());
}
#[cfg(feature = "rustls-tls")]
fn serve_gophers_once() -> u16 {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
let Ok((mut sock, _)) = listener.accept() else {
return;
};
let cfg = crate::proto::tls::rustls_tests::server_config();
let mut server = rustls::ServerConnection::new(cfg).unwrap();
{
let mut tls = rustls::Stream::new(&mut server, &mut sock);
let mut byte = [0u8; 1];
let mut line = Vec::new();
while tls.read(&mut byte).map(|n| n == 1).unwrap_or(false) {
line.push(byte[0]);
if line.ends_with(b"\r\n") {
break;
}
}
let _ = tls.write_all(b"hello");
tls.conn.send_close_notify();
let _ = tls.flush();
}
crate::test_support::graceful_close(&mut sock);
});
port
}
#[cfg(feature = "rustls-tls")]
#[test]
fn non_http_tls_honours_client_tls_settings() {
let url = |port: u16| format!("gophers://localhost:{port}/");
let port = serve_gophers_once();
assert!(
Client::new().transfer(&url(port)).is_err(),
"untrusted test CA must be rejected by default"
);
let port = serve_gophers_once();
let body = Client::new()
.verify_tls(false)
.transfer(&url(port))
.unwrap();
assert_eq!(body, b"hello");
let dir = std::env::temp_dir().join(format!("rsurl-ca-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let ca = dir.join("ca.pem");
std::fs::write(&ca, crate::proto::tls::rustls_tests::CA_CERT_PEM).unwrap();
let port = serve_gophers_once();
let body = Client::new()
.ca_bundle(ca.to_str().unwrap())
.transfer(&url(port))
.unwrap();
assert_eq!(body, b"hello");
let port = serve_gophers_once();
let wrong_pin = "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
assert!(
Client::new()
.verify_tls(false)
.pinned_pubkey(wrong_pin)
.transfer(&url(port))
.is_err(),
"a pin mismatch must fail even with -k"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn net_config_carries_tls_settings_and_read_timeout() {
let c = Client::new()
.verify_tls(false)
.ca_bundle("/ca.pem")
.pinned_pubkey("sha256//x")
.read_timeout(Some(Duration::from_secs(7)));
let cfg = c.net_config_for("example.com");
assert!(!cfg.tls.verify);
assert_eq!(cfg.tls.ca_bundle.as_deref(), Some("/ca.pem"));
assert_eq!(cfg.tls.pinned_pubkey.as_deref(), Some("sha256//x"));
assert_eq!(cfg.io_timeout(), Some(Duration::from_secs(7)));
assert_eq!(
NetConfig::default().io_timeout(),
Some(DEFAULT_READ_TIMEOUT)
);
}
}