use crate::{ProxyError, Result};
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt};
use tokio::time::{timeout_at, Duration, Instant};
pub(crate) const HTTP_READ_TIMEOUT: Duration = Duration::from_secs(15);
pub(crate) const MAX_HTTP_HEADERS: usize = 100;
pub(crate) const MAX_HTTP_HEADER_BYTES: usize = 64 * 1024;
pub(crate) const MAX_HTTP_BODY_BYTES: usize = 8 * 1024 * 1024;
pub(crate) fn constant_time_eq_str(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
pub(crate) struct RequestHead {
pub method: String,
pub path: String,
pub content_length: usize,
pub headers: Vec<String>,
}
impl RequestHead {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.iter().find_map(|line| {
let (k, v) = line.split_once(':')?;
k.trim().eq_ignore_ascii_case(name).then(|| v.trim())
})
}
}
pub(crate) async fn read_head<R: AsyncBufRead + Unpin>(
reader: &mut R,
deadline: Instant,
) -> Result<RequestHead> {
let mut line = String::new();
let mut method = String::new();
let mut path = String::new();
let mut content_length = 0usize;
let mut headers = Vec::new();
let mut total = 0usize;
let mut first = true;
loop {
line.clear();
let n = timeout_at(deadline, reader.read_line(&mut line))
.await
.map_err(|_| ProxyError::Network("request read timeout".to_string()))?
.map_err(|e| ProxyError::Network(format!("request read: {}", e)))?;
if n == 0 || line == "\r\n" || line == "\n" {
break;
}
total += n;
if headers.len() >= MAX_HTTP_HEADERS || total > MAX_HTTP_HEADER_BYTES {
return Err(ProxyError::Network(
"request header section too large".to_string(),
));
}
if first {
let mut parts = line.split_whitespace();
method = parts.next().unwrap_or("").to_string();
path = parts.next().unwrap_or("").to_string();
first = false;
continue;
}
let trimmed = line.trim();
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("content-length:") {
content_length = trimmed
.split(':')
.nth(1)
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
}
headers.push(trimmed.to_string());
}
Ok(RequestHead {
method,
path,
content_length,
headers,
})
}
pub(crate) async fn read_body<R: AsyncBufRead + Unpin>(
reader: &mut R,
len: usize,
deadline: Instant,
) -> Result<Vec<u8>> {
let mut buf = vec![0u8; len];
if len > 0 {
timeout_at(deadline, reader.read_exact(&mut buf))
.await
.map_err(|_| ProxyError::Network("request body read timeout".to_string()))?
.map_err(|e| ProxyError::Network(format!("request body read: {}", e)))?;
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_time_eq_basic() {
assert!(constant_time_eq_str("Bearer abc", "Bearer abc"));
assert!(!constant_time_eq_str("Bearer abc", "Bearer abd"));
assert!(!constant_time_eq_str("Bearer abc", "Bearer ab"));
assert!(!constant_time_eq_str("", "x"));
assert!(constant_time_eq_str("", ""));
}
#[tokio::test]
async fn read_head_bounds_headers() {
let mut big = String::from("POST /sql HTTP/1.1\r\n");
for i in 0..(MAX_HTTP_HEADERS + 5) {
big.push_str(&format!("X-Pad-{}: y\r\n", i));
}
big.push_str("\r\n");
let mut r = tokio::io::BufReader::new(big.as_bytes());
let deadline = Instant::now() + Duration::from_secs(5);
assert!(read_head(&mut r, deadline).await.is_err());
}
#[tokio::test]
async fn read_head_parses_and_exposes_headers() {
let req = "POST /sql HTTP/1.1\r\nContent-Length: 7\r\nAuthorization: Bearer tok\r\n\r\n";
let mut r = tokio::io::BufReader::new(req.as_bytes());
let deadline = Instant::now() + Duration::from_secs(5);
let head = read_head(&mut r, deadline).await.unwrap();
assert_eq!(head.method, "POST");
assert_eq!(head.path, "/sql");
assert_eq!(head.content_length, 7);
assert_eq!(head.header("authorization"), Some("Bearer tok"));
assert_eq!(head.header("x-missing"), None);
}
}