use deadpool::managed;
use std::collections::VecDeque;
use std::io;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio::sync::Notify;
use tokio::sync::RwLock;
use crate::connection_error::ConnectionError;
use crate::protocol::{RequestContext, authinfo_pass, authinfo_user};
use crate::stream::ConnectionStream;
use crate::tls::{TlsConfig, TlsManager};
pub type Pool = managed::Pool<TcpManager>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompressionSupport {
Supported,
Unsupported,
}
#[derive(Debug, Default)]
enum CompressionSupportState {
#[default]
Unknown,
Probing(Arc<Notify>),
Supported,
Unsupported,
}
#[derive(Debug, Clone)]
pub struct TcpManagerOptions {
pub username: Option<String>,
pub password: Option<String>,
pub tls_config: Option<TlsConfig>,
pub recv_buffer_size: usize,
pub send_buffer_size: usize,
pub compress: Option<bool>,
pub compress_level: Option<u32>,
pub send_mode_reader: bool,
}
impl Default for TcpManagerOptions {
fn default() -> Self {
Self {
username: None,
password: None,
tls_config: None,
recv_buffer_size: crate::constants::socket::HIGH_THROUGHPUT_RECV_BUFFER,
send_buffer_size: crate::constants::socket::HIGH_THROUGHPUT_SEND_BUFFER,
compress: None,
compress_level: None,
send_mode_reader: true,
}
}
}
#[derive(Debug, Clone)]
pub struct TcpManager {
pub(crate) host: String,
pub(crate) port: u16,
pub(crate) name: String,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) tls_config: TlsConfig,
pub(crate) tls_manager: Option<Arc<TlsManager>>,
resolved_socket_addrs: Arc<RwLock<Option<Arc<[SocketAddr]>>>>,
next_resolved_socket_addr: Arc<AtomicUsize>,
pub(crate) recv_buffer_size: usize,
pub(crate) send_buffer_size: usize,
pub(crate) compress: Option<bool>,
pub(crate) compress_level: Option<u32>,
compression_support: Arc<Mutex<CompressionSupportState>>,
pub(crate) send_mode_reader: bool,
}
impl TcpManager {
fn socket_buffer_size_u32(size: usize, label: &str) -> Result<u32, ConnectionError> {
u32::try_from(size).map_err(|_| {
ConnectionError::IoError(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{label} socket buffer size {size} exceeds u32::MAX"),
))
})
}
fn ip_literal_socket_addr(&self) -> Option<SocketAddr> {
self.host
.parse::<IpAddr>()
.ok()
.map(|ip| SocketAddr::new(ip, self.port))
}
async fn resolve_socket_addrs(&self) -> Result<Arc<[SocketAddr]>, ConnectionError> {
if let Some(socket_addr) = self.ip_literal_socket_addr() {
return Ok(Arc::from([socket_addr]));
}
if let Some(addrs) = self.resolved_socket_addrs.read().await.as_ref() {
if addrs.is_empty() {
return Err(ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
});
}
return Ok(addrs.clone());
}
let mut cached_addrs = self.resolved_socket_addrs.write().await;
if let Some(addrs) = cached_addrs.as_ref() {
if addrs.is_empty() {
return Err(ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
});
}
return Ok(addrs.clone());
}
let addrs = tokio::net::lookup_host((self.host.as_str(), self.port))
.await?
.collect::<Vec<_>>();
if addrs.is_empty() {
return Err(ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
});
}
let addrs: Arc<[SocketAddr]> = Arc::from(addrs);
*cached_addrs = Some(addrs.clone());
Ok(addrs)
}
async fn refresh_socket_addrs(&self) -> Result<Arc<[SocketAddr]>, ConnectionError> {
if let Some(socket_addr) = self.ip_literal_socket_addr() {
return Ok(Arc::from([socket_addr]));
}
let addrs = tokio::net::lookup_host((self.host.as_str(), self.port))
.await?
.collect::<Vec<_>>();
if addrs.is_empty() {
return Err(ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
});
}
let addrs: Arc<[SocketAddr]> = Arc::from(addrs);
*self.resolved_socket_addrs.write().await = Some(addrs.clone());
Ok(addrs)
}
fn is_ipv6_network_unreachable(socket_addr: SocketAddr, error: &ConnectionError) -> bool {
socket_addr.is_ipv6()
&& matches!(
error,
ConnectionError::IoError(error)
if matches!(
error.kind(),
io::ErrorKind::NetworkUnreachable | io::ErrorKind::HostUnreachable
)
)
}
async fn remove_cached_ipv6_socket_addrs(&self) {
let mut cached_addrs = self.resolved_socket_addrs.write().await;
let Some(addrs) = cached_addrs.as_ref() else {
return;
};
let ipv4_addrs = addrs
.iter()
.copied()
.filter(SocketAddr::is_ipv4)
.collect::<Vec<_>>();
if ipv4_addrs.len() == addrs.len() {
return;
}
*cached_addrs = if ipv4_addrs.is_empty() {
None
} else {
Some(Arc::<[SocketAddr]>::from(ipv4_addrs))
};
}
pub fn new(
host: String,
port: u16,
name: String,
options: TcpManagerOptions,
) -> Result<Self, ConnectionError> {
let (tls_config, tls_manager) = match options.tls_config {
Some(cfg) if cfg.use_tls => {
let mgr = Arc::new(TlsManager::new(cfg.clone()).map_err(|e| {
ConnectionError::TlsHandshake {
backend: name.clone(),
source: e.into(),
}
})?);
(cfg, Some(mgr))
}
Some(cfg) => (cfg, None),
None => (TlsConfig::default(), None),
};
Ok(Self {
host,
port,
name,
username: options.username,
password: options.password,
tls_config,
tls_manager,
resolved_socket_addrs: Arc::new(RwLock::new(None)),
next_resolved_socket_addr: Arc::new(AtomicUsize::new(0)),
recv_buffer_size: options.recv_buffer_size,
send_buffer_size: options.send_buffer_size,
compress: options.compress,
compress_level: options.compress_level,
compression_support: Arc::new(Mutex::new(CompressionSupportState::Unknown)),
send_mode_reader: options.send_mode_reader,
})
}
async fn connect_socket_addr(
&self,
socket_addr: SocketAddr,
) -> Result<TcpStream, ConnectionError> {
let socket = if socket_addr.is_ipv4() {
tokio::net::TcpSocket::new_v4()?
} else {
tokio::net::TcpSocket::new_v6()?
};
if self.recv_buffer_size > 0 {
socket.set_recv_buffer_size(Self::socket_buffer_size_u32(
self.recv_buffer_size,
"receive",
)?)?;
}
if self.send_buffer_size > 0 {
socket.set_send_buffer_size(Self::socket_buffer_size_u32(
self.send_buffer_size,
"send",
)?)?;
}
socket.set_reuseaddr(true)?;
let tcp_stream = socket.connect(socket_addr).await?;
let sock_ref = socket2::SockRef::from(&tcp_stream);
sock_ref.set_keepalive(true)?;
let keepalive = socket2::TcpKeepalive::new()
.with_time(crate::constants::duration_polyfill::from_minutes(1))
.with_interval(std::time::Duration::from_secs(10));
sock_ref.set_tcp_keepalive(&keepalive)?;
sock_ref.set_tcp_nodelay(true)?;
Ok(tcp_stream)
}
async fn create_connected_tcp_stream(&self) -> Result<TcpStream, ConnectionError> {
let addrs = self.resolve_socket_addrs().await?;
let last_error = match self.try_resolved_socket_addrs(&addrs).await {
Ok(tcp_stream) => return Ok(tcp_stream),
Err(last_error) => last_error,
};
if self.ip_literal_socket_addr().is_some() {
return Err(
last_error.unwrap_or_else(|| ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
}),
);
}
tracing::debug!(
backend = %self.name,
host = %self.host,
"All cached backend socket addresses failed; refreshing DNS before final connect pass"
);
let addrs = self.refresh_socket_addrs().await?;
self.try_resolved_socket_addrs(&addrs)
.await
.map_err(|last_error| {
last_error.unwrap_or_else(|| ConnectionError::DnsNoAddresses {
address: format!("{}:{}", self.host, self.port),
})
})
}
async fn try_resolved_socket_addrs(
&self,
addrs: &[SocketAddr],
) -> Result<TcpStream, Option<ConnectionError>> {
let start = self
.next_resolved_socket_addr
.fetch_add(1, Ordering::Relaxed)
% addrs.len();
let mut last_error = None;
let mut remaining_addrs = (0..addrs.len())
.map(|offset| addrs[(start + offset) % addrs.len()])
.collect::<VecDeque<_>>();
while let Some(socket_addr) = remaining_addrs.pop_front() {
match self.connect_socket_addr(socket_addr).await {
Ok(tcp_stream) => return Ok(tcp_stream),
Err(error) => {
if Self::is_ipv6_network_unreachable(socket_addr, &error) {
self.remove_cached_ipv6_socket_addrs().await;
remaining_addrs.retain(SocketAddr::is_ipv4);
}
tracing::debug!(
backend = %self.name,
host = %self.host,
socket_addr = %socket_addr,
error = %error,
"Backend socket address connect failed; trying next resolved address"
);
last_error = Some(error);
}
}
}
Err(last_error)
}
pub(crate) async fn create_optimized_stream(
&self,
) -> Result<ConnectionStream, ConnectionError> {
let tcp_stream = self.create_connected_tcp_stream().await?;
if self.tls_config.use_tls {
let Some(tls_manager) = self.tls_manager.as_ref() else {
return Err(ConnectionError::TlsHandshake {
backend: self.name.clone(),
source: "TLS enabled but TLS manager not initialized".into(),
});
};
let tls_stream = tls_manager
.handshake(tcp_stream, &self.host, &self.name)
.await
.map_err(|e| ConnectionError::TlsHandshake {
backend: self.name.clone(),
source: e.into(),
})?;
Ok(ConnectionStream::tls(tls_stream))
} else {
Ok(ConnectionStream::plain(tcp_stream))
}
}
}
impl TcpManager {
async fn read_backend_setup_reply(
stream: &mut ConnectionStream,
request: &RequestContext,
buffer: &mut [u8],
) -> Result<String, ConnectionError> {
match crate::session::backend::read_single_line_reply(stream, request, buffer).await {
Ok(reply) => Ok(reply),
Err(
crate::session::backend::SingleLineReplyReadError::Full { bytes_read }
| crate::session::backend::SingleLineReplyReadError::Invalid { bytes_read },
) => Err(ConnectionError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"invalid or truncated backend setup reply: {}",
String::from_utf8_lossy(&buffer[..bytes_read]).trim_end()
),
))),
Err(crate::session::backend::SingleLineReplyReadError::Io(err)) => Err(err.into()),
Err(crate::session::backend::SingleLineReplyReadError::Closed) => {
Err(ConnectionError::IoError(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"backend closed while reading setup reply",
)))
}
}
}
async fn consume_greeting(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<(), ConnectionError> {
let request = RequestContext::from_verb_args(b"MODE", b"READER");
let greeting = Self::read_backend_setup_reply(stream, &request, buffer).await?;
if !crate::protocol::StatusCode::parse(greeting.as_bytes())
.is_some_and(|code| code.is_greeting())
{
return Err(ConnectionError::InvalidGreeting {
backend: self.name.clone(),
greeting: greeting.trim().to_string(),
});
}
Ok(())
}
async fn negotiate_mode_reader(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<(), ConnectionError> {
stream.write_all(b"MODE READER\r\n").await?;
stream.flush().await?;
let request = RequestContext::from_verb_args(b"MODE", b"READER");
let response = Self::read_backend_setup_reply(stream, &request, buffer).await?;
if crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|code| matches!(code.as_u16(), 200 | 201))
{
tracing::debug!(
backend = %self.name,
response = %response.trim(),
"MODE READER accepted"
);
return Ok(());
}
Err(ConnectionError::InvalidGreeting {
backend: self.name.clone(),
greeting: response.trim().to_string(),
})
}
async fn negotiate_compression(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<bool, ConnectionError> {
if self.compress == Some(false) {
return Ok(false);
}
if self.compress == Some(true) {
return self
.probe_compression_with_timeout(stream, buffer)
.await
.map(|support| matches!(support, CompressionSupport::Supported));
}
loop {
let probe_waiter = {
let mut cached_support = self.compression_support.lock().await;
match &*cached_support {
CompressionSupportState::Unsupported => {
tracing::debug!(
backend = %self.name,
"Skipping COMPRESS DEFLATE; backend previously reported it unsupported"
);
return Ok(false);
}
CompressionSupportState::Supported => {
drop(cached_support);
return self
.probe_compression_with_timeout(stream, buffer)
.await
.map(|support| matches!(support, CompressionSupport::Supported));
}
CompressionSupportState::Probing(notify) => {
Some(notify.clone().notified_owned())
}
CompressionSupportState::Unknown => {
let notify = Arc::new(Notify::new());
*cached_support = CompressionSupportState::Probing(notify);
None
}
}
};
if let Some(waiter) = probe_waiter {
waiter.await;
continue;
}
let support = self.probe_compression_with_timeout(stream, buffer).await;
let mut cached_support = self.compression_support.lock().await;
let notify = match std::mem::take(&mut *cached_support) {
CompressionSupportState::Probing(notify) => notify,
state => {
*cached_support = state;
return support.map(|support| matches!(support, CompressionSupport::Supported));
}
};
match support {
Ok(CompressionSupport::Supported) => {
*cached_support = CompressionSupportState::Supported;
notify.notify_waiters();
return Ok(true);
}
Ok(CompressionSupport::Unsupported) => {
*cached_support = CompressionSupportState::Unsupported;
notify.notify_waiters();
return Ok(false);
}
Err(err) => {
*cached_support = CompressionSupportState::Unknown;
notify.notify_waiters();
return Err(err);
}
}
}
}
async fn probe_compression_with_timeout(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<CompressionSupport, ConnectionError> {
tokio::time::timeout(
crate::constants::timeout::CONNECTION,
self.probe_compression(stream, buffer),
)
.await
.map_err(|_| {
ConnectionError::IoError(io::Error::new(
io::ErrorKind::TimedOut,
"timed out negotiating COMPRESS DEFLATE",
))
})?
}
async fn probe_compression(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<CompressionSupport, ConnectionError> {
stream.write_all(crate::protocol::COMPRESS_DEFLATE).await?;
stream.flush().await?;
let request = RequestContext::from_verb_args(b"COMPRESS", b"DEFLATE");
let response = Self::read_backend_setup_reply(stream, &request, buffer).await?;
if crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|code| code.as_u16() == 206)
{
tracing::debug!(
backend = %self.name,
"COMPRESS DEFLATE negotiated successfully"
);
return Ok(CompressionSupport::Supported);
}
if self.compress == Some(true) {
return Err(ConnectionError::CompressionRequired {
backend: self.name.clone(),
response: response.trim().to_string(),
});
}
tracing::debug!(
backend = %self.name,
response = %response.trim(),
"COMPRESS DEFLATE not supported, continuing without compression"
);
Ok(CompressionSupport::Unsupported)
}
async fn negotiate_auth(
&self,
stream: &mut ConnectionStream,
buffer: &mut [u8],
) -> Result<(), ConnectionError> {
let Some(username) = &self.username else {
return Ok(());
};
authinfo_user(username).write_wire_to(stream).await?;
let user_request = authinfo_user(username);
let response = Self::read_backend_setup_reply(stream, &user_request, buffer).await?;
if crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|code| code.requires_auth_credentials())
{
let Some(password) = self.password.as_ref() else {
return Err(ConnectionError::PasswordRequired {
backend: self.name.clone(),
});
};
authinfo_pass(password).write_wire_to(stream).await?;
let pass_request = authinfo_pass(password);
let response = Self::read_backend_setup_reply(stream, &pass_request, buffer).await?;
if !crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|code| code.is_auth_accepted())
{
if crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|c| c.as_u16() == 482)
{
tracing::error!(
backend = %self.name,
host = %self.host,
port = self.port,
response = %response.trim(),
"Backend connection limit exceeded"
);
return Err(ConnectionError::ConnectionLimitExceeded {
backend: self.name.clone(),
response: response.trim().to_string(),
});
}
tracing::error!(
"Authentication failed for {} ({}:{}) - Server response: {} - Username: {}",
self.name,
self.host,
self.port,
response.trim(),
username
);
return Err(ConnectionError::AuthenticationFailed {
backend: self.name.clone(),
response: response.trim().to_string(),
});
}
tracing::debug!(
"Successfully authenticated to {} ({}:{}) as {}",
self.name,
self.host,
self.port,
username
);
} else if !crate::protocol::StatusCode::parse(response.as_bytes())
.is_some_and(|code| code.is_auth_accepted())
{
return Err(ConnectionError::UnexpectedAuthResponse {
backend: self.name.clone(),
response: response.trim().to_string(),
});
}
Ok(())
}
}
impl managed::Manager for TcpManager {
type Type = ConnectionStream;
type Error = ConnectionError;
async fn create(&self) -> Result<ConnectionStream, ConnectionError> {
let mut stream = self.create_optimized_stream().await?;
let mut buffer = [0u8; 4096];
self.consume_greeting(&mut stream, &mut buffer).await?;
self.negotiate_auth(&mut stream, &mut buffer).await?;
if self.send_mode_reader {
self.negotiate_mode_reader(&mut stream, &mut buffer).await?;
}
if self.negotiate_compression(&mut stream, &mut buffer).await? {
let level = self.compress_level.unwrap_or(1);
stream = stream.into_compressed(level)?;
}
Ok(stream)
}
async fn recycle(
&self,
conn: &mut ConnectionStream,
_metrics: &managed::Metrics,
) -> managed::RecycleResult<ConnectionError> {
use super::health_check::check_tcp_alive;
match check_tcp_alive(conn) {
Ok(()) => Ok(()),
Err(e) => {
let _ = socket2::SockRef::from(conn.underlying_tcp_stream())
.shutdown(std::net::Shutdown::Both);
Err(e)
}
}
}
fn detach(&self, _conn: &mut ConnectionStream) {}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncReadExt;
use tokio::net::TcpListener;
#[test]
fn test_socket_buffer_size_u32_rejects_oversized_values() {
let result = TcpManager::socket_buffer_size_u32(u32::MAX as usize + 1, "receive");
assert!(matches!(
result,
Err(ConnectionError::IoError(ref error))
if error.kind() == io::ErrorKind::InvalidInput
&& error.to_string().contains("receive socket buffer size")
));
}
#[test]
fn test_tcp_manager_new_plain() {
let manager = TcpManager::new(
"news.example.com".to_string(),
119,
"TestServer".to_string(),
TcpManagerOptions {
username: Some("user".to_string()),
password: Some("pass".to_string()),
..TcpManagerOptions::default()
},
)
.unwrap();
assert_eq!(manager.host, "news.example.com");
assert_eq!(manager.port, 119);
assert_eq!(manager.name, "TestServer");
assert_eq!(manager.username, Some("user".to_string()));
assert_eq!(manager.password, Some("pass".to_string()));
assert!(!manager.tls_config.use_tls);
assert!(manager.tls_manager.is_none());
}
#[test]
fn test_tcp_manager_new_without_auth() {
let manager = TcpManager::new(
"news.example.com".to_string(),
563,
"SecureServer".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
assert_eq!(manager.host, "news.example.com");
assert_eq!(manager.port, 563);
assert_eq!(manager.name, "SecureServer");
assert!(manager.username.is_none());
assert!(manager.password.is_none());
}
#[test]
fn ip_literal_socket_addr_parses_ipv4_without_dns() {
let manager = TcpManager::new(
"127.0.0.1".to_string(),
119,
"IpBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
assert_eq!(
manager.ip_literal_socket_addr(),
Some(SocketAddr::from(([127, 0, 0, 1], 119)))
);
}
#[test]
fn ip_literal_socket_addr_parses_ipv6_without_dns() {
let manager = TcpManager::new(
"::1".to_string(),
563,
"IpBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
assert_eq!(
manager.ip_literal_socket_addr(),
Some(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 563)))
);
}
#[test]
fn ip_literal_socket_addr_leaves_hostnames_for_dns() {
let manager = TcpManager::new(
"news.example.com".to_string(),
119,
"DnsBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
assert_eq!(manager.ip_literal_socket_addr(), None);
}
#[test]
fn ipv6_network_unreachable_matches_error_kind_only_for_ipv6() {
let ipv6_addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 563));
let ipv4_addr = SocketAddr::from(([127, 0, 0, 1], 563));
let error = ConnectionError::IoError(io::Error::new(
io::ErrorKind::NetworkUnreachable,
"network unreachable",
));
assert!(TcpManager::is_ipv6_network_unreachable(ipv6_addr, &error));
assert!(!TcpManager::is_ipv6_network_unreachable(ipv4_addr, &error));
}
#[tokio::test]
async fn remove_cached_ipv6_socket_addrs_keeps_only_ipv4_addresses() {
let manager = TcpManager::new(
"test.example.com".to_string(),
563,
"DnsBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
let ipv6_addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 563));
let ipv4_addr = SocketAddr::from(([127, 0, 0, 1], 563));
*manager.resolved_socket_addrs.write().await = Some(Arc::from([ipv6_addr, ipv4_addr]));
manager.remove_cached_ipv6_socket_addrs().await;
let cached_addrs = manager
.resolved_socket_addrs
.read()
.await
.as_ref()
.expect("cached addresses should remain initialized")
.clone();
assert_eq!(&*cached_addrs, &[ipv4_addr]);
}
#[tokio::test]
async fn remove_cached_ipv6_socket_addrs_clears_ipv6_only_cache() {
let manager = TcpManager::new(
"test.example.com".to_string(),
563,
"DnsBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
let ipv6_addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 563));
*manager.resolved_socket_addrs.write().await = Some(Arc::from([ipv6_addr]));
manager.remove_cached_ipv6_socket_addrs().await;
assert!(
manager.resolved_socket_addrs.read().await.is_none(),
"IPv6-only cached address list should be cleared instead of retained as an empty cache"
);
}
#[tokio::test]
async fn create_connected_tcp_stream_refreshes_dns_after_cached_addresses_fail() {
let live_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let live_addr = live_listener.local_addr().unwrap();
let stale_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let stale_addr = stale_listener.local_addr().unwrap();
drop(stale_listener);
let manager = TcpManager::new(
"localhost".to_string(),
live_addr.port(),
"DnsBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
*manager.resolved_socket_addrs.write().await = Some(Arc::from([stale_addr]));
let accept = tokio::spawn(async move { live_listener.accept().await.unwrap() });
let stream = manager.create_connected_tcp_stream().await.unwrap();
let _accepted = accept.await.unwrap();
assert_eq!(stream.peer_addr().unwrap(), live_addr);
let cached_addrs = manager
.resolved_socket_addrs
.read()
.await
.as_ref()
.expect("successful refresh should update cached addresses")
.clone();
assert!(cached_addrs.contains(&live_addr));
}
#[test]
fn test_tcp_manager_new_with_tls_disabled() {
let tls_config = TlsConfig::default(); let manager = TcpManager::new(
"news.example.com".to_string(),
119,
"PlainServer".to_string(),
TcpManagerOptions {
username: Some("user".to_string()),
password: Some("pass".to_string()),
tls_config: Some(tls_config),
..TcpManagerOptions::default()
},
)
.unwrap();
assert_eq!(manager.host, "news.example.com");
assert_eq!(manager.port, 119);
assert!(!manager.tls_config.use_tls);
assert!(manager.tls_manager.is_none());
}
#[test]
fn test_tcp_manager_new_with_tls_enabled() {
let tls_config = TlsConfig {
use_tls: true,
tls_verify_cert: true,
tls_cert_path: None,
};
let manager = TcpManager::new(
"secure.example.com".to_string(),
563,
"SecureServer".to_string(),
TcpManagerOptions {
username: Some("user".to_string()),
password: Some("pass".to_string()),
tls_config: Some(tls_config),
..TcpManagerOptions::default()
},
)
.unwrap();
assert_eq!(manager.host, "secure.example.com");
assert_eq!(manager.port, 563);
assert!(manager.tls_config.use_tls);
assert!(manager.tls_manager.is_some());
}
#[test]
fn test_tcp_manager_clone() {
let manager = TcpManager::new(
"news.example.com".to_string(),
119,
"TestServer".to_string(),
TcpManagerOptions {
username: Some("user".to_string()),
password: Some("pass".to_string()),
..TcpManagerOptions::default()
},
)
.unwrap();
let cloned = manager.clone();
assert_eq!(cloned.host, manager.host);
assert_eq!(cloned.port, manager.port);
assert_eq!(cloned.name, manager.name);
assert_eq!(cloned.username, manager.username);
assert_eq!(cloned.password, manager.password);
assert!(Arc::ptr_eq(
&cloned.resolved_socket_addrs,
&manager.resolved_socket_addrs
));
assert!(Arc::ptr_eq(
&cloned.next_resolved_socket_addr,
&manager.next_resolved_socket_addr
));
}
#[test]
fn test_tcp_manager_debug_format() {
let manager = TcpManager::new(
"news.example.com".to_string(),
119,
"TestServer".to_string(),
TcpManagerOptions {
username: Some("user".to_string()),
password: Some("pass".to_string()),
..TcpManagerOptions::default()
},
)
.unwrap();
let debug_str = format!("{manager:?}");
assert!(debug_str.contains("TcpManager"));
assert!(debug_str.contains("news.example.com"));
assert!(debug_str.contains("119"));
}
#[tokio::test]
async fn create_optimized_stream_tries_next_resolved_address_after_connect_error() {
let unavailable_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let unavailable_addr = unavailable_listener.local_addr().unwrap();
drop(unavailable_listener);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let available_addr = listener.local_addr().unwrap();
let accept_task = tokio::spawn(async move {
let (_stream, _) = listener.accept().await.unwrap();
});
let manager = TcpManager::new(
"test.example.com".to_string(),
available_addr.port(),
"FallbackBackend".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
*manager.resolved_socket_addrs.write().await =
Some(Arc::from([unavailable_addr, available_addr]));
let stream = manager
.create_optimized_stream()
.await
.expect("second resolved address should be tried after first connect error");
assert_eq!(stream.connection_type(), "TCP");
accept_task.await.unwrap();
}
#[test]
fn test_tcp_manager_with_tls_manager_is_some() {
let tls_config = TlsConfig {
use_tls: true,
tls_verify_cert: false,
tls_cert_path: None,
};
let manager = TcpManager::new(
"secure.example.com".to_string(),
563,
"SecureServer".to_string(),
TcpManagerOptions {
tls_config: Some(tls_config),
..TcpManagerOptions::default()
},
)
.unwrap();
assert!(manager.tls_manager.is_some());
let arc_clone = manager.tls_manager.as_ref().unwrap().clone();
assert!(Arc::ptr_eq(
manager.tls_manager.as_ref().unwrap(),
&arc_clone
));
}
#[test]
fn test_tcp_manager_with_tls_cert_path() {
let tls_config = TlsConfig {
use_tls: true,
tls_verify_cert: true,
tls_cert_path: Some("/path/to/ca.pem".to_string()),
};
let result = TcpManager::new(
"secure.example.com".to_string(),
563,
"SecureServer".to_string(),
TcpManagerOptions {
tls_config: Some(tls_config),
..TcpManagerOptions::default()
},
);
assert!(result.is_err());
}
#[tokio::test]
async fn mode_reader_negotiation_reads_split_setup_reply() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut command = [0u8; 13];
stream.read_exact(&mut command).await.unwrap();
assert_eq!(&command, b"MODE READER\r\n");
stream.write_all(b"20").await.unwrap();
stream.write_all(b"0 Posting allowed\r\n").await.unwrap();
});
let manager = TcpManager::new(
addr.ip().to_string(),
addr.port(),
"SplitSetup".to_string(),
TcpManagerOptions::default(),
)
.unwrap();
let tcp_stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut stream = ConnectionStream::plain(tcp_stream);
let mut buffer = [0u8; 64];
manager
.negotiate_mode_reader(&mut stream, &mut buffer)
.await
.expect("split MODE READER setup reply should be accepted");
}
#[tokio::test]
async fn compression_negotiation_reads_split_unsupported_reply() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut command = [0u8; 18];
stream.read_exact(&mut command).await.unwrap();
assert_eq!(&command, crate::protocol::COMPRESS_DEFLATE);
stream.write_all(b"50").await.unwrap();
stream.write_all(b"0 Not supported\r\n").await.unwrap();
});
let manager = TcpManager::new(
addr.ip().to_string(),
addr.port(),
"SplitCompression".to_string(),
TcpManagerOptions {
compress: None,
..TcpManagerOptions::default()
},
)
.unwrap();
let tcp_stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut stream = ConnectionStream::plain(tcp_stream);
let mut buffer = [0u8; 64];
let enabled = manager
.negotiate_compression(&mut stream, &mut buffer)
.await
.expect("split unsupported compression reply should be accepted");
assert!(!enabled);
}
#[tokio::test]
async fn auto_compression_serializes_and_remembers_unsupported_backend() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let compress_commands = Arc::new(AtomicUsize::new(0));
let server_commands = compress_commands.clone();
tokio::spawn(async move {
for _ in 0..2 {
let (mut stream, _) = listener.accept().await.unwrap();
let server_commands = server_commands.clone();
tokio::spawn(async move {
let mut command = [0u8; 18];
let read = tokio::time::timeout(
std::time::Duration::from_millis(100),
stream.read_exact(&mut command),
)
.await;
if read.is_err() {
return;
}
read.unwrap().unwrap();
if command == crate::protocol::COMPRESS_DEFLATE {
server_commands.fetch_add(1, Ordering::SeqCst);
stream.write_all(b"500 Not supported\r\n").await.unwrap();
}
});
}
});
let manager = TcpManager::new(
addr.ip().to_string(),
addr.port(),
"CachedUnsupportedCompression".to_string(),
TcpManagerOptions {
compress: None,
..TcpManagerOptions::default()
},
)
.unwrap();
let mut tasks = Vec::new();
for _ in 0..2 {
let manager = manager.clone();
tasks.push(tokio::spawn(async move {
let tcp_stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut stream = ConnectionStream::plain(tcp_stream);
let mut buffer = [0u8; 64];
let enabled = manager
.negotiate_compression(&mut stream, &mut buffer)
.await
.expect("unsupported compression should fall back in auto mode");
assert!(!enabled);
}));
}
for task in tasks {
task.await.unwrap();
}
assert_eq!(
compress_commands.load(Ordering::SeqCst),
1,
"auto mode should remember unsupported COMPRESS DEFLATE"
);
}
}