use std::io::{BufRead as _, BufReader, Read as _, Write as _};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Exchange {
pub method: String,
pub target: String,
pub body: String,
pub status: u16,
pub answer: String,
}
pub struct Proxy {
address: String,
seen: Arc<Mutex<Vec<Exchange>>>,
}
impl Proxy {
pub fn in_front_of(server: &str) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("a loopback port");
let address = listener
.local_addr()
.expect("the bound address")
.to_string();
let seen = Arc::new(Mutex::new(Vec::new()));
let onward = server
.trim()
.trim_end_matches('/')
.strip_prefix("http://")
.unwrap_or(server)
.to_owned();
let recording = Arc::clone(&seen);
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
forward(stream, &onward, &recording);
}
});
Self { address, seen }
}
pub fn url(&self) -> String {
format!("http://{}", self.address)
}
pub fn last(&self) -> Exchange {
self.seen
.lock()
.expect("the recording is takeable")
.last()
.cloned()
.expect("a call reached the supervisor through this proxy")
}
pub fn calls(&self) -> usize {
self.seen.lock().expect("the recording is takeable").len()
}
}
fn forward(client: TcpStream, onward: &str, recording: &Arc<Mutex<Vec<Exchange>>>) -> Option<()> {
let mut reading = BufReader::new(client);
let mut request = String::new();
reading.read_line(&mut request).ok()?;
let mut words = request.split_whitespace();
let method = words.next()?.to_owned();
let target = words.next()?.to_owned();
let mut head = String::new();
let mut length = 0_usize;
loop {
let mut line = String::new();
if reading.read_line(&mut line).ok()? == 0 || line.trim().is_empty() {
break;
}
if let Some((name, value)) = line.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
length = value.trim().parse().unwrap_or(0);
}
head.push_str(&line);
}
let mut carried = vec![0_u8; length];
reading.read_exact(&mut carried).ok()?;
let body = String::from_utf8_lossy(&carried).into_owned();
let mut upstream = TcpStream::connect(onward).ok()?;
upstream
.write_all(format!("{method} {target} HTTP/1.1\r\n{head}\r\n{body}").as_bytes())
.ok()?;
let mut answered = Vec::new();
upstream.read_to_end(&mut answered).ok()?;
let separator = answered
.windows(4)
.position(|window| window == b"\r\n\r\n")?;
let answer_head = String::from_utf8_lossy(&answered[..separator]).into_owned();
let status = answer_head
.split_whitespace()
.nth(1)
.and_then(|code| code.parse::<u16>().ok())?;
recording
.lock()
.expect("the recording is takeable")
.push(Exchange {
method,
target,
body,
status,
answer: String::from_utf8_lossy(&answered[separator + 4..]).into_owned(),
});
let mut client = reading.into_inner();
client.write_all(&answered).ok()?;
client.flush().ok()?;
Some(())
}
pub fn matches<T: Serialize>(answered: &T, captured: &str) -> bool {
let sent: Value = serde_json::from_str(captured).expect("the supervisor answered a document");
serde_json::to_value(answered).expect("an answer renders back as a document") == sent
}
pub fn same<T: Serialize>(operation: &str, answered: &T, captured: &str) {
assert!(
matches(answered, captured),
"`{operation}` answered something other than what the supervisor sent.\n\
it answered: {}\nthe supervisor sent: {captured}",
serde_json::to_string(answered).unwrap_or_default()
);
}