use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
use url::Url;
use crate::error::{Aria2Error, RecoverableError, Result};
use crate::http::cookie_storage::{CookieJar, JarCookie};
#[derive(Debug, Clone)]
pub struct HttpConfig {
pub max_connections: usize,
pub connect_timeout: Duration,
pub read_timeout: Duration,
pub write_timeout: Duration,
pub idle_timeout: Duration,
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
max_connections: crate::constants::HTTP_CONFIG_DEFAULT_MAX_CONNECTIONS,
connect_timeout: Duration::from_secs(
crate::constants::HTTP_CONFIG_DEFAULT_CONNECT_TIMEOUT_SECS,
),
read_timeout: Duration::from_secs(
crate::constants::HTTP_CONFIG_DEFAULT_READ_TIMEOUT_SECS,
),
write_timeout: Duration::from_secs(
crate::constants::HTTP_CONFIG_DEFAULT_WRITE_TIMEOUT_SECS,
),
idle_timeout: Duration::from_secs(
crate::constants::HTTP_CONFIG_DEFAULT_IDLE_TIMEOUT_SECS,
),
}
}
}
#[derive(Debug)]
pub struct ActiveConnection {
pub id: u64,
pub stream: TcpStream,
pub host: String,
pub last_used: Instant,
}
impl ActiveConnection {
pub fn is_valid(&self) -> bool {
self.stream.peer_addr().is_ok()
}
pub fn touch(&mut self) {
self.last_used = Instant::now();
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Idle,
InUse,
Closed,
}
pub struct HttpConnectionManager {
config: HttpConfig,
pool: HashMap<u64, ActiveConnection>,
host_connections: HashMap<String, Vec<u64>>,
active_count: usize,
id_counter: AtomicU64,
max_redirects: u32,
cookie_jar: Option<CookieJar>,
}
impl HttpConnectionManager {
pub fn new(config: &HttpConfig) -> Self {
Self {
config: config.clone(),
pool: HashMap::new(),
host_connections: HashMap::new(),
active_count: 0,
id_counter: AtomicU64::new(1),
max_redirects: crate::constants::HTTP_DEFAULT_MAX_REDIRECTS as u32,
cookie_jar: None,
}
}
pub fn max_connections(&self) -> usize {
self.config.max_connections
}
pub fn active_count(&self) -> usize {
self.active_count
}
pub fn pool_size(&self) -> usize {
self.pool.len()
}
pub async fn acquire(&mut self, url: &Url) -> Result<ActiveConnection> {
let host = Self::extract_host(url);
if let Some(conn) = self.try_reuse_connection(&host)? {
tracing::debug!("复用连接: id={}, host={}", conn.id, host);
return Ok(conn);
}
if self.active_count >= self.config.max_connections {
self.evict_idle_connections();
if self.active_count >= self.config.max_connections {
return Err(Aria2Error::Recoverable(
RecoverableError::TemporaryNetworkFailure {
message: format!(
"达到最大连接数限制: {} (host={})",
self.config.max_connections, host
),
},
));
}
}
self.create_new_connection(url, &host).await
}
pub async fn release(&mut self, conn_id: u64) {
if let Some(mut conn) = self.pool.remove(&conn_id) {
if !conn.is_valid() {
tracing::debug!("连接已失效,移除: id={}", conn_id);
self.active_count = self.active_count.saturating_sub(1);
self.remove_from_host_map(&conn.host, conn_id);
return;
}
conn.touch();
self.pool.insert(conn_id, conn);
tracing::debug!("归还连接到池: id={}", conn_id);
} else {
tracing::warn!("尝试释放不存在的连接: id={}", conn_id);
}
}
pub fn follow_redirects(
&self,
response: &HttpResponse,
current_url: &Url,
redirect_chain: &HashSet<Url>,
redirect_count: u32,
) -> Result<Url> {
if !response.is_redirect() {
return Err(Aria2Error::Parse(format!(
"非重定向响应码: {}",
response.status_code
)));
}
if redirect_count >= self.max_redirects {
return Err(Aria2Error::Network(format!(
"超过最大重定向次数限制: {}",
self.max_redirects
)));
}
let location = response
.location()
.ok_or_else(|| Aria2Error::Parse("缺少 Location 头部".to_string()))?;
let new_url = current_url
.join(location)
.map_err(|e| Aria2Error::Parse(format!("解析重定向 URL 失败: {}", e)))?;
if redirect_chain.contains(&new_url) {
return Err(Aria2Error::Network(format!(
"检测到循环重定向: {}",
new_url
)));
}
tracing::info!(
"跟随重定向: {} -> {} ({}/{})",
current_url,
new_url,
redirect_count + 1,
self.max_redirects
);
Ok(new_url)
}
pub async fn follow_redirects_iterative<F, Fut>(
&self,
initial_url: &Url,
mut get_response: F,
) -> Result<HttpResponse>
where
F: FnMut(&Url) -> Fut,
Fut: std::future::Future<Output = Result<HttpResponse>>,
{
const MAX_REDIRECTS: u8 = crate::constants::HTTP_DEFAULT_MAX_REDIRECTS as u8;
let mut current_url = initial_url.clone();
let mut seen_urls = std::collections::HashSet::<String>::new();
for iteration in 0..MAX_REDIRECTS {
let url_str = current_url.to_string();
if !seen_urls.insert(url_str.clone()) {
return Err(Aria2Error::Network(format!(
"Redirect loop detected: {}",
url_str
)));
}
let resp = get_response(¤t_url).await?;
if !resp.is_redirect() {
return Ok(resp);
}
let location = resp.location().ok_or_else(|| {
Aria2Error::Network("Missing Location header in redirect response".into())
})?;
current_url = current_url
.join(location)
.map_err(|e| Aria2Error::Parse(format!("Failed to parse redirect URL: {}", e)))?;
tracing::info!(
"Following redirect: iteration {}/{}",
iteration + 1,
MAX_REDIRECTS
);
}
Err(Aria2Error::Network(format!(
"Too many redirects (>{}), last URL: {}",
MAX_REDIRECTS, current_url
)))
}
pub fn build_range_header(&self, start: u64, end: Option<u64>) -> String {
match end {
Some(end_val) => format!("bytes={}-{}", start, end_val),
None => format!("bytes={}-", start),
}
}
pub fn parse_content_range(&self, header: &str) -> Option<(u64, u64, u64)> {
let header = header.trim();
if !header.starts_with("bytes ") {
return None;
}
let range_part = &header[6..];
let parts: Vec<&str> = range_part.split('/').collect();
if parts.len() != 2 {
return None;
}
let range_values: Vec<&str> = parts[0].split('-').collect();
if range_values.len() != 2 {
return None;
}
let start: u64 = range_values[0].trim().parse().ok()?;
let end: u64 = range_values[1].trim().parse().ok()?;
let total = match parts[1].trim() {
"*" => u64::MAX,
s => s.parse().ok()?,
};
Some((start, end, total))
}
pub async fn cleanup(&mut self) {
for (_, mut conn) in self.pool.drain() {
let _ = conn.shutdown().await;
}
self.host_connections.clear();
self.active_count = 0;
tracing::info!("连接池已清理");
}
pub async fn close_connection(&mut self, conn_id: u64) {
if let Some(mut conn) = self.pool.remove(&conn_id) {
let _ = conn.shutdown().await;
self.active_count = self.active_count.saturating_sub(1);
self.remove_from_host_map(&conn.host, conn_id);
tracing::debug!("Force closed connection: id={}", conn_id);
}
}
pub fn set_cookie_jar(&mut self, jar: Option<CookieJar>) {
self.cookie_jar = jar;
}
pub fn cookie_jar(&self) -> &Option<CookieJar> {
&self.cookie_jar
}
pub fn cookie_jar_mut(&mut self) -> &mut Option<CookieJar> {
&mut self.cookie_jar
}
pub fn attach_cookies_to_request(&self, url: &Url) -> Option<String> {
let jar = self.cookie_jar.as_ref()?;
let is_https = url.scheme() == "https";
jar.cookie_header_for_url(url.as_str(), is_https)
}
pub fn extract_cookies_from_response(
&mut self,
response_headers: &[(String, String)],
_request_url: &Url,
) -> usize {
let jar = match &mut self.cookie_jar {
Some(j) => j,
None => return 0,
};
let mut stored = 0;
for (name, value) in response_headers {
if name.eq_ignore_ascii_case("set-cookie")
&& let Some(cookie) = JarCookie::parse_set_cookie(value)
{
jar.store(cookie);
stored += 1;
tracing::debug!(
"Extracted and stored cookie from Set-Cookie header: {}",
&value[..value.len().min(80)]
);
}
}
stored
}
fn extract_host(url: &Url) -> String {
match url.port_or_known_default() {
Some(port) => format!(
"{}:{}",
url.host_str().unwrap_or(crate::constants::DEFAULT_HOST),
port
),
None => url
.host_str()
.unwrap_or(crate::constants::DEFAULT_HOST)
.to_string(),
}
}
fn try_reuse_connection(&mut self, host: &str) -> Result<Option<ActiveConnection>> {
let conn_ids = match self.host_connections.get(host) {
Some(ids) => ids.clone(),
None => return Ok(None),
};
for &conn_id in &conn_ids {
if let Some(mut conn) = self.pool.remove(&conn_id) {
if conn.is_valid() {
conn.touch();
let idle_time = conn.last_used.elapsed();
if idle_time < self.config.idle_timeout {
tracing::debug!(
"复用空闲连接: id={}, idle={:.2}s",
conn_id,
idle_time.as_secs_f64()
);
return Ok(Some(conn));
} else {
tracing::debug!(
"连接已过期: id={}, idle={:.2}s",
conn_id,
idle_time.as_secs_f64()
);
self.active_count = self.active_count.saturating_sub(1);
std::mem::drop(conn.shutdown()); }
} else {
self.active_count = self.active_count.saturating_sub(1);
}
}
}
self.cleanup_invalid_connections(host);
Ok(None)
}
async fn create_new_connection(&mut self, url: &Url, host: &str) -> Result<ActiveConnection> {
let addr = Self::resolve_address(url)?;
let stream = timeout(self.config.connect_timeout, TcpStream::connect(&addr))
.await
.map_err(|_| Aria2Error::Recoverable(RecoverableError::Timeout))?
.map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("TCP 连接失败 ({}): {}", addr, e),
})
})?;
if let Err(e) = stream.set_nodelay(true) {
tracing::warn!("设置 nodelay 失败: {}", e);
}
let conn_id = self.id_counter.fetch_add(1, Ordering::SeqCst);
let conn = ActiveConnection {
id: conn_id,
stream,
host: host.to_string(),
last_used: Instant::now(),
};
self.active_count += 1;
self.host_connections
.entry(host.to_string())
.or_default()
.push(conn_id);
tracing::info!(
"创建新连接: id={}, host={}, active={}/{}",
conn_id,
host,
self.active_count,
self.config.max_connections
);
Ok(conn)
}
fn resolve_address(url: &Url) -> Result<SocketAddr> {
let host = url
.host_str()
.ok_or_else(|| Aria2Error::Parse("URL 缺少主机名".to_string()))?;
let port = url
.port_or_known_default()
.ok_or_else(|| Aria2Error::Parse("无法确定端口号".to_string()))?;
let addr_str = format!("{}:{}", host, port);
addr_str
.parse::<SocketAddr>()
.map_err(|e| Aria2Error::Parse(format!("解析地址失败: {}", e)))
}
fn evict_idle_connections(&mut self) {
let now = Instant::now();
let mut evicted = Vec::new();
for (&conn_id, conn) in &self.pool {
if now.duration_since(conn.last_used) > self.config.idle_timeout {
evicted.push((conn_id, conn.host.clone()));
}
}
let evict_count = evicted.len();
for (conn_id, host) in evicted {
if let Some(mut conn) = self.pool.remove(&conn_id) {
std::mem::drop(conn.shutdown());
self.active_count = self.active_count.saturating_sub(1);
self.remove_from_host_map(&host, conn_id);
tracing::debug!("LRU 淘汰过期连接: id={}, host={}", conn_id, host);
}
}
if evict_count > 0 {
tracing::info!("LRU 淘汰了 {} 个过期连接", evict_count);
}
}
fn cleanup_invalid_connections(&mut self, host: &str) {
if let Some(ids) = self.host_connections.get_mut(host) {
ids.retain(|&id| self.pool.contains_key(&id));
if ids.is_empty() {
self.host_connections.remove(host);
}
}
}
fn remove_from_host_map(&mut self, host: &str, conn_id: u64) {
if let Some(ids) = self.host_connections.get_mut(host) {
ids.retain(|&id| id != conn_id);
if ids.is_empty() {
self.host_connections.remove(host);
}
}
}
}
impl ActiveConnection {
pub async fn read_with_timeout(
&mut self,
buf: &mut [u8],
read_timeout: Duration,
) -> Result<usize> {
timeout(read_timeout, self.stream.read(buf))
.await
.map_err(|_| Aria2Error::Recoverable(RecoverableError::Timeout))?
.map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("读取数据失败: {}", e),
})
})
}
pub async fn write_with_timeout(
&mut self,
buf: &[u8],
write_timeout: Duration,
) -> Result<usize> {
timeout(write_timeout, self.stream.write(buf))
.await
.map_err(|_| Aria2Error::Recoverable(RecoverableError::Timeout))?
.map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("写入数据失败: {}", e),
})
})
}
pub async fn flush_with_timeout(&mut self, write_timeout: Duration) -> Result<()> {
timeout(write_timeout, self.stream.flush())
.await
.map_err(|_| Aria2Error::Recoverable(RecoverableError::Timeout))?
.map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("刷新缓冲区失败: {}", e),
})
})
}
pub async fn shutdown(&mut self) -> Result<()> {
match self.stream.shutdown().await {
Ok(_) => Ok(()),
Err(e) => {
tracing::debug!("关闭连接失败: id={}, error={}", self.id, e);
Ok(())
}
}
}
pub fn peer_addr(&self) -> Result<SocketAddr> {
self.stream.peer_addr().map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("获取对等端地址失败: {}", e),
})
})
}
pub fn local_addr(&self) -> Result<SocketAddr> {
self.stream.local_addr().map_err(|e| {
Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
message: format!("获取本地地址失败: {}", e),
})
})
}
}
impl Drop for HttpConnectionManager {
fn drop(&mut self) {
for (_, conn) in self.pool.drain() {
drop(conn);
}
self.host_connections.clear();
}
}
impl std::fmt::Debug for HttpConnectionManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpConnectionManager")
.field("max_connections", &self.config.max_connections)
.field("connect_timeout", &self.config.connect_timeout)
.field("read_timeout", &self.config.read_timeout)
.field("write_timeout", &self.config.write_timeout)
.field("idle_timeout", &self.config.idle_timeout)
.field("active_count", &self.active_count)
.field("pool_size", &self.pool.len())
.field("cookie_jar_set", &self.cookie_jar.is_some())
.finish()
}
}
pub use aria2_protocol::http::response::HttpResponse;
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
use tokio::time::{sleep, timeout};
fn create_test_config() -> HttpConfig {
HttpConfig {
max_connections: 4,
connect_timeout: Duration::from_millis(100),
read_timeout: Duration::from_millis(200),
write_timeout: Duration::from_millis(200),
idle_timeout: Duration::from_millis(500),
}
}
#[test]
fn test_config_default() {
let config = HttpConfig::default();
assert_eq!(config.max_connections, 16);
assert_eq!(config.connect_timeout, Duration::from_secs(30));
assert_eq!(config.read_timeout, Duration::from_secs(60));
assert_eq!(config.write_timeout, Duration::from_secs(60));
assert_eq!(config.idle_timeout, Duration::from_secs(300));
}
#[test]
fn test_manager_creation() {
let config = create_test_config();
let manager = HttpConnectionManager::new(&config);
assert_eq!(manager.max_connections(), 4);
assert_eq!(manager.active_count(), 0);
assert_eq!(manager.pool_size(), 0);
}
#[test]
fn test_build_range_header() {
let manager = HttpConnectionManager::new(&Default::default());
assert_eq!(manager.build_range_header(0, Some(999)), "bytes=0-999");
assert_eq!(manager.build_range_header(500, None), "bytes=500-");
assert_eq!(manager.build_range_header(42, Some(42)), "bytes=42-42");
assert_eq!(
manager.build_range_header(u64::MAX - 1, Some(u64::MAX)),
"bytes=18446744073709551614-18446744073709551615"
);
}
#[test]
fn test_parse_content_range() {
let manager = HttpConnectionManager::new(&Default::default());
assert_eq!(
manager.parse_content_range("bytes 0-499/1000"),
Some((0, 499, 1000))
);
assert_eq!(
manager.parse_content_range("bytes 500-999/*"),
Some((500, 999, u64::MAX))
);
assert_eq!(manager.parse_content_range("bytes 0-0/1"), Some((0, 0, 1)));
assert_eq!(manager.parse_content_range(""), None);
assert_eq!(manager.parse_content_range("invalid"), None);
assert_eq!(manager.parse_content_range("bits 0-99/1000"), None);
assert_eq!(manager.parse_content_range("bytes 0-499"), None); assert_eq!(manager.parse_content_range("bytes abc-def/1000"), None);
}
#[test]
fn test_follow_redirects_success() {
let manager = HttpConnectionManager::new(&Default::default());
let current_url = Url::parse("http://example.com/old").unwrap();
let mut chain = HashSet::new();
chain.insert(current_url.clone());
let mut response = HttpResponse::new(301, "Moved Permanently".to_string());
response
.headers
.push(("Location".to_string(), "http://example.com/new".to_string()));
let result = manager.follow_redirects(&response, ¤t_url, &chain, 1);
assert!(result.is_ok());
let new_url = result.unwrap();
assert!(new_url.as_str().starts_with("http://example.com/new"));
}
#[test]
fn test_follow_redirects_relative_path() {
let manager = HttpConnectionManager::new(&Default::default());
let current_url = Url::parse("http://example.com/path/page.html").unwrap();
let chain = HashSet::new();
let mut response = HttpResponse::new(302, "Found".to_string());
response
.headers
.push(("Location".to_string(), "../other".to_string()));
let result = manager.follow_redirects(&response, ¤t_url, &chain, 1);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "http://example.com/other");
}
#[test]
fn test_follow_redirects_loop_detection() {
let manager = HttpConnectionManager::new(&Default::default());
let url_a = Url::parse("http://example.com/a").unwrap();
let url_b = Url::parse("http://example.com/b").unwrap();
let mut chain = HashSet::new();
chain.insert(url_a.clone());
chain.insert(url_b.clone());
let mut response = HttpResponse::new(301, "Moved".to_string());
response
.headers
.push(("Location".to_string(), "http://example.com/a".to_string()));
let result = manager.follow_redirects(&response, &url_b, &chain, 2);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("循环重定向"));
}
#[test]
fn test_follow_redirects_max_exceeded() {
let manager = HttpConnectionManager::new(&Default::default());
let current_url = Url::parse("http://example.com/start").unwrap();
let chain = HashSet::new();
let mut response = HttpResponse::new(302, "Found".to_string());
response.headers.push((
"Location".to_string(),
"http://example.com/next".to_string(),
));
let result = manager.follow_redirects(&response, ¤t_url, &chain, 6);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("最大重定向"));
}
#[test]
fn test_follow_redirects_non_redirect_response() {
let manager = HttpConnectionManager::new(&Default::default());
let current_url = Url::parse("http://example.com/").unwrap();
let chain = HashSet::new();
let response = HttpResponse::new(200, "OK".to_string());
let result = manager.follow_redirects(&response, ¤t_url, &chain, 0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("非重定向"));
}
#[test]
fn test_follow_redirects_missing_location() {
let manager = HttpConnectionManager::new(&Default::default());
let current_url = Url::parse("http://example.com/").unwrap();
let chain = HashSet::new();
let response = HttpResponse::new(301, "Moved".to_string());
let result = manager.follow_redirects(&response, ¤t_url, &chain, 0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Location"));
}
#[test]
fn test_extract_host() {
let url = Url::parse("http://example.com/path").unwrap();
assert_eq!(HttpConnectionManager::extract_host(&url), "example.com:80");
let url = Url::parse("https://example.com:443/path").unwrap();
assert_eq!(HttpConnectionManager::extract_host(&url), "example.com:443");
let url = Url::parse("http://example.com:8080/path").unwrap();
assert_eq!(
HttpConnectionManager::extract_host(&url),
"example.com:8080"
);
}
#[test]
fn test_debug_format() {
let config = create_test_config();
let manager = HttpConnectionManager::new(&config);
let debug_str = format!("{:?}", manager);
assert!(debug_str.contains("HttpConnectionManager"));
assert!(debug_str.contains("max_connections: 4"));
assert!(debug_str.contains("active_count: 0"));
}
async fn start_test_server(
handler: impl Fn(TcpStream) + Send + 'static,
) -> (SocketAddr, tokio::task::JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let handle = tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
handler(stream);
}
});
(addr, handle)
}
#[tokio::test]
async fn test_connection_pool_reuse() {
let config = HttpConfig {
max_connections: 4,
connect_timeout: Duration::from_millis(500),
read_timeout: Duration::from_millis(1000),
write_timeout: Duration::from_millis(1000),
idle_timeout: Duration::from_millis(2000),
};
let mut manager = HttpConnectionManager::new(&config);
let (addr, server_handle) = start_test_server(|mut stream| {
tokio::spawn(async move {
let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
stream.write_all(response.as_bytes()).await.unwrap();
});
})
.await;
sleep(Duration::from_millis(100)).await;
let url = Url::parse(&format!("http://{}", addr)).unwrap();
let conn1 = manager.acquire(&url).await.expect("第一次获取连接应成功");
let _conn1_id = conn1.id;
assert_eq!(manager.active_count(), 1);
manager.release(conn1.id).await;
let conn2 = manager.acquire(&url).await.expect("第二次应能获取连接");
assert!(manager.active_count() >= 1);
manager.release(conn2.id).await;
manager.cleanup().await;
server_handle.abort();
}
#[tokio::test]
async fn test_redirect_follow_5_jumps() {
let manager = HttpConnectionManager::new(&create_test_config());
let current_url = Url::parse("http://example.com/start").unwrap();
let mut redirect_chain = HashSet::new();
redirect_chain.insert(current_url.clone());
let urls = [
"http://example.com/page1",
"http://example.com/page2",
"http://example.com/page3",
"http://example.com/page4",
"http://example.com/final",
];
let mut current = current_url;
for (i, target) in urls.iter().enumerate() {
let mut response = HttpResponse::new(302, "Found".to_string());
response
.headers
.push(("Location".to_string(), target.to_string()));
redirect_chain.insert(current.clone());
let result = manager.follow_redirects(&response, ¤t, &redirect_chain, i as u32);
assert!(
result.is_ok(),
"第 {} 次重定向应成功: {:?}",
i + 1,
result.err()
);
current = result.unwrap();
}
assert!(current.as_str().contains("example.com/final"));
}
#[tokio::test]
async fn test_redirect_loop_detection() {
let manager = HttpConnectionManager::new(&create_test_config());
let url_a = Url::parse("http://example.com/a").unwrap();
let url_b = Url::parse("http://example.com/b").unwrap();
let url_c = Url::parse("http://example.com/c").unwrap();
let mut chain = HashSet::new();
chain.insert(url_a.clone());
chain.insert(url_b.clone());
chain.insert(url_c.clone());
let mut response = HttpResponse::new(301, "Moved".to_string());
response
.headers
.push(("Location".to_string(), "http://example.com/a".to_string()));
let result = manager.follow_redirects(&response, &url_c, &chain, 3);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("循环重定向"));
}
#[test]
fn test_range_request_build() {
let manager = HttpConnectionManager::new(&create_test_config());
assert_eq!(manager.build_range_header(0, Some(999)), "bytes=0-999");
assert_eq!(manager.build_range_header(500, None), "bytes=500-");
assert_eq!(manager.build_range_header(42, Some(42)), "bytes=42-42");
assert_eq!(
manager.parse_content_range("bytes 0-499/1000"),
Some((0, 499, 1000))
);
assert_eq!(
manager.parse_content_range("bytes 500-999/*"),
Some((500, 999, u64::MAX))
);
assert_eq!(manager.parse_content_range("invalid"), None);
}
#[tokio::test]
async fn test_timeout_on_slow_server() {
use std::time::Instant;
let config = HttpConfig {
max_connections: 2,
connect_timeout: Duration::from_millis(100),
read_timeout: Duration::from_millis(200),
write_timeout: Duration::from_millis(200),
idle_timeout: Duration::from_secs(60),
};
let mut manager = HttpConnectionManager::new(&config);
let (addr, server_handle) = start_test_server(|_stream| {
tokio::spawn(async move {
sleep(Duration::from_secs(10)).await;
});
})
.await;
sleep(Duration::from_millis(50)).await;
let url = Url::parse(&format!("http://{}", addr)).unwrap();
let start = Instant::now();
let _result = timeout(
config.connect_timeout + Duration::from_millis(50),
manager.acquire(&url),
)
.await;
let elapsed = start.elapsed();
assert!(
elapsed < config.connect_timeout + Duration::from_millis(300),
"耗时过长: {:.2}ms",
elapsed.as_millis()
);
manager.cleanup().await;
server_handle.abort();
}
#[tokio::test]
async fn test_max_connections_limit() {
let config = HttpConfig {
max_connections: 2,
connect_timeout: Duration::from_millis(500),
read_timeout: Duration::from_millis(1000),
write_timeout: Duration::from_millis(1000),
idle_timeout: Duration::from_secs(60),
};
let mut manager = HttpConnectionManager::new(&config);
let (addr, _server_handle) = start_test_server(|mut stream| {
tokio::spawn(async move {
let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
stream.write_all(response.as_bytes()).await.unwrap();
sleep(Duration::from_secs(10)).await;
});
})
.await;
sleep(Duration::from_millis(100)).await;
let url = Url::parse(&format!("http://{}", addr)).unwrap();
let conn1 = manager.acquire(&url).await.unwrap();
assert!(manager.active_count() >= 1);
let conn2 = manager.acquire(&url).await.unwrap();
assert!(manager.active_count() >= 2);
let result = manager.acquire(&url).await;
assert!(result.is_err(), "超过最大连接数限制时应返回错误");
if let Err(e) = result {
match &e {
Aria2Error::Recoverable(_) => {}
other => panic!("期望 Recoverable 错误,得到: {:?}", other),
}
}
manager.release(conn1.id).await;
match manager.acquire(&url).await {
Ok(conn3) => {
println!("✓ 归还后成功获取新连接: id={}", conn3.id);
manager.release(conn3.id).await;
}
Err(e) => {
println!("⚠ 归还后获取失败(可能是连接复用限制): {}", e);
}
}
manager.release(conn2.id).await;
manager.cleanup().await;
}
#[test]
fn test_cookie_jar_initially_none() {
let mut manager = HttpConnectionManager::new(&create_test_config());
assert!(manager.cookie_jar().is_none());
assert!(manager.cookie_jar_mut().is_none());
let url = Url::parse("https://example.com/").unwrap();
assert!(manager.attach_cookies_to_request(&url).is_none());
}
#[test]
fn test_set_and_get_cookie_jar() {
let mut manager = HttpConnectionManager::new(&create_test_config());
assert!(manager.cookie_jar().is_none());
let jar = CookieJar::new();
manager.set_cookie_jar(Some(jar));
assert!(manager.cookie_jar().is_some());
manager.set_cookie_jar(None);
assert!(manager.cookie_jar().is_none());
}
#[test]
fn test_attach_cookies_to_request() {
let mut manager = HttpConnectionManager::new(&create_test_config());
let mut jar = CookieJar::new();
jar.store(JarCookie::new("session_id", "abc123", "example.com"));
jar.store(JarCookie::new("theme", "dark", "example.com"));
manager.set_cookie_jar(Some(jar));
let url = Url::parse("http://example.com/api/data").unwrap();
let header = manager.attach_cookies_to_request(&url);
assert!(header.is_some(), "Should return Some with matching cookies");
let hdr = header.unwrap();
assert!(
hdr.contains("session_id=abc123"),
"Header should contain session_id cookie: {}",
hdr
);
assert!(
hdr.contains("theme=dark"),
"Header should contain theme cookie: {}",
hdr
);
let url2 = Url::parse("http://other.com/").unwrap();
let header2 = manager.attach_cookies_to_request(&url2);
assert!(header2.is_none(), "No cookies should match other domain");
}
#[test]
fn test_extract_cookies_from_response() {
let mut manager = HttpConnectionManager::new(&create_test_config());
manager.set_cookie_jar(Some(CookieJar::new()));
let response_headers = vec![
(
"Set-Cookie".to_string(),
"session=xyz789; Domain=example.com; Path=/".to_string(),
),
(
"Set-Cookie".to_string(),
"prefs=en-US; Domain=example.com; Path=/; Secure; HttpOnly".to_string(),
),
("Content-Type".to_string(), "text/html".to_string()), ];
let url = Url::parse("https://example.com/login").unwrap();
let count = manager.extract_cookies_from_response(&response_headers, &url);
assert_eq!(count, 2, "Should extract exactly 2 cookies");
let jar = manager.cookie_jar().as_ref().unwrap();
assert_eq!(jar.len(), 2, "Jar should contain 2 stored cookies");
let cookies = jar.get_cookies_for_url("https://example.com/", true);
assert_eq!(cookies.len(), 2);
let names: Vec<&str> = cookies.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"session"));
assert!(names.contains(&"prefs"));
let prefs_cookie = cookies.iter().find(|c| c.name == "prefs").unwrap();
assert!(prefs_cookie.secure, "prefs cookie should be marked secure");
assert!(
prefs_cookie.http_only,
"prefs cookie should be marked http_only"
);
}
#[test]
fn test_extract_cookies_no_jar_returns_zero() {
let mut manager = HttpConnectionManager::new(&create_test_config());
let headers = vec![("Set-Cookie".to_string(), "test=val".to_string())];
let url = Url::parse("http://example.com/").unwrap();
let count = manager.extract_cookies_from_response(&headers, &url);
assert_eq!(count, 0, "Should return 0 when no jar is set");
}
#[test]
fn test_extract_cookies_invalid_header_skipped() {
let mut manager = HttpConnectionManager::new(&create_test_config());
manager.set_cookie_jar(Some(CookieJar::new()));
let headers = vec![
(
"Set-Cookie".to_string(),
"valid=test_value; Domain=x.com".to_string(),
),
("Set-Cookie".to_string(), "no-equal-sign".to_string()), ("Set-Cookie".to_string(), "".to_string()), ];
let url = Url::parse("http://x.com/").unwrap();
let count = manager.extract_cookies_from_response(&headers, &url);
assert_eq!(count, 1, "Only 1 valid cookie should be extracted");
let jar = manager.cookie_jar().as_ref().unwrap();
assert_eq!(jar.len(), 1);
let cookies = jar.get_cookies_for_url("http://x.com/", false);
assert_eq!(cookies[0].name, "valid");
}
#[test]
fn test_debug_format_includes_cookie_jar() {
let mut manager = HttpConnectionManager::new(&create_test_config());
let debug_str = format!("{:?}", manager);
assert!(!debug_str.contains("cookie_jar_set: true"));
manager.set_cookie_jar(Some(CookieJar::new()));
let debug_str_with_jar = format!("{:?}", manager);
assert!(
debug_str_with_jar.contains("cookie_jar_set: true"),
"Debug output should show cookie_jar is set: {}",
debug_str_with_jar
);
}
#[test]
fn test_secure_cookie_not_sent_over_http() {
let mut manager = HttpConnectionManager::new(&create_test_config());
let mut jar = CookieJar::new();
let mut secure_cookie = JarCookie::new("token", "secret", "secure.example.com");
secure_cookie.secure = true;
jar.store(secure_cookie);
manager.set_cookie_jar(Some(jar));
let url_http = Url::parse("http://secure.example.com/api").unwrap();
let header_http = manager.attach_cookies_to_request(&url_http);
assert!(
header_http.is_none(),
"Secure cookie must not be sent over HTTP"
);
let url_https = Url::parse("https://secure.example.com/api").unwrap();
let header_https = manager.attach_cookies_to_request(&url_https);
assert!(
header_https.is_some(),
"Secure cookie should be sent over HTTPS"
);
assert!(
header_https.unwrap().contains("token=secret"),
"Header should contain the secure token cookie"
);
}
}