use base64::{Engine, engine::general_purpose};
use std::collections::HashMap;
use url::Url;
use crate::error::{Aria2Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum HttpMethod {
Get,
Post,
Head,
Put,
Delete,
}
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HttpMethod::Get => write!(f, "GET"),
HttpMethod::Post => write!(f, "POST"),
HttpMethod::Head => write!(f, "HEAD"),
HttpMethod::Put => write!(f, "PUT"),
HttpMethod::Delete => write!(f, "DELETE"),
}
}
}
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub method: HttpMethod,
pub url: Url,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
impl HttpRequest {
pub fn to_bytes(&self) -> Vec<u8> {
let mut result = String::new();
let path = self.url.path();
let query = self.url.query();
if let Some(q) = query {
result.push_str(&format!("{} {}?{} HTTP/1.1\r\n", self.method, path, q));
} else {
result.push_str(&format!("{} {} HTTP/1.1\r\n", self.method, path));
}
for (key, value) in &self.headers {
result.push_str(&format!("{}: {}\r\n", key, value));
}
result.push_str("\r\n");
let mut bytes = result.into_bytes();
if let Some(ref body) = self.body {
bytes.extend_from_slice(body);
}
bytes
}
}
pub struct HttpRequestBuilder {
method: HttpMethod,
url: Url,
headers: HashMap<String, String>,
body: Option<Vec<u8>>,
}
impl HttpRequestBuilder {
pub fn new(method: HttpMethod, url: Url) -> Self {
Self {
method,
url,
headers: HashMap::new(),
body: None,
}
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers.insert(key.to_string(), value.to_string());
self
}
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
pub fn body(mut self, body: Vec<u8>) -> Self {
self.body = Some(body);
self
}
pub fn build(self) -> Result<HttpRequest> {
let mut final_headers = self.headers;
if !final_headers.contains_key("Host") {
let host = self.url.host_str().unwrap_or("");
if let Some(port) = self.url.port() {
final_headers.insert("Host".to_string(), format!("{}:{}", host, port));
} else {
final_headers.insert("Host".to_string(), host.to_string());
}
}
if !final_headers.contains_key("User-Agent") {
final_headers.insert("User-Agent".to_string(), "aria2-rust/1.0".to_string());
}
if !final_headers.contains_key("Accept") {
final_headers.insert("Accept".to_string(), "*/*".to_string());
}
if !final_headers.contains_key("Connection") {
final_headers.insert("Connection".to_string(), "close".to_string());
}
if let Some(body) = &self.body
&& !final_headers.contains_key("Content-Length")
{
let len = body.len();
final_headers.insert("Content-Length".to_string(), len.to_string());
}
Ok(HttpRequest {
method: self.method,
url: self.url,
headers: final_headers,
body: self.body,
})
}
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status_code: u16,
pub reason_phrase: String,
pub version: String,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
}
impl HttpResponse {
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let response_str = String::from_utf8(data.to_vec())
.map_err(|e| Aria2Error::Parse(format!("Invalid UTF-8 in HTTP response: {}", e)))?;
let (header_part, body_part) = match response_str.find("\r\n\r\n") {
Some(pos) => (&response_str[..pos], &response_str[pos + 4..]),
None => (response_str.as_str(), ""),
};
let mut lines = header_part.split("\r\n");
let status_line = lines
.next()
.ok_or_else(|| Aria2Error::Parse("Empty HTTP response".to_string()))?;
let parts: Vec<&str> = status_line.split_whitespace().collect();
if parts.len() < 2 {
return Err(Aria2Error::Parse(
"Invalid HTTP status line format".to_string(),
));
}
let version = parts[0].to_string();
let status_code: u16 = parts[1]
.parse()
.map_err(|e| Aria2Error::Parse(format!("Invalid status code: {}", e)))?;
let reason_phrase = if parts.len() > 2 {
parts[2..].join(" ")
} else {
String::new()
};
let mut headers: HashMap<String, Vec<String>> = HashMap::new();
for line in lines {
if line.is_empty() {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim().to_string();
let value = value.trim().to_string();
headers.entry(key).or_default().push(value);
}
}
let body = if body_part.is_empty() {
None
} else {
Some(body_part.as_bytes().to_vec())
};
Ok(HttpResponse {
status_code,
reason_phrase,
version,
headers,
body,
})
}
pub fn header(&self, name: &str) -> Option<&String> {
let name_lower = name.to_lowercase();
for (key, values) in &self.headers {
if key.to_lowercase() == name_lower {
return values.first();
}
}
None
}
pub fn header_all(&self, name: &str) -> Vec<String> {
let name_lower = name.to_lowercase();
for (key, values) in &self.headers {
if key.to_lowercase() == name_lower {
return values.clone();
}
}
Vec::new()
}
pub fn content_length(&self) -> Option<u64> {
self.header("Content-Length")
.and_then(|v| v.parse::<u64>().ok())
}
pub fn is_redirect(&self) -> bool {
(300..400).contains(&self.status_code)
}
pub fn location(&self) -> Option<Url> {
self.header("Location").and_then(|loc| Url::parse(loc).ok())
}
pub fn decoded_body(&self) -> Result<Vec<u8>> {
use crate::http::stream_filter::{AutoFilterSelector, process_filters};
let encoding = self.header("Content-Encoding").map(|s| s.as_str());
let transfer_enc = self.header("Transfer-Encoding").map(|s| s.as_str());
let mut filters = AutoFilterSelector::select_filters(encoding, transfer_enc);
match &self.body {
Some(raw_data) => process_filters(&mut filters, raw_data),
None => Ok(Vec::new()),
}
}
}
pub fn basic_auth(username: &str, password: &str) -> String {
let credentials = format!("{}:{}", username, password);
let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
format!("Basic {}", encoded)
}
pub fn bearer_token(token: &str) -> String {
format!("Bearer {}", token)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_builder_fluent_api() {
let url = Url::parse("http://example.com/api/test").unwrap();
let request = HttpRequestBuilder::new(HttpMethod::Post, url.clone())
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(b"{\"key\":\"value\"}".to_vec())
.build()
.unwrap();
assert_eq!(request.method, HttpMethod::Post);
assert_eq!(request.url, url);
assert_eq!(
request.headers.get("Content-Type").unwrap(),
"application/json"
);
assert_eq!(request.headers.get("Accept").unwrap(), "application/json");
assert!(request.body.is_some());
assert_eq!(request.body.unwrap(), b"{\"key\":\"value\"}");
}
#[test]
fn test_request_auto_headers_generation() {
let url = Url::parse("http://example.com:8080/path").unwrap();
let request = HttpRequestBuilder::new(HttpMethod::Get, url)
.build()
.unwrap();
assert_eq!(request.headers.get("Host").unwrap(), "example.com:8080");
assert_eq!(request.headers.get("User-Agent").unwrap(), "aria2-rust/1.0");
assert_eq!(request.headers.get("Accept").unwrap(), "*/*");
assert_eq!(request.headers.get("Connection").unwrap(), "close");
}
#[test]
fn test_request_auto_content_length() {
let url = Url::parse("http://example.com/api").unwrap();
let body = b"test body data";
let request = HttpRequestBuilder::new(HttpMethod::Post, url)
.body(body.to_vec())
.build()
.unwrap();
assert_eq!(
request.headers.get("Content-Length").unwrap(),
&body.len().to_string()
);
}
#[test]
fn test_request_custom_host_not_overridden() {
let url = Url::parse("http://example.com/api").unwrap();
let request = HttpRequestBuilder::new(HttpMethod::Get, url)
.header("Host", "custom-host.com")
.build()
.unwrap();
assert_eq!(request.headers.get("Host").unwrap(), "custom-host.com");
}
#[test]
fn test_request_to_bytes() {
let url = Url::parse("http://example.com/path?q=1").unwrap();
let request = HttpRequestBuilder::new(HttpMethod::Get, url)
.header("Custom-Header", "test-value")
.build()
.unwrap();
let bytes = request.to_bytes();
let request_str = String::from_utf8(bytes).unwrap();
assert!(request_str.starts_with("GET /path?q=1 HTTP/1.1\r\n"));
assert!(request_str.contains("Custom-Header: test-value"));
assert!(request_str.contains("Host: example.com"));
assert!(request_str.contains("User-Agent: aria2-rust/1.0"));
}
#[test]
fn test_request_to_bytes_with_body() {
let url = Url::parse("http://example.com/api").unwrap();
let request = HttpRequestBuilder::new(HttpMethod::Post, url)
.header("Content-Type", "text/plain")
.body(b"Hello, World!".to_vec())
.build()
.unwrap();
let bytes = request.to_bytes();
let request_str = String::from_utf8_lossy(&bytes);
assert!(request_str.contains("POST /api HTTP/1.1"));
assert!(request_str.contains("Content-Length: 13"));
assert!(request_str.ends_with("Hello, World!"));
}
#[test]
fn test_response_status_parsing() {
let response_200 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<body>";
let resp = HttpResponse::from_bytes(response_200.as_bytes()).unwrap();
assert_eq!(resp.status_code, 200);
assert_eq!(resp.reason_phrase, "OK");
assert_eq!(resp.version, "HTTP/1.1");
let response_404 = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\nNot Found";
let resp = HttpResponse::from_bytes(response_404.as_bytes()).unwrap();
assert_eq!(resp.status_code, 404);
assert_eq!(resp.reason_phrase, "Not Found");
let response_301 = "HTTP/1.1 301 Moved Permanently\r\nLocation: /new-url\r\n\r\n";
let resp = HttpResponse::from_bytes(response_301.as_bytes()).unwrap();
assert_eq!(resp.status_code, 301);
assert_eq!(resp.reason_phrase, "Moved Permanently");
}
#[test]
fn test_response_multi_value_headers() {
let response = "HTTP/1.1 200 OK\r\n\
Set-Cookie: session=abc123; Path=/\r\n\
Set-Cookie: user=john; Domain=example.com\r\n\
Content-Type: text/html\r\n\r\n<body>";
let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
let all_cookies = resp.header_all("Set-Cookie");
assert_eq!(all_cookies.len(), 2);
assert!(all_cookies.contains(&"session=abc123; Path=/".to_string()));
assert!(all_cookies.contains(&"user=john; Domain=example.com".to_string()));
let first_cookie = resp.header("Set-Cookie").unwrap();
assert_eq!(first_cookie, "session=abc123; Path=/");
}
#[test]
fn test_response_content_length() {
let response = "HTTP/1.1 200 OK\r\nContent-Length: 1024\r\n\r\n";
let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
assert_eq!(resp.content_length(), Some(1024));
let response_no_cl = "HTTP/1.1 200 OK\r\n\r\n";
let resp_no_cl = HttpResponse::from_bytes(response_no_cl.as_bytes()).unwrap();
assert_eq!(resp_no_cl.content_length(), None);
}
#[test]
fn test_response_is_redirect() {
let redirect_resp = HttpResponse::from_bytes(
"HTTP/1.1 301 Moved Permanently\r\nLocation: /new\r\n\r\n".as_bytes(),
)
.unwrap();
assert!(redirect_resp.is_redirect());
let redirect_302 =
HttpResponse::from_bytes("HTTP/1.1 302 Found\r\n\r\n".as_bytes()).unwrap();
assert!(redirect_302.is_redirect());
let ok_resp = HttpResponse::from_bytes("HTTP/1.1 200 OK\r\n\r\n".as_bytes()).unwrap();
assert!(!ok_resp.is_redirect());
let error_resp =
HttpResponse::from_bytes("HTTP/1.1 500 Internal Server Error\r\n\r\n".as_bytes())
.unwrap();
assert!(!error_resp.is_redirect());
}
#[test]
fn test_response_location() {
let response =
"HTTP/1.1 301 Moved Permanently\r\nLocation: https://example.com/new-page\r\n\r\n";
let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
let location = resp.location().unwrap();
assert_eq!(location.as_str(), "https://example.com/new-page");
}
#[test]
fn test_response_body_parsing() {
let response =
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"status\":\"success\"}";
let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
assert!(resp.body.is_some());
assert_eq!(resp.body.unwrap(), b"{\"status\":\"success\"}");
let response_no_body = "HTTP/1.1 204 No Content\r\n\r\n";
let resp_no_body = HttpResponse::from_bytes(response_no_body.as_bytes()).unwrap();
assert!(resp_no_body.body.is_none());
}
#[test]
fn test_basic_auth_header_generation() {
let auth = basic_auth("user", "pass");
assert_eq!(auth, "Basic dXNlcjpwYXNz");
let auth_special = basic_auth("admin@email.com", "p@ssw0rd!");
assert!(auth_special.starts_with("Basic "));
let encoded = &auth_special["Basic ".len()..];
let decoded = String::from_utf8(
general_purpose::STANDARD
.decode(encoded)
.unwrap_or_default(),
)
.unwrap_or_default();
assert_eq!(decoded, "admin@email.com:p@ssw0rd!");
let auth_empty_pass = basic_auth("user", "");
assert!(auth_empty_pass.starts_with("Basic "));
}
#[test]
fn test_bearer_token_generation() {
let token = bearer_token("my-access-token-12345");
assert_eq!(token, "Bearer my-access-token-12345");
}
#[test]
fn test_http_method_display() {
assert_eq!(HttpMethod::Get.to_string(), "GET");
assert_eq!(HttpMethod::Post.to_string(), "POST");
assert_eq!(HttpMethod::Head.to_string(), "HEAD");
assert_eq!(HttpMethod::Put.to_string(), "PUT");
assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
}
#[test]
fn test_request_builder_batch_headers() {
let url = Url::parse("http://example.com/api").unwrap();
let mut custom_headers = HashMap::new();
custom_headers.insert("X-Custom-1".to_string(), "value1".to_string());
custom_headers.insert("X-Custom-2".to_string(), "value2".to_string());
let request = HttpRequestBuilder::new(HttpMethod::Get, url)
.headers(custom_headers.clone())
.build()
.unwrap();
assert_eq!(request.headers.get("X-Custom-1").unwrap(), "value1");
assert_eq!(request.headers.get("X-Custom-2").unwrap(), "value2");
}
#[test]
fn test_response_case_insensitive_headers() {
let response = "HTTP/1.1 200 OK\r\n\
Content-Type: text/html\r\n\
content-length: 100\r\n\r\n";
let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
assert!(resp.header("content-type").is_some());
assert!(resp.header("CONTENT-TYPE").is_some());
assert!(resp.header("Content-Length").is_some());
}
}