1use std::io;
7use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
8use tokio::net::{TcpListener, TcpStream};
9
10pub async fn tcp_connect(addr: &str) -> Result<TcpStream, io::Error> {
12 TcpStream::connect(addr).await
13}
14
15pub async fn tcp_bind(addr: &str) -> Result<TcpListener, io::Error> {
17 TcpListener::bind(addr).await
18}
19
20pub async fn read_stream(stream: &mut TcpStream) -> Result<Vec<u8>, io::Error> {
22 let mut buf = Vec::with_capacity(4096);
23 stream.read_to_end(&mut buf).await?;
24 Ok(buf)
25}
26
27pub async fn write_stream(stream: &mut TcpStream, data: &[u8]) -> Result<(), io::Error> {
29 stream.write_all(data).await
30}
31
32pub struct HttpResponse {
36 pub status: u16,
37 pub body: String,
38 pub headers: Vec<(String, String)>,
39}
40
41pub async fn http_get(url: &str) -> Result<HttpResponse, String> {
46 let (host, path) = parse_url(url)?;
47 let addr = format!("{}:80", host);
48
49 let mut stream = TcpStream::connect(&addr)
50 .await
51 .map_err(|e| format!("connect {}: {}", addr, e))?;
52
53 let request = format!(
54 "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
55 path, host
56 );
57 stream
58 .write_all(request.as_bytes())
59 .await
60 .map_err(|e| format!("send: {}", e))?;
61
62 let mut reader = BufReader::new(&mut stream);
63 let mut status_line = String::new();
64 reader
65 .read_line(&mut status_line)
66 .await
67 .map_err(|e| format!("read status: {}", e))?;
68
69 let status = status_line
70 .split_whitespace()
71 .nth(1)
72 .and_then(|s| s.parse::<u16>().ok())
73 .unwrap_or(0);
74
75 let mut headers = Vec::new();
76 loop {
77 let mut line = String::new();
78 reader.read_line(&mut line).await.map_err(|e| format!("read header: {}", e))?;
79 let trimmed = line.trim();
80 if trimmed.is_empty() {
81 break;
82 }
83 if let Some((k, v)) = trimmed.split_once(':') {
84 headers.push((k.trim().to_string(), v.trim().to_string()));
85 }
86 }
87
88 let mut body = String::new();
89 reader
90 .read_to_string(&mut body)
91 .await
92 .map_err(|e| format!("read body: {}", e))?;
93
94 Ok(HttpResponse { status, body, headers })
95}
96
97fn parse_url(url: &str) -> Result<(&str, &str), String> {
98 let url = url
99 .strip_prefix("http://")
100 .ok_or_else(|| format!("Only http:// URLs supported: {}", url))?;
101 match url.find('/') {
102 Some(pos) => Ok((&url[..pos], &url[pos..])),
103 None => Ok((url, "/")),
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_parse_url() {
113 let (host, path) = parse_url("http://example.com/api").unwrap();
114 assert_eq!(host, "example.com");
115 assert_eq!(path, "/api");
116 }
117
118 #[test]
119 fn test_parse_url_root() {
120 let (host, path) = parse_url("http://example.com").unwrap();
121 assert_eq!(host, "example.com");
122 assert_eq!(path, "/");
123 }
124}