use std::io::{Read, Write, ErrorKind};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use std::sync::{Mutex, LazyLock};
use native_tls::TlsConnector;
static GLOBAL_AGENT: LazyLock<Mutex<Agent>> = LazyLock::new(|| Mutex::new(Agent::new()));
pub trait ReadWrite: Read + Write + Send {}
impl<T: Read + Write + Send> ReadWrite for T {}
pub struct Http;
impl Http {
pub fn get(url: &str) -> RequestBuilder { RequestBuilder::new("GET", url) }
pub fn post(url: &str) -> RequestBuilder { RequestBuilder::new("POST", url) }
pub fn put(url: &str) -> RequestBuilder { RequestBuilder::new("PUT", url) }
pub fn patch(url: &str) -> RequestBuilder { RequestBuilder::new("PATCH", url) }
pub fn delete(url: &str) -> RequestBuilder { RequestBuilder::new("DELETE", url) }
pub fn head(url: &str) -> RequestBuilder { RequestBuilder::new("HEAD", url) }
pub fn options(url: &str) -> RequestBuilder { RequestBuilder::new("OPTIONS", url) }
pub fn connect(url: &str) -> RequestBuilder { RequestBuilder::new("CONNECT", url) }
pub fn trace(url: &str) -> RequestBuilder { RequestBuilder::new("TRACE", url) }
pub fn copy(url: &str) -> RequestBuilder { RequestBuilder::new("COPY", url) }
pub fn mov(url: &str) -> RequestBuilder { RequestBuilder::new("MOVE", url) }
pub fn mkcol(url: &str) -> RequestBuilder { RequestBuilder::new("MKCOL", url) }
pub fn propfind(url: &str) -> RequestBuilder { RequestBuilder::new("PROPFIND", url) }
pub fn lock(url: &str) -> RequestBuilder { RequestBuilder::new("LOCK", url) }
pub fn unlock(url: &str) -> RequestBuilder { RequestBuilder::new("UNLOCK", url) }
}
pub struct Agent {
stream: Option<Box<dyn ReadWrite>>,
host: String,
}
impl Agent { fn new() -> Self { Self { stream: None, host: String::new() } } }
pub struct Response {
pub raw: String,
}
impl Response {
pub fn status(&self) -> u16 {
self.raw.lines().next()
.and_then(|line| line.split_whitespace().nth(1))
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub fn body(&self) -> &str {
self.raw.split("\r\n\r\n").nth(1).unwrap_or("")
}
pub fn get_header(&self, name: &str) -> Option<&str> {
let prefix = format!("{}: ", name).to_lowercase();
self.raw.lines()
.find(|l| l.to_lowercase().starts_with(&prefix))
.map(|l| l[prefix.len()..].trim())
}
pub fn get_cookies(&self) -> Vec<String> {
self.raw.lines()
.filter(|l| l.to_lowercase().starts_with("set-cookie: "))
.map(|l| l["set-cookie: ".len()..].split(';').next().unwrap_or("").to_string())
.collect()
}
}
pub struct RequestBuilder {
method: String,
url: String,
headers: Vec<(String, String)>,
timeout: u64,
redirects: u8,
}
impl RequestBuilder {
fn new(method: &str, url: &str) -> Self {
Self {
method: method.to_string(),
url: url.to_string(),
headers: Vec::new(),
timeout: 10,
redirects: 0,
}
}
pub fn header(mut self, key: &str, value: String) -> Self {
self.headers.push((key.to_string(), value));
self
}
pub fn cookie(self, name: &str, value: &str) -> Self {
self.header("Cookie", format!("{}={}", name, value))
}
pub fn timeout(mut self, seconds: u64) -> Self {
self.timeout = seconds;
self
}
pub fn send(mut self, body: &str) -> Result<Response, Box<dyn std::error::Error>> {
let res_raw = match self.execute(body) {
Ok(raw) => raw,
Err(e) => {
if let Ok(mut agent) = GLOBAL_AGENT.lock() {
agent.stream = None;
}
if is_retryable(&e) {
self.execute(body)?
} else {
return Err(e);
}
}
};
let response = Response { raw: res_raw };
let status = response.status();
if (status >= 301 && status <= 308) && self.redirects < 5 {
if let Some(new_url) = response.get_header("Location") {
self.url = new_url.to_string();
self.redirects += 1;
return self.send(body);
}
}
Ok(response)
}
fn execute(&self, body: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut agent = GLOBAL_AGENT.lock().map_err(|_| "Mutex Lock Failed")?;
let is_https = self.url.starts_with("https://");
let url_clean = self.url.replace("https://", "").replace("http://", "");
let (host, path_part) = url_clean.split_once('/').unwrap_or((&url_clean, ""));
let path = if path_part.is_empty() { "" } else { path_part };
let port = if is_https { 443 } else { 80 };
if agent.stream.is_none() || agent.host != host {
let addr = format!("{}:{}", host, port).to_socket_addrs()?.next().ok_or("DNS Fail")?;
let tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
tcp.set_read_timeout(Some(Duration::from_secs(self.timeout)))?;
let stream: Box<dyn ReadWrite> = if is_https {
let connector = TlsConnector::new()?;
Box::new(connector.connect(host, tcp)?)
} else {
Box::new(tcp)
};
agent.stream = Some(stream);
agent.host = host.to_string();
}
let stream = agent.stream.as_mut().unwrap();
let mut req_str = format!("{} /{} HTTP/1.1\r\nHost: {}\r\nConnection: keep-alive\r\n", self.method, path, host);
for (k, v) in &self.headers {
req_str.push_str(&format!("{}: {}\r\n", k, v));
}
if !body.is_empty() {
req_str.push_str(&format!("Content-Length: {}\r\n", body.len()));
}
req_str.push_str("\r\n");
stream.write_all(req_str.as_bytes())?;
if !body.is_empty() {
stream.write_all(body.as_bytes())?;
}
let mut buffer = Vec::new();
let mut temp_buf = [0u8; 1024];
let (headers_part, body_bytes) = loop {
let n = stream.read(&mut temp_buf)?;
if n == 0 {
return Err(Box::new(std::io::Error::new(ErrorKind::UnexpectedEof, "Connection closed while reading headers")));
}
buffer.extend_from_slice(&temp_buf[..n]);
let current_str = String::from_utf8_lossy(&buffer);
if let Some(idx) = current_str.find("\r\n\r\n") {
let end_idx = idx + 4;
let headers = current_str[..end_idx].to_string();
let body = buffer.split_off(end_idx);
break (headers, body);
}
if buffer.len() > 16384 {
return Err(Box::new(std::io::Error::new(ErrorKind::InvalidData, "Headers too large")));
}
};
let mut content_length: Option<usize> = None;
let mut is_chunked = false;
for line in headers_part.lines() {
let line_lower = line.to_lowercase();
if let Some(val) = line_lower.strip_prefix("content-length:") {
content_length = val.trim().parse().ok();
} else if line_lower.starts_with("transfer-encoding") && line_lower.contains("chunked") {
is_chunked = true;
}
}
let mut full_body = Vec::new();
if is_chunked {
let mut combined_reader = body_bytes.chain(stream);
loop {
let mut line = String::new();
let mut b = [0u8; 1];
while combined_reader.read_exact(&mut b).is_ok() {
line.push(b[0] as char);
if line.ends_with("\r\n") { break; }
}
let size_str = line.trim();
if size_str.is_empty() { break; }
let chunk_size = usize::from_str_radix(size_str, 16).unwrap_or(0);
if chunk_size == 0 { break; }
let mut chunk_buf = vec![0u8; chunk_size];
combined_reader.read_exact(&mut chunk_buf)?;
full_body.extend_from_slice(&chunk_buf);
let mut crlf = [0u8; 2];
combined_reader.read_exact(&mut crlf)?;
}
} else if let Some(cl) = content_length {
full_body = body_bytes;
while full_body.len() < cl {
let n = stream.read(&mut temp_buf)?;
if n == 0 { break; }
full_body.extend_from_slice(&temp_buf[..n]);
}
} else {
full_body = body_bytes;
loop {
let n = stream.read(&mut temp_buf)?;
if n == 0 { break; }
full_body.extend_from_slice(&temp_buf[..n]);
}
}
let mut full_response = headers_part;
full_response.push_str(&String::from_utf8_lossy(&full_body));
Ok(full_response)
}
}
fn is_retryable(e: &Box<dyn std::error::Error>) -> bool {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
return matches!(
io_err.kind(),
ErrorKind::BrokenPipe |
ErrorKind::ConnectionAborted |
ErrorKind::ConnectionReset |
ErrorKind::UnexpectedEof |
ErrorKind::TimedOut );
}
false
}