Skip to main content

iris_core/
net.rs

1//! Async networking utilities built on Tokio.
2//!
3//! Provides TCP connection and listener helpers, plus a minimal HTTP
4//! GET/POST client for common use cases.
5
6use std::io;
7use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
8use tokio::net::{TcpListener, TcpStream};
9
10/// Connect to a TCP server.
11pub async fn tcp_connect(addr: &str) -> Result<TcpStream, io::Error> {
12    TcpStream::connect(addr).await
13}
14
15/// Bind a TCP listener on the given address.
16pub async fn tcp_bind(addr: &str) -> Result<TcpListener, io::Error> {
17    TcpListener::bind(addr).await
18}
19
20/// Read all available data from a TCP stream into a `Vec<u8>`.
21pub 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
27/// Write data to a TCP stream.
28pub async fn write_stream(stream: &mut TcpStream, data: &[u8]) -> Result<(), io::Error> {
29    stream.write_all(data).await
30}
31
32// ─── Minimal HTTP client ─────────────────────────────────────────────
33
34/// A minimal async HTTP response.
35pub struct HttpResponse {
36    pub status: u16,
37    pub body: String,
38    pub headers: Vec<(String, String)>,
39}
40
41/// Perform a minimal HTTP GET request.
42///
43/// This is intentionally simple — it speaks HTTP/1.1 over TCP.
44/// For production use, depend on `reqwest` directly.
45pub 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}