use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
use openmoat_core::{Decision, Verdict};
use crate::audit::{Connection, Recorder};
pub const RULE_CAPACITY: &str = "proxy-capacity";
pub(super) const INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Full {
Slots,
Pending,
}
impl Full {
pub(super) fn reply(self) -> &'static str {
match self {
Self::Slots => "too many connections",
Self::Pending => "too many connections waiting to authenticate",
}
}
}
#[derive(Debug, Default)]
struct Kind {
last: Option<Instant>,
unrecorded: u64,
}
#[derive(Debug, Default)]
pub(super) struct Refusals {
kinds: Mutex<[Kind; 2]>,
}
impl Refusals {
pub(super) fn refused(&self, full: Full, recorder: &dyn Recorder, now: Instant) {
let due = {
let mut kinds = self.kinds.lock().unwrap_or_else(PoisonError::into_inner);
let kind = &mut kinds[full as usize];
kind.unrecorded += 1;
take_due(kind, now)
};
if let Some(count) = due {
write(full, count, recorder);
}
}
pub(super) fn flush(&self, recorder: &dyn Recorder, now: Instant) {
let due: Vec<(Full, u64)> = {
let mut kinds = self.kinds.lock().unwrap_or_else(PoisonError::into_inner);
[Full::Slots, Full::Pending]
.into_iter()
.filter_map(|full| {
let kind = &mut kinds[full as usize];
(kind.unrecorded > 0)
.then(|| take_due(kind, now))
.flatten()
.map(|n| (full, n))
})
.collect()
};
for (full, count) in due {
write(full, count, recorder);
}
}
}
fn take_due(kind: &mut Kind, now: Instant) -> Option<u64> {
let due = kind
.last
.is_none_or(|last| now.saturating_duration_since(last) >= INTERVAL);
if !due {
return None;
}
kind.last = Some(now);
Some(std::mem::take(&mut kind.unrecorded))
}
fn write(full: Full, count: u64, recorder: &dyn Recorder) {
let plural = if count == 1 { "" } else { "s" };
let decision = Decision::single(
Verdict::Deny,
RULE_CAPACITY,
format!(
"{count} connection{plural} refused before a request was read: {}; later ones \
within {} s are counted in the next row",
full.reply(),
INTERVAL.as_secs()
),
);
let connection = Connection {
session: None,
method: None,
destination: None,
decision: &decision,
latency_us: 0,
};
if let Err(error) = recorder.record(&connection) {
recorder.unrecorded(&error);
}
}
#[cfg(test)]
mod tests;