light_tool/
http.rs

1use std::collections::HashMap;
2use std::error::Error;
3use std::io::{Read, Write};
4use std::net::{TcpStream, ToSocketAddrs};
5use std::time::Duration;
6use std::fmt::Write as FmtWrite;
7
8struct HttpClient {
9    host: String,
10    port: u16,
11    path: String,
12    timeout: Duration,
13}
14
15const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
16
17impl HttpClient {
18    fn new(host: &str, port: u16, path: &str, timeout: Duration) -> Self {
19        HttpClient {
20            host: host.to_string(),
21            port,
22            path: path.to_string(),
23            timeout,
24        }
25    }
26
27    fn request(
28        &self,
29        method: &str,
30        headers: Option<HashMap<&str, &str>>,
31        body: Option<&str>,
32    ) -> Result<String, Box<dyn  Error>> {
33        let addr = format!("{}:{}", self.host, self.port);
34        let mut addrs = addr.to_socket_addrs()?;
35        let socket_addr = addrs.next().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Could not resolve address"))?;
36
37        let mut stream = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(2))?;
38
39        stream.set_read_timeout(Some(self.timeout))?;
40        stream.set_write_timeout(Some(self.timeout))?;
41
42        let mut request = String::new();
43        write!(&mut request, "{} {} HTTP/1.1\r\nHost: {}\r\n", method, self.path, self.host)?;
44        write!(&mut request, "User-Agent: Rust HTTP Client\r\n")?;
45        if let Some(headers) = headers {
46            for (key, value) in headers {
47                write!(&mut request, "{}: {}\r\n", key, value)?;
48            }
49        }
50        write!(request, "Connection: close\r\n")?;
51        if let Some(body) = body {
52            write!(&mut request, "Content-Length: {}\r\n", body.len())?;
53        }
54
55        write!(&mut request, "\r\n")?;
56        if let Some(body) = body {
57            write!(&mut request, "{}", body)?;
58        }
59
60        stream.write_all(request.as_bytes())?;
61
62        let mut response = String::new();
63        stream.read_to_string(&mut response)?;
64
65        let body = response.split("\r\n\r\n").nth(1).unwrap_or("");
66        Ok(body.to_string())
67    }
68}
69
70fn client(url: &str, timeout: Duration) -> Result<HttpClient, Box<dyn Error>> {
71    // 127.0.0.1:9090/ping
72    let url = url.strip_prefix("http://")
73        .or_else(|| url.strip_prefix("https://"))
74        .ok_or("Invalid URL: Missing protocol (http or https)")?;
75
76    // ["127.0.0.1:9090", "ping"]
77    let mut parts = url.splitn(2, '/');
78    let host_and_port = parts.next().ok_or("Invalid URL: Missing host")?;
79    let path = format!("/{}", parts.next().unwrap_or(""));
80
81    // ["127.0.0.1", "9090"]
82    let mut host_parts = host_and_port.splitn(2, ':');
83    let host = host_parts.next().ok_or("Invalid URL: Missing host")?.to_string();
84    let port = host_parts
85        .next()
86        .map(|p| p.parse::<u16>().map_err(|_| "Invalid port"))
87        .transpose()?
88        .unwrap_or(80);
89
90    Ok(HttpClient::new(&host, port, &path, timeout))
91}
92
93/// GET Request
94///
95/// # Example
96///
97/// ```txt
98/// use light_tool::http;
99/// assert_eq!(http::get("http://example.com", None).is_ok(), true)
100/// ```
101pub fn get(url: &str, headers: Option<HashMap<&str, &str>>) -> Result<String, Box<dyn Error>> {
102    let client = client(url, DEFAULT_TIMEOUT)?;
103    client.request("GET", headers, None)
104}
105
106/// POST Request
107///
108/// # Example
109///
110/// ```txt
111/// use light_tool::http;
112/// assert_eq!(http::post("http://example.com", None, None).is_ok(), true)
113/// ```
114pub fn post(url: &str, headers: Option<HashMap<&str, &str>>, body: Option<&str>) -> Result<String, Box<dyn Error>> {
115    let client = client(url, DEFAULT_TIMEOUT)?;
116    client.request("POST", headers, body)
117}
118
119/// PUT Request
120///
121/// # Example
122///
123/// ```txt
124/// use light_tool::http;
125/// assert_eq!(http::put("http://192.168.110.106:9900/api/v1/sys/node/dtu", None,
126///     Some("{\"dtu\": true, \"identity\": \"e540f857-704b-4985-bb69-3d6c935debb0\"}")).is_ok(), true)
127/// ```
128pub fn put(url: &str, headers: Option<HashMap<&str, &str>>, body: Option<&str>) -> Result<String, Box<dyn Error>> {
129    let client = client(url, DEFAULT_TIMEOUT)?;
130    client.request("PUT", headers, body)
131}
132
133/// DELETE Request
134///
135/// # Example
136///
137/// ```txt
138/// use light_tool::http;
139/// assert_eq!(http::delete("http://192.168.110.106:9900/api/v1/sys/param/quality/delete?identity=1", None).is_ok(), true)
140/// ```
141pub fn delete(url: &str, headers: Option<HashMap<&str, &str>>) -> Result<String, Box<dyn Error>> {
142    let client = client(url, DEFAULT_TIMEOUT)?;
143    client.request("DELETE", headers, None)
144}
145
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_get() {
153        let response = get("http://example.com", None).expect("GET request failed");
154        // <!doctype html>
155        // <html> ...
156        println!("Response: {}", response);
157    }
158
159    #[test]
160    fn test_post() {
161        let response = post("http://192.168.111.250:9610/api/v1/index/system/state/get", None, None).
162            expect("POST request failed");
163        // Response: {"code":200,"msg":"success","data":{"state_all":"CLOSE","processes":[]}}
164        println!("Response: {}", response);
165    }
166
167    #[test]
168    fn test_put() {
169        let response = put("http://192.168.110.106:9900/api/v1/sys/node/dtu", None,
170                           Some("{\"dtu\": true, \"identity\": \"e540f857-704b-4985-bb69-3d6c935debb0\"}")).
171            expect("PUT request failed");
172        // Response: {"code":200,"msg":"成功","data":null}
173        println!("Response: {}", response);
174    }
175
176    #[test]
177    fn test_delete() {
178        let response = delete("http://192.168.110.106:9900/api/v1/sys/param/quality/delete?identity=1", None).
179            expect("DELETE request failed");
180        // Response: {"code":200,"msg":"成功","data":null}
181        println!("Response: {}", response);
182    }
183}