use std::io::{ErrorKind, Read as _, Write as _};
use std::net::{Shutdown, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;
use std::time::{Duration, Instant};
const BUFFER: usize = 16 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Forward {
All,
Part(usize),
Stop,
}
pub(crate) type Inspect<'a> = &'a mut dyn FnMut(&[u8]) -> Forward;
#[derive(Clone, Copy)]
pub(crate) struct Recheck<'a> {
pub(crate) every: Duration,
pub(crate) still_allowed: &'a (dyn Fn() -> bool + Sync),
}
pub(crate) fn relay(
client: &TcpStream,
upstream: &TcpStream,
idle: Duration,
inspect: Option<Inspect<'_>>,
recheck: Option<Recheck<'_>>,
) {
let wait = recheck.map_or(idle, |r| r.every.min(idle));
for s in [client, upstream] {
if s.set_read_timeout(Some(wait)).is_err() || s.set_write_timeout(Some(idle)).is_err() {
close(client, upstream);
return;
}
}
let activity = Activity::new();
let checks = Checks::new(recheck);
thread::scope(|scope| {
let back = thread::Builder::new().spawn_scoped(scope, || {
copy(upstream, client, &activity, &checks, idle, None);
});
if back.is_err() {
close(client, upstream);
return;
}
copy(client, upstream, &activity, &checks, idle, inspect);
});
}
fn copy(
from: &TcpStream,
to: &TcpStream,
activity: &Activity,
checks: &Checks<'_>,
idle: Duration,
mut inspect: Option<Inspect<'_>>,
) {
let mut buf = [0u8; BUFFER];
let mut dropping = false;
loop {
let read = (&*from).read(&mut buf);
if !checks.still_allowed(activity) {
break;
}
match read {
Ok(0) => {
let _ = to.shutdown(Shutdown::Write);
return;
}
Ok(_) if dropping => {}
Ok(n) => {
let forward = inspect
.as_mut()
.map_or(Forward::All, |pass| pass(&buf[..n]));
let send = match forward {
Forward::All => n,
Forward::Part(k) => {
dropping = true;
k.min(n)
}
Forward::Stop => break,
};
if (&*to).write_all(&buf[..send]).is_err() {
break;
}
activity.touch();
}
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e)
if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut)
&& activity.idle_for() < idle => {}
Err(_) => break,
}
}
close(from, to);
}
fn close(a: &TcpStream, b: &TcpStream) {
let _ = a.shutdown(Shutdown::Both);
let _ = b.shutdown(Shutdown::Both);
}
struct Checks<'a> {
recheck: Option<Recheck<'a>>,
last_ms: AtomicU64,
refused: AtomicBool,
}
impl<'a> Checks<'a> {
fn new(recheck: Option<Recheck<'a>>) -> Self {
Self {
recheck,
last_ms: AtomicU64::new(0),
refused: AtomicBool::new(false),
}
}
fn still_allowed(&self, activity: &Activity) -> bool {
let Some(recheck) = self.recheck else {
return true;
};
if self.refused.load(Ordering::Acquire) {
return false;
}
let now = activity.now_ms();
let last = self.last_ms.load(Ordering::Acquire);
let every = u64::try_from(recheck.every.as_millis()).unwrap_or(u64::MAX);
let due = now.saturating_sub(last) >= every;
if due
&& self
.last_ms
.compare_exchange(last, now, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
&& !(recheck.still_allowed)()
{
self.refused.store(true, Ordering::Release);
return false;
}
true
}
}
struct Activity {
start: Instant,
last_ms: AtomicU64,
}
impl Activity {
fn new() -> Self {
Self {
start: Instant::now(),
last_ms: AtomicU64::new(0),
}
}
fn now_ms(&self) -> u64 {
u64::try_from(self.start.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn touch(&self) {
self.last_ms.store(self.now_ms(), Ordering::Relaxed);
}
fn idle_for(&self) -> Duration {
Duration::from_millis(
self.now_ms()
.saturating_sub(self.last_ms.load(Ordering::Relaxed)),
)
}
}