use std::io::{BufRead as _, BufReader, Read as _, Write as _};
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::{Receiver, channel};
use std::thread::JoinHandle;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Received {
pub method: String,
pub target: String,
pub body: String,
}
pub struct Host {
address: String,
arrivals: Receiver<Received>,
serving: Option<JoinHandle<()>>,
}
impl Host {
pub fn answering(status: u16, body: &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 (sending, arrivals) = channel();
let answer = body.to_owned();
let serving = std::thread::spawn(move || {
if let Ok((stream, _)) = listener.accept()
&& let Some(received) = serve(stream, status, &answer)
{
let _ = sending.send(received);
}
});
Self {
address,
arrivals,
serving: Some(serving),
}
}
pub fn address(&self) -> &str {
&self.address
}
pub fn received(&self) -> Received {
self.arrivals
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the call reached this host")
}
pub fn requests(&self) -> usize {
self.arrivals.try_iter().count()
}
}
impl Drop for Host {
fn drop(&mut self) {
if let Some(serving) = self.serving.take() {
if serving.is_finished() {
let _ = serving.join();
}
}
}
}
fn serve(stream: TcpStream, status: u16, body: &str) -> Option<Received> {
let mut reader = BufReader::new(stream);
let mut request = String::new();
reader.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 length = 0_usize;
loop {
let mut line = String::new();
if reader.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);
}
}
let mut carried = vec![0_u8; length];
reader.read_exact(&mut carried).ok()?;
let answer = format!(
"HTTP/1.1 {status} STUB\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\
Connection: close\r\n\r\n{body}",
body.len()
);
let mut stream = reader.into_inner();
stream.write_all(answer.as_bytes()).ok()?;
stream.flush().ok()?;
Some(Received {
method,
target,
body: String::from_utf8_lossy(&carried).into_owned(),
})
}