use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
task::Poll,
time::Duration,
};
use crate::{Error, Result, SessionError};
pub(crate) const MAX_URI: usize = 8192;
const MAX_TIMEOUT_MS: u64 = (1 << 62) - 1;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Goaway {
pub(crate) uri: String,
pub(crate) timeout: Option<Duration>,
}
impl Goaway {
pub fn new() -> Self {
Self::default()
}
pub fn redirect(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
timeout: None,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
let millis = timeout.as_nanos().div_ceil(1_000_000).min(u128::from(MAX_TIMEOUT_MS)) as u64;
self.timeout = (millis != 0).then(|| Duration::from_millis(millis));
self
}
pub fn uri(&self) -> &str {
&self.uri
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
}
pub struct Producer {
trigger: kio::Producer<Option<Goaway>>,
redirect: bool,
}
impl Producer {
pub fn send(self, goaway: Goaway) -> Result<()> {
if !self.redirect && !goaway.uri.is_empty() {
return Err(Error::ProtocolViolation);
}
if goaway.uri.len() > MAX_URI {
return Err(Error::ProtocolViolation);
}
if let Ok(mut state) = self.trigger.write() {
if state.is_some() {
return Err(Error::Duplicate);
}
*state = Some(goaway);
}
Ok(())
}
}
#[derive(Clone)]
pub struct Consumer {
state: kio::Consumer<Option<Goaway>>,
}
impl Consumer {
pub fn peek(&self) -> Option<Goaway> {
self.state.read().clone()
}
pub fn poll(&self, waiter: &kio::Waiter) -> Poll<Result<Goaway>> {
match self.state.poll(waiter, |state| match &**state {
Some(goaway) => Poll::Ready(goaway.clone()),
None => Poll::Pending,
}) {
Poll::Ready(Ok(goaway)) => Poll::Ready(Ok(goaway)),
Poll::Ready(Err(_)) => Poll::Ready(Err(Error::Closed)),
Poll::Pending => Poll::Pending,
}
}
pub async fn recv(&self) -> Option<Goaway> {
kio::wait(|waiter| self.poll(waiter)).await.ok()
}
}
#[derive(Clone)]
pub(crate) struct GoingAway {
flag: Arc<AtomicBool>,
received: Consumer,
}
impl GoingAway {
pub fn set(&self) -> bool {
!self.flag.swap(true, Ordering::AcqRel)
}
pub fn is_set(&self) -> bool {
self.flag.load(Ordering::Acquire)
}
pub fn poll(&self, waiter: &kio::Waiter) -> Poll<()> {
match self.received.state.poll(waiter, |state| match &**state {
Some(_) => Poll::Ready(()),
None => Poll::Pending,
}) {
Poll::Ready(Ok(())) => Poll::Ready(()),
Poll::Ready(Err(_)) => Poll::Pending,
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
impl Default for GoingAway {
fn default() -> Self {
let received = kio::Producer::new(None);
Self {
flag: Default::default(),
received: Consumer {
state: received.consume(),
},
}
}
}
#[derive(Clone)]
pub(crate) struct Protocol {
trigger: kio::Consumer<Option<Goaway>>,
received: kio::Producer<Option<Goaway>>,
pub going_away: GoingAway,
}
impl Protocol {
pub fn record(&self, goaway: Goaway) -> Result<()> {
if !self.going_away.set() {
return Err(Error::ProtocolViolation);
}
if let Ok(mut state) = self.received.write() {
*state = Some(goaway);
}
Ok(())
}
pub fn poll_triggered(&self, waiter: &kio::Waiter) -> Poll<Option<Goaway>> {
match self.trigger.poll(waiter, |state| match &**state {
Some(goaway) => Poll::Ready(goaway.clone()),
None => Poll::Pending,
}) {
Poll::Ready(Ok(goaway)) => Poll::Ready(Some(goaway)),
Poll::Ready(Err(_)) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
pub(crate) struct Enforce<S: crate::transport::poll::Session> {
session: S,
deadline: Option<(crate::runtime::Deadline<crate::time::Clock>, Duration)>,
}
impl<S: crate::transport::poll::Session> Enforce<S> {
pub fn new(runtime: &crate::time::Clock, session: S, timeout: Option<Duration>) -> Self {
Self {
session,
deadline: timeout.map(|timeout| (crate::runtime::Deadline::after(runtime, timeout), timeout)),
}
}
pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
let Some((deadline, timeout)) = &mut self.deadline else {
return Poll::Ready(());
};
let mut cx = std::task::Context::from_waker(waiter.waker());
if self.session.poll_closed(&mut cx).is_ready() {
return Poll::Ready(());
}
std::task::ready!(deadline.poll(waiter));
tracing::warn!(?timeout, "peer did not leave before the GOAWAY deadline; closing");
self.session
.close(SessionError::GoawayTimeout.to_code(), &Error::GoawayTimeout.to_string());
Poll::Ready(())
}
}
pub(crate) async fn enforce<S: crate::transport::poll::Session>(
runtime: &crate::time::Clock,
session: &mut S,
timeout: Option<Duration>,
) {
let mut enforce = Enforce::new(runtime, session.clone(), timeout);
kio::wait(|waiter| enforce.poll(waiter)).await
}
pub(crate) struct Handle {
trigger: kio::Producer<Option<Goaway>>,
redirect: bool,
consumer: Consumer,
}
impl Handle {
pub fn new(redirect: bool) -> (Self, Protocol) {
let trigger = kio::Producer::new(None);
let received = kio::Producer::new(None);
let consumer = Consumer {
state: received.consume(),
};
let going_away = GoingAway {
flag: Default::default(),
received: consumer.clone(),
};
let handle = Self {
trigger: trigger.clone(),
redirect,
consumer,
};
let protocol = Protocol {
trigger: trigger.consume(),
received,
going_away,
};
(handle, protocol)
}
pub fn producer(&self) -> Producer {
Producer {
trigger: self.trigger.clone(),
redirect: self.redirect,
}
}
pub fn consumer(&self) -> Consumer {
self.consumer.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_first_send_wins() {
let (handle, protocol) = Handle::new(true);
handle
.producer()
.send(Goaway::redirect("https://first.example/"))
.unwrap();
let err = handle
.producer()
.send(Goaway::redirect("https://second.example/"))
.unwrap_err();
assert!(matches!(err, Error::Duplicate));
assert_eq!(
protocol.trigger.read().as_ref().map(|g| g.uri.as_str()),
Some("https://first.example/"),
"the peer must not see a URI replaced out from under it"
);
}
#[test]
fn client_redirect_is_refused() {
let (handle, _protocol) = Handle::new(false);
let err = handle
.producer()
.send(Goaway {
uri: "https://elsewhere.example/".to_string(),
..Default::default()
})
.unwrap_err();
assert!(matches!(err, Error::ProtocolViolation));
let (handle, _protocol) = Handle::new(false);
handle.producer().send(Goaway::default()).unwrap();
}
#[test]
fn timeout_matches_wire_milliseconds() {
assert_eq!(Goaway::new().with_timeout(Duration::ZERO).timeout(), None);
assert_eq!(
Goaway::new().with_timeout(Duration::from_micros(1)).timeout(),
Some(Duration::from_millis(1))
);
assert_eq!(
Goaway::new().with_timeout(Duration::MAX).timeout(),
Some(Duration::from_millis(MAX_TIMEOUT_MS))
);
}
#[test]
fn oversized_uri_is_refused() {
let (handle, _protocol) = Handle::new(true);
let err = handle
.producer()
.send(Goaway::redirect("x".repeat(MAX_URI + 1)))
.unwrap_err();
assert!(matches!(err, Error::ProtocolViolation));
let (handle, _protocol) = Handle::new(true);
handle
.producer()
.send(Goaway::redirect("x".repeat(MAX_URI)))
.expect("exactly at the cap is fine");
}
#[test]
fn duplicate_is_a_protocol_violation() {
let (_handle, protocol) = Handle::new(true);
protocol.record(Goaway::default()).unwrap();
let err = protocol.record(Goaway::default()).unwrap_err();
assert!(matches!(err, Error::ProtocolViolation));
}
#[tokio::test]
async fn consumer_observes_the_recorded_goaway() {
let (handle, protocol) = Handle::new(true);
let consumer = handle.consumer();
assert_eq!(consumer.peek(), None);
let goaway = Goaway {
uri: "https://elsewhere.example/".to_string(),
timeout: Some(Duration::from_secs(5)),
};
protocol.record(goaway.clone()).unwrap();
assert_eq!(consumer.peek(), Some(goaway.clone()));
assert_eq!(consumer.recv().await, Some(goaway));
}
#[tokio::test]
async fn recv_resolves_when_the_session_closes() {
let (handle, protocol) = Handle::new(true);
let consumer = handle.consumer();
drop(protocol);
assert_eq!(consumer.recv().await, None);
}
}