Skip to main content

hatchery_build/
http.rs

1//! Minimal HTTP/1.1 helpers (one request per connection) shared by the host, C2 and the node.
2
3use std::io;
4use std::net::SocketAddr;
5use std::time::Duration;
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7use tokio::net::TcpStream;
8use tokio::time::timeout;
9
10pub const MAX_RESPONSE: usize = 64 * 1024 * 1024;
11
12/// POST a body; returns (status, response body).
13pub async fn post(addr: SocketAddr, path: &str, bearer: Option<&str>, body: &[u8], deadline: Duration) -> io::Result<(u16, Vec<u8>)> {
14    let io = async {
15        let mut stream = TcpStream::connect(addr).await?;
16        let auth = bearer.map(|t| format!("Authorization: Bearer {t}\r\n")).unwrap_or_default();
17        let head = format!(
18            "POST {path} HTTP/1.1\r\nHost: {addr}\r\n{auth}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
19            body.len()
20        );
21        stream.write_all(head.as_bytes()).await?;
22        stream.write_all(body).await?;
23        let mut raw = Vec::new();
24        (&mut stream).take(MAX_RESPONSE as u64).read_to_end(&mut raw).await?;
25        Ok::<_, io::Error>(raw)
26    };
27    let raw = timeout(deadline, io).await.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "request timed out"))??;
28    let split = raw.windows(4).position(|w| w == b"\r\n\r\n").ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no header end"))?;
29    let status = std::str::from_utf8(&raw[..split])
30        .ok()
31        .and_then(|h| h.split_whitespace().nth(1))
32        .and_then(|s| s.parse().ok())
33        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "bad status line"))?;
34    Ok((status, raw[split + 4..].to_vec()))
35}
36
37/// Read one request: (method, path, bearer, body). Body limited to `max_body`.
38pub async fn read_request(stream: &mut TcpStream, max_body: usize) -> io::Result<(String, String, Option<String>, Vec<u8>)> {
39    let mut bytes = Vec::with_capacity(2048);
40    let mut chunk = [0_u8; 2048];
41    let header_end = loop {
42        let n = stream.read(&mut chunk).await?;
43        if n == 0 {
44            return Err(io::ErrorKind::UnexpectedEof.into());
45        }
46        bytes.extend_from_slice(&chunk[..n]);
47        if bytes.len() > max_body + 8192 {
48            return Err(io::Error::new(io::ErrorKind::InvalidData, "too large"));
49        }
50        if let Some(pos) = bytes.windows(4).position(|w| w == b"\r\n\r\n") {
51            break pos + 4;
52        }
53    };
54    let head = std::str::from_utf8(&bytes[..header_end]).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "utf8"))?;
55    let mut lines = head.split("\r\n");
56    let mut first = lines.next().unwrap_or_default().split_whitespace();
57    let method = first.next().unwrap_or_default().to_owned();
58    let path = first.next().unwrap_or_default().to_owned();
59    let (mut length, mut bearer) = (0_usize, None);
60    for line in lines {
61        if let Some((name, value)) = line.split_once(':') {
62            if name.eq_ignore_ascii_case("content-length") {
63                length = value.trim().parse().map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "length"))?;
64            } else if name.eq_ignore_ascii_case("authorization") {
65                bearer = value.trim().strip_prefix("Bearer ").map(str::to_owned);
66            }
67        }
68    }
69    if length > max_body {
70        return Err(io::Error::new(io::ErrorKind::InvalidData, "body too large"));
71    }
72    let mut body = bytes[header_end..].to_vec();
73    while body.len() < length {
74        let n = stream.read(&mut chunk).await?;
75        if n == 0 {
76            return Err(io::ErrorKind::UnexpectedEof.into());
77        }
78        body.extend_from_slice(&chunk[..n]);
79    }
80    body.truncate(length);
81    Ok((method, path, bearer, body))
82}
83
84pub async fn respond(stream: &mut TcpStream, status: u16, body: &[u8]) -> io::Result<()> {
85    let reason = match status {
86        200 => "OK",
87        400 => "Bad Request",
88        401 => "Unauthorized",
89        403 => "Forbidden",
90        404 => "Not Found",
91        422 => "Unprocessable Entity",
92        429 => "Too Many Requests",
93        502 => "Bad Gateway",
94        504 => "Gateway Timeout",
95        _ => "Internal Server Error",
96    };
97    let head = format!(
98        "HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
99        body.len()
100    );
101    stream.write_all(head.as_bytes()).await?;
102    stream.write_all(body).await?;
103    stream.shutdown().await
104}
105
106/// Constant-time comparison for bearer secrets.
107pub fn secret_eq(a: &str, b: &str) -> bool {
108    let (a, b) = (a.as_bytes(), b.as_bytes());
109    let mut diff = (a.len() ^ b.len()) as u8;
110    for i in 0..a.len().max(b.len()) {
111        diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0);
112    }
113    diff == 0
114}