1use base64::{Engine, engine::general_purpose};
7use std::collections::HashMap;
8use url::Url;
9
10use crate::error::{Aria2Error, Result};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum HttpMethod {
15 Get,
17 Post,
19 Head,
21 Put,
23 Delete,
25}
26
27impl std::fmt::Display for HttpMethod {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 HttpMethod::Get => write!(f, "GET"),
31 HttpMethod::Post => write!(f, "POST"),
32 HttpMethod::Head => write!(f, "HEAD"),
33 HttpMethod::Put => write!(f, "PUT"),
34 HttpMethod::Delete => write!(f, "DELETE"),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
43pub struct HttpRequest {
44 pub method: HttpMethod,
46 pub url: Url,
48 pub headers: HashMap<String, String>,
50 pub body: Option<Vec<u8>>,
52}
53
54impl HttpRequest {
55 pub fn to_bytes(&self) -> Vec<u8> {
64 let mut result = String::new();
65
66 let path = self.url.path();
68 let query = self.url.query();
69 if let Some(q) = query {
70 result.push_str(&format!("{} {}?{} HTTP/1.1\r\n", self.method, path, q));
71 } else {
72 result.push_str(&format!("{} {} HTTP/1.1\r\n", self.method, path));
73 }
74
75 for (key, value) in &self.headers {
77 result.push_str(&format!("{}: {}\r\n", key, value));
78 }
79
80 result.push_str("\r\n");
82
83 let mut bytes = result.into_bytes();
84
85 if let Some(ref body) = self.body {
87 bytes.extend_from_slice(body);
88 }
89
90 bytes
91 }
92}
93
94pub struct HttpRequestBuilder {
111 method: HttpMethod,
113 url: Url,
115 headers: HashMap<String, String>,
117 body: Option<Vec<u8>>,
119}
120
121impl HttpRequestBuilder {
122 pub fn new(method: HttpMethod, url: Url) -> Self {
133 Self {
134 method,
135 url,
136 headers: HashMap::new(),
137 body: None,
138 }
139 }
140
141 pub fn header(mut self, key: &str, value: &str) -> Self {
154 self.headers.insert(key.to_string(), value.to_string());
155 self
156 }
157
158 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
171 self.headers.extend(headers);
172 self
173 }
174
175 pub fn body(mut self, body: Vec<u8>) -> Self {
185 self.body = Some(body);
186 self
187 }
188
189 pub fn build(self) -> Result<HttpRequest> {
202 let mut final_headers = self.headers;
203
204 if !final_headers.contains_key("Host") {
207 let host = self.url.host_str().unwrap_or("");
208 if let Some(port) = self.url.port() {
209 final_headers.insert("Host".to_string(), format!("{}:{}", host, port));
210 } else {
211 final_headers.insert("Host".to_string(), host.to_string());
212 }
213 }
214
215 if !final_headers.contains_key("User-Agent") {
217 final_headers.insert("User-Agent".to_string(), "aria2-rust/1.0".to_string());
218 }
219
220 if !final_headers.contains_key("Accept") {
222 final_headers.insert("Accept".to_string(), "*/*".to_string());
223 }
224
225 if !final_headers.contains_key("Connection") {
227 final_headers.insert("Connection".to_string(), "close".to_string());
228 }
229
230 if let Some(body) = &self.body
232 && !final_headers.contains_key("Content-Length")
233 {
234 let len = body.len();
235 final_headers.insert("Content-Length".to_string(), len.to_string());
236 }
237
238 Ok(HttpRequest {
239 method: self.method,
240 url: self.url,
241 headers: final_headers,
242 body: self.body,
243 })
244 }
245}
246
247#[derive(Debug, Clone)]
252pub struct HttpResponse {
253 pub status_code: u16,
255 pub reason_phrase: String,
257 pub version: String,
259 pub headers: HashMap<String, Vec<String>>,
261 pub body: Option<Vec<u8>>,
263}
264
265impl HttpResponse {
266 pub fn from_bytes(data: &[u8]) -> Result<Self> {
283 let response_str = String::from_utf8(data.to_vec())
284 .map_err(|e| Aria2Error::Parse(format!("Invalid UTF-8 in HTTP response: {}", e)))?;
285
286 let (header_part, body_part) = match response_str.find("\r\n\r\n") {
288 Some(pos) => (&response_str[..pos], &response_str[pos + 4..]),
289 None => (response_str.as_str(), ""),
290 };
291
292 let mut lines = header_part.split("\r\n");
294 let status_line = lines
295 .next()
296 .ok_or_else(|| Aria2Error::Parse("Empty HTTP response".to_string()))?;
297
298 let parts: Vec<&str> = status_line.split_whitespace().collect();
300 if parts.len() < 2 {
301 return Err(Aria2Error::Parse(
302 "Invalid HTTP status line format".to_string(),
303 ));
304 }
305
306 let version = parts[0].to_string();
307 let status_code: u16 = parts[1]
308 .parse()
309 .map_err(|e| Aria2Error::Parse(format!("Invalid status code: {}", e)))?;
310 let reason_phrase = if parts.len() > 2 {
311 parts[2..].join(" ")
312 } else {
313 String::new()
314 };
315
316 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
318 for line in lines {
319 if line.is_empty() {
320 continue;
321 }
322 if let Some((key, value)) = line.split_once(':') {
323 let key = key.trim().to_string();
324 let value = value.trim().to_string();
325 headers.entry(key).or_default().push(value);
326 }
327 }
328
329 let body = if body_part.is_empty() {
331 None
332 } else {
333 Some(body_part.as_bytes().to_vec())
334 };
335
336 Ok(HttpResponse {
337 status_code,
338 reason_phrase,
339 version,
340 headers,
341 body,
342 })
343 }
344
345 pub fn header(&self, name: &str) -> Option<&String> {
355 let name_lower = name.to_lowercase();
356 for (key, values) in &self.headers {
357 if key.to_lowercase() == name_lower {
358 return values.first();
359 }
360 }
361 None
362 }
363
364 pub fn header_all(&self, name: &str) -> Vec<String> {
376 let name_lower = name.to_lowercase();
377 for (key, values) in &self.headers {
378 if key.to_lowercase() == name_lower {
379 return values.clone();
380 }
381 }
382 Vec::new()
383 }
384
385 pub fn content_length(&self) -> Option<u64> {
391 self.header("Content-Length")
392 .and_then(|v| v.parse::<u64>().ok())
393 }
394
395 pub fn is_redirect(&self) -> bool {
401 (300..400).contains(&self.status_code)
402 }
403
404 pub fn location(&self) -> Option<Url> {
412 self.header("Location").and_then(|loc| Url::parse(loc).ok())
413 }
414
415 pub fn decoded_body(&self) -> Result<Vec<u8>> {
439 use crate::http::stream_filter::{AutoFilterSelector, process_filters};
440
441 let encoding = self.header("Content-Encoding").map(|s| s.as_str());
442 let transfer_enc = self.header("Transfer-Encoding").map(|s| s.as_str());
443
444 let mut filters = AutoFilterSelector::select_filters(encoding, transfer_enc);
445
446 match &self.body {
447 Some(raw_data) => process_filters(&mut filters, raw_data),
448 None => Ok(Vec::new()),
449 }
450 }
451}
452
453pub fn basic_auth(username: &str, password: &str) -> String {
466 let credentials = format!("{}:{}", username, password);
467 let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
468 format!("Basic {}", encoded)
469}
470
471pub fn bearer_token(token: &str) -> String {
481 format!("Bearer {}", token)
482}
483
484#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_request_builder_fluent_api() {
492 let url = Url::parse("http://example.com/api/test").unwrap();
493 let request = HttpRequestBuilder::new(HttpMethod::Post, url.clone())
494 .header("Content-Type", "application/json")
495 .header("Accept", "application/json")
496 .body(b"{\"key\":\"value\"}".to_vec())
497 .build()
498 .unwrap();
499
500 assert_eq!(request.method, HttpMethod::Post);
501 assert_eq!(request.url, url);
502 assert_eq!(
503 request.headers.get("Content-Type").unwrap(),
504 "application/json"
505 );
506 assert_eq!(request.headers.get("Accept").unwrap(), "application/json");
507 assert!(request.body.is_some());
508 assert_eq!(request.body.unwrap(), b"{\"key\":\"value\"}");
509 }
510
511 #[test]
512 fn test_request_auto_headers_generation() {
513 let url = Url::parse("http://example.com:8080/path").unwrap();
514 let request = HttpRequestBuilder::new(HttpMethod::Get, url)
515 .build()
516 .unwrap();
517
518 assert_eq!(request.headers.get("Host").unwrap(), "example.com:8080");
520
521 assert_eq!(request.headers.get("User-Agent").unwrap(), "aria2-rust/1.0");
523
524 assert_eq!(request.headers.get("Accept").unwrap(), "*/*");
526
527 assert_eq!(request.headers.get("Connection").unwrap(), "close");
529 }
530
531 #[test]
532 fn test_request_auto_content_length() {
533 let url = Url::parse("http://example.com/api").unwrap();
534 let body = b"test body data";
535 let request = HttpRequestBuilder::new(HttpMethod::Post, url)
536 .body(body.to_vec())
537 .build()
538 .unwrap();
539
540 assert_eq!(
541 request.headers.get("Content-Length").unwrap(),
542 &body.len().to_string()
543 );
544 }
545
546 #[test]
547 fn test_request_custom_host_not_overridden() {
548 let url = Url::parse("http://example.com/api").unwrap();
549 let request = HttpRequestBuilder::new(HttpMethod::Get, url)
550 .header("Host", "custom-host.com")
551 .build()
552 .unwrap();
553
554 assert_eq!(request.headers.get("Host").unwrap(), "custom-host.com");
556 }
557
558 #[test]
559 fn test_request_to_bytes() {
560 let url = Url::parse("http://example.com/path?q=1").unwrap();
561 let request = HttpRequestBuilder::new(HttpMethod::Get, url)
562 .header("Custom-Header", "test-value")
563 .build()
564 .unwrap();
565
566 let bytes = request.to_bytes();
567 let request_str = String::from_utf8(bytes).unwrap();
568
569 assert!(request_str.starts_with("GET /path?q=1 HTTP/1.1\r\n"));
571
572 assert!(request_str.contains("Custom-Header: test-value"));
574
575 assert!(request_str.contains("Host: example.com"));
577 assert!(request_str.contains("User-Agent: aria2-rust/1.0"));
578 }
579
580 #[test]
581 fn test_request_to_bytes_with_body() {
582 let url = Url::parse("http://example.com/api").unwrap();
583 let request = HttpRequestBuilder::new(HttpMethod::Post, url)
584 .header("Content-Type", "text/plain")
585 .body(b"Hello, World!".to_vec())
586 .build()
587 .unwrap();
588
589 let bytes = request.to_bytes();
590 let request_str = String::from_utf8_lossy(&bytes);
591
592 assert!(request_str.contains("POST /api HTTP/1.1"));
593 assert!(request_str.contains("Content-Length: 13"));
594 assert!(request_str.ends_with("Hello, World!"));
595 }
596
597 #[test]
598 fn test_response_status_parsing() {
599 let response_200 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<body>";
601 let resp = HttpResponse::from_bytes(response_200.as_bytes()).unwrap();
602 assert_eq!(resp.status_code, 200);
603 assert_eq!(resp.reason_phrase, "OK");
604 assert_eq!(resp.version, "HTTP/1.1");
605
606 let response_404 = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\nNot Found";
608 let resp = HttpResponse::from_bytes(response_404.as_bytes()).unwrap();
609 assert_eq!(resp.status_code, 404);
610 assert_eq!(resp.reason_phrase, "Not Found");
611
612 let response_301 = "HTTP/1.1 301 Moved Permanently\r\nLocation: /new-url\r\n\r\n";
614 let resp = HttpResponse::from_bytes(response_301.as_bytes()).unwrap();
615 assert_eq!(resp.status_code, 301);
616 assert_eq!(resp.reason_phrase, "Moved Permanently");
617 }
618
619 #[test]
620 fn test_response_multi_value_headers() {
621 let response = "HTTP/1.1 200 OK\r\n\
622 Set-Cookie: session=abc123; Path=/\r\n\
623 Set-Cookie: user=john; Domain=example.com\r\n\
624 Content-Type: text/html\r\n\r\n<body>";
625
626 let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
627
628 let all_cookies = resp.header_all("Set-Cookie");
630 assert_eq!(all_cookies.len(), 2);
631 assert!(all_cookies.contains(&"session=abc123; Path=/".to_string()));
632 assert!(all_cookies.contains(&"user=john; Domain=example.com".to_string()));
633
634 let first_cookie = resp.header("Set-Cookie").unwrap();
636 assert_eq!(first_cookie, "session=abc123; Path=/");
637 }
638
639 #[test]
640 fn test_response_content_length() {
641 let response = "HTTP/1.1 200 OK\r\nContent-Length: 1024\r\n\r\n";
642 let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
643
644 assert_eq!(resp.content_length(), Some(1024));
645
646 let response_no_cl = "HTTP/1.1 200 OK\r\n\r\n";
648 let resp_no_cl = HttpResponse::from_bytes(response_no_cl.as_bytes()).unwrap();
649 assert_eq!(resp_no_cl.content_length(), None);
650 }
651
652 #[test]
653 fn test_response_is_redirect() {
654 let redirect_resp = HttpResponse::from_bytes(
656 "HTTP/1.1 301 Moved Permanently\r\nLocation: /new\r\n\r\n".as_bytes(),
657 )
658 .unwrap();
659 assert!(redirect_resp.is_redirect());
660
661 let redirect_302 =
662 HttpResponse::from_bytes("HTTP/1.1 302 Found\r\n\r\n".as_bytes()).unwrap();
663 assert!(redirect_302.is_redirect());
664
665 let ok_resp = HttpResponse::from_bytes("HTTP/1.1 200 OK\r\n\r\n".as_bytes()).unwrap();
667 assert!(!ok_resp.is_redirect());
668
669 let error_resp =
670 HttpResponse::from_bytes("HTTP/1.1 500 Internal Server Error\r\n\r\n".as_bytes())
671 .unwrap();
672 assert!(!error_resp.is_redirect());
673 }
674
675 #[test]
676 fn test_response_location() {
677 let response =
678 "HTTP/1.1 301 Moved Permanently\r\nLocation: https://example.com/new-page\r\n\r\n";
679 let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
680
681 let location = resp.location().unwrap();
682 assert_eq!(location.as_str(), "https://example.com/new-page");
683 }
684
685 #[test]
686 fn test_response_body_parsing() {
687 let response =
688 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"status\":\"success\"}";
689 let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
690
691 assert!(resp.body.is_some());
692 assert_eq!(resp.body.unwrap(), b"{\"status\":\"success\"}");
693
694 let response_no_body = "HTTP/1.1 204 No Content\r\n\r\n";
696 let resp_no_body = HttpResponse::from_bytes(response_no_body.as_bytes()).unwrap();
697 assert!(resp_no_body.body.is_none());
698 }
699
700 #[test]
701 fn test_basic_auth_header_generation() {
702 let auth = basic_auth("user", "pass");
704 assert_eq!(auth, "Basic dXNlcjpwYXNz");
705
706 let auth_special = basic_auth("admin@email.com", "p@ssw0rd!");
708 assert!(auth_special.starts_with("Basic "));
710 let encoded = &auth_special["Basic ".len()..];
712 let decoded = String::from_utf8(
713 general_purpose::STANDARD
714 .decode(encoded)
715 .unwrap_or_default(),
716 )
717 .unwrap_or_default();
718 assert_eq!(decoded, "admin@email.com:p@ssw0rd!");
719
720 let auth_empty_pass = basic_auth("user", "");
722 assert!(auth_empty_pass.starts_with("Basic "));
723 }
724
725 #[test]
726 fn test_bearer_token_generation() {
727 let token = bearer_token("my-access-token-12345");
728 assert_eq!(token, "Bearer my-access-token-12345");
729 }
730
731 #[test]
732 fn test_http_method_display() {
733 assert_eq!(HttpMethod::Get.to_string(), "GET");
734 assert_eq!(HttpMethod::Post.to_string(), "POST");
735 assert_eq!(HttpMethod::Head.to_string(), "HEAD");
736 assert_eq!(HttpMethod::Put.to_string(), "PUT");
737 assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
738 }
739
740 #[test]
741 fn test_request_builder_batch_headers() {
742 let url = Url::parse("http://example.com/api").unwrap();
743 let mut custom_headers = HashMap::new();
744 custom_headers.insert("X-Custom-1".to_string(), "value1".to_string());
745 custom_headers.insert("X-Custom-2".to_string(), "value2".to_string());
746
747 let request = HttpRequestBuilder::new(HttpMethod::Get, url)
748 .headers(custom_headers.clone())
749 .build()
750 .unwrap();
751
752 assert_eq!(request.headers.get("X-Custom-1").unwrap(), "value1");
753 assert_eq!(request.headers.get("X-Custom-2").unwrap(), "value2");
754 }
755
756 #[test]
757 fn test_response_case_insensitive_headers() {
758 let response = "HTTP/1.1 200 OK\r\n\
759 Content-Type: text/html\r\n\
760 content-length: 100\r\n\r\n";
761
762 let resp = HttpResponse::from_bytes(response.as_bytes()).unwrap();
763
764 assert!(resp.header("content-type").is_some());
766 assert!(resp.header("CONTENT-TYPE").is_some());
767 assert!(resp.header("Content-Length").is_some());
768 }
769}