1use crate::util::{err, Result};
4use std::io::{Read, Write};
5use std::net::{SocketAddr, TcpStream};
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone)]
9pub struct Response {
10 pub status: u16,
11 pub content_type: String,
12 pub body: Vec<u8>,
13}
14
15impl Response {
16 pub fn text(&self) -> String {
17 String::from_utf8_lossy(&self.body).into_owned()
18 }
19}
20
21pub fn request(
23 addr: SocketAddr,
24 method: &str,
25 path: &str,
26 auth: Option<&str>,
27 body: Option<&str>,
28 timeout: Duration,
29) -> Result<Response> {
30 if !addr.ip().is_loopback() {
31 return err(format!(
32 "QA control endpoint must be loopback, received {addr}"
33 ));
34 }
35 let deadline = Instant::now() + timeout;
36 let mut s = TcpStream::connect_timeout(&addr, timeout.min(Duration::from_secs(5)))?;
37 s.set_write_timeout(Some(timeout))?;
38 let mut head =
39 format!("{method} {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\nAccept: */*\r\n");
40 if let Some(a) = auth {
41 head.push_str(&format!("Authorization: {a}\r\n"));
42 }
43 let payload = body.unwrap_or("");
44 if body.is_some() {
45 head.push_str(&format!(
46 "Content-Type: application/json\r\nContent-Length: {}\r\n",
47 payload.len()
48 ));
49 }
50 head.push_str("\r\n");
51 s.write_all(head.as_bytes())?;
52 s.write_all(payload.as_bytes())?;
53 let mut buf = Vec::new();
54 let mut chunk = [0u8; 16384];
55 loop {
56 let left = deadline.saturating_duration_since(Instant::now());
57 if left.is_zero() {
58 return err(format!(
59 "{method} {path} exceeded {}ms",
60 timeout.as_millis()
61 ));
62 }
63 s.set_read_timeout(Some(left))?;
64 match s.read(&mut chunk) {
65 Ok(0) => break,
66 Ok(n) => buf.extend_from_slice(&chunk[..n]),
67 Err(e)
68 if matches!(
69 e.kind(),
70 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
71 ) =>
72 {
73 return err(format!(
74 "{method} {path} exceeded {}ms",
75 timeout.as_millis()
76 ))
77 }
78 Err(e) => return Err(e.into()),
79 }
80 }
81 let split = buf
82 .windows(4)
83 .position(|w| w == b"\r\n\r\n")
84 .ok_or_else(|| crate::util::Error("malformed HTTP response".into()))?;
85 let head = String::from_utf8_lossy(&buf[..split]).to_string();
86 let status = head
87 .lines()
88 .next()
89 .and_then(|l| l.split_whitespace().nth(1))
90 .and_then(|c| c.parse().ok())
91 .unwrap_or(0);
92 let content_type = head
93 .lines()
94 .find_map(|l| {
95 l.to_ascii_lowercase()
96 .strip_prefix("content-type:")
97 .map(|v| v.trim().to_string())
98 })
99 .unwrap_or_default();
100 Ok(Response {
101 status,
102 content_type,
103 body: buf[split + 4..].to_vec(),
104 })
105}
106
107pub fn loopback(port: u16) -> SocketAddr {
108 SocketAddr::from(([127, 0, 0, 1], port))
109}
110
111pub fn port_open(port: u16) -> bool {
112 TcpStream::connect_timeout(&loopback(port), Duration::from_millis(250)).is_ok()
113}