use std::io;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
pub async fn tcp_connect(addr: &str) -> Result<TcpStream, io::Error> {
TcpStream::connect(addr).await
}
pub async fn tcp_bind(addr: &str) -> Result<TcpListener, io::Error> {
TcpListener::bind(addr).await
}
pub async fn read_stream(stream: &mut TcpStream) -> Result<Vec<u8>, io::Error> {
let mut buf = Vec::with_capacity(4096);
stream.read_to_end(&mut buf).await?;
Ok(buf)
}
pub async fn write_stream(stream: &mut TcpStream, data: &[u8]) -> Result<(), io::Error> {
stream.write_all(data).await
}
pub struct HttpResponse {
pub status: u16,
pub body: String,
pub headers: Vec<(String, String)>,
}
pub async fn http_get(url: &str) -> Result<HttpResponse, String> {
let (host, path) = parse_url(url)?;
let addr = format!("{}:80", host);
let mut stream = TcpStream::connect(&addr)
.await
.map_err(|e| format!("connect {}: {}", addr, e))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
path, host
);
stream
.write_all(request.as_bytes())
.await
.map_err(|e| format!("send: {}", e))?;
let mut reader = BufReader::new(&mut stream);
let mut status_line = String::new();
reader
.read_line(&mut status_line)
.await
.map_err(|e| format!("read status: {}", e))?;
let status = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
let mut headers = Vec::new();
loop {
let mut line = String::new();
reader.read_line(&mut line).await.map_err(|e| format!("read header: {}", e))?;
let trimmed = line.trim();
if trimmed.is_empty() {
break;
}
if let Some((k, v)) = trimmed.split_once(':') {
headers.push((k.trim().to_string(), v.trim().to_string()));
}
}
let mut body = String::new();
reader
.read_to_string(&mut body)
.await
.map_err(|e| format!("read body: {}", e))?;
Ok(HttpResponse { status, body, headers })
}
fn parse_url(url: &str) -> Result<(&str, &str), String> {
let url = url
.strip_prefix("http://")
.ok_or_else(|| format!("Only http:// URLs supported: {}", url))?;
match url.find('/') {
Some(pos) => Ok((&url[..pos], &url[pos..])),
None => Ok((url, "/")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_url() {
let (host, path) = parse_url("http://example.com/api").unwrap();
assert_eq!(host, "example.com");
assert_eq!(path, "/api");
}
#[test]
fn test_parse_url_root() {
let (host, path) = parse_url("http://example.com").unwrap();
assert_eq!(host, "example.com");
assert_eq!(path, "/");
}
}