use std::future::Future;
use std::time::Instant;
use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
use tokio::net::TcpListener;
#[cfg(test)]
use tokio::net::TcpStream;
use zeroize::{Zeroize as _, Zeroizing};
#[derive(Debug)]
#[non_exhaustive]
pub enum LoopbackConnectionEvent<C, P> {
Callback(C),
Preflight(P),
}
pub trait BrowserLoopbackHooks {
type Error;
type Callback;
type Preflight;
type Output;
fn deadline(&self) -> Instant;
fn max_connections(&self) -> usize {
1
}
fn max_request_bytes(&self) -> usize {
8 * 1024
}
fn deadline_error(&self) -> Self::Error;
fn callback_error(&self) -> Self::Error;
fn parse_request(
&mut self,
request: &[u8],
local_port: u16,
) -> Result<LoopbackConnectionEvent<Self::Callback, Self::Preflight>, Self::Error>;
fn preflight_response_bytes(&self, preflight: &Self::Preflight) -> Vec<u8>;
fn callback_response_bytes(&self, complete: Result<&Self::Output, &Self::Error>) -> Vec<u8>;
fn complete_callback(
&mut self,
callback: Self::Callback,
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
}
pub async fn bind_loopback_listener(ports: &[u16]) -> std::io::Result<(TcpListener, u16)> {
let mut last = std::io::Error::from(std::io::ErrorKind::AddrNotAvailable);
for port in ports {
match TcpListener::bind(("127.0.0.1", *port)).await {
Ok(listener) => {
let bound = listener.local_addr()?.port();
return Ok((listener, bound));
}
Err(error) => last = error,
}
}
Err(last)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ReadRequestHeadError {
Deadline,
Invalid,
}
pub async fn read_http11_request_head(
stream: &mut (impl AsyncRead + Unpin),
max_bytes: usize,
deadline: Instant,
) -> Result<Zeroizing<Vec<u8>>, ReadRequestHeadError> {
let mut bytes = Zeroizing::new(Vec::with_capacity(1024.min(max_bytes)));
loop {
if bytes.len() >= max_bytes {
return Err(ReadRequestHeadError::Invalid);
}
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or(ReadRequestHeadError::Deadline)?;
let mut chunk = [0_u8; 512];
let read_limit = (max_bytes - bytes.len()).min(chunk.len());
let read = tokio::time::timeout(remaining, stream.read(&mut chunk[..read_limit]))
.await
.map_err(|_| ReadRequestHeadError::Deadline)?
.map_err(|_| ReadRequestHeadError::Invalid)?;
if read == 0 {
return Err(ReadRequestHeadError::Invalid);
}
bytes.extend_from_slice(&chunk[..read]);
chunk[..read].zeroize();
if let Some(end) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
let header_end = end + 4;
if bytes.len() != header_end {
return Err(ReadRequestHeadError::Invalid);
}
return Ok(bytes);
}
}
}
pub async fn write_all_before_deadline(
stream: &mut (impl AsyncWrite + Unpin),
bytes: &[u8],
deadline: Instant,
) -> std::io::Result<()> {
let timed_out = || std::io::Error::from(std::io::ErrorKind::TimedOut);
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or_else(timed_out)?;
tokio::time::timeout(remaining, async {
stream.write_all(bytes).await?;
stream.shutdown().await
})
.await
.map_err(|_| timed_out())?
}
pub async fn run_browser_loopback_session<H>(
listener: TcpListener,
hooks: &mut H,
) -> Result<H::Output, H::Error>
where
H: BrowserLoopbackHooks,
{
let local_port = listener
.local_addr()
.map_err(|_| hooks.callback_error())?
.port();
let max_connections = hooks.max_connections().max(1);
for _ in 0..max_connections {
let deadline = hooks.deadline();
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or_else(|| hooks.deadline_error())?;
let (mut stream, peer) = tokio::time::timeout(remaining, listener.accept())
.await
.map_err(|_| hooks.deadline_error())?
.map_err(|_| hooks.callback_error())?;
if !peer.ip().is_loopback() {
return Err(hooks.callback_error());
}
let request = match read_http11_request_head(
&mut stream,
hooks.max_request_bytes(),
hooks.deadline(),
)
.await
{
Ok(request) => request,
Err(kind) => {
let err = match kind {
ReadRequestHeadError::Deadline => hooks.deadline_error(),
ReadRequestHeadError::Invalid => hooks.callback_error(),
};
let page = hooks.callback_response_bytes(Err(&err));
let _ = write_all_before_deadline(&mut stream, &page, hooks.deadline()).await;
return Err(err);
}
};
match hooks.parse_request(&request, local_port) {
Ok(LoopbackConnectionEvent::Preflight(preflight)) => {
let page = hooks.preflight_response_bytes(&preflight);
write_all_before_deadline(&mut stream, &page, hooks.deadline())
.await
.map_err(|_| hooks.callback_error())?;
}
Ok(LoopbackConnectionEvent::Callback(callback)) => {
let outcome = hooks.complete_callback(callback).await;
let page = match &outcome {
Ok(output) => hooks.callback_response_bytes(Ok(output)),
Err(error) => hooks.callback_response_bytes(Err(error)),
};
let _ = write_all_before_deadline(&mut stream, &page, hooks.deadline()).await;
return outcome;
}
Err(error) => {
let page = hooks.callback_response_bytes(Err(&error));
let _ = write_all_before_deadline(&mut stream, &page, hooks.deadline()).await;
return Err(error);
}
}
}
Err(hooks.callback_error())
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
reason = "unit tests may assert with expect/unwrap"
)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn bind_loopback_uses_first_free_port() {
let reserved = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let reserved_port = reserved.local_addr().unwrap().port();
let free = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let free_port = free.local_addr().unwrap().port();
drop(free);
let (listener, bound) = bind_loopback_listener(&[reserved_port, free_port])
.await
.expect("second port free");
assert_eq!(bound, free_port);
drop(listener);
drop(reserved);
}
#[tokio::test]
async fn read_head_rejects_body_after_headers() {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let port = listener.local_addr().unwrap().port();
let client = tokio::spawn(async move {
let mut stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
stream
.write_all(b"GET /x HTTP/1.1\r\nHost: localhost\r\n\r\nbody")
.await
.unwrap();
});
let (mut stream, _) = listener.accept().await.unwrap();
let result = read_http11_request_head(
&mut stream,
8 * 1024,
Instant::now() + Duration::from_secs(2),
)
.await;
assert_eq!(result, Err(ReadRequestHeadError::Invalid));
let _ = client.await;
}
#[tokio::test]
async fn read_head_accepts_exact_headers() {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let port = listener.local_addr().unwrap().port();
let client = tokio::spawn(async move {
let mut stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
stream
.write_all(b"GET /callback?code=c&state=s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
.await
.unwrap();
});
let (mut stream, _) = listener.accept().await.unwrap();
let head = read_http11_request_head(
&mut stream,
8 * 1024,
Instant::now() + Duration::from_secs(2),
)
.await
.expect("headers");
assert!(head.ends_with(b"\r\n\r\n"));
assert!(
std::str::from_utf8(&head)
.unwrap()
.contains("GET /callback")
);
let _ = client.await;
}
}