use openmoat_core::{Decision, Secret, Verdict};
use zeroize::Zeroizing;
pub const RULE_SECRET: &str = "proxy-secret";
struct Held {
secret: Secret,
placeholder: String,
value: Zeroizing<String>,
}
#[derive(Default)]
pub struct Broker {
held: Vec<Held>,
}
impl std::fmt::Debug for Broker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.held.iter().map(|h| (&h.secret.id, &h.secret.host)))
.finish()
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BrokerError {
#[error("secret `{0}` is empty")]
Empty(String),
#[error("secret `{0}` contains a control character")]
Control(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Carried {
Placeholder,
Value,
}
#[derive(Debug, Clone, Copy)]
pub struct Leak<'a> {
pub id: &'a str,
pub owner: &'a str,
pub carried: Carried,
}
impl Leak<'_> {
#[must_use]
pub fn decision(&self, host: &str) -> Decision {
let what = match self.carried {
Carried::Placeholder => "placeholder",
Carried::Value => "value",
};
Decision::single(
Verdict::Deny,
RULE_SECRET,
format!(
"request to {host} carries the {what} of secret `{}`, which only {} may receive",
self.id, self.owner
),
)
}
}
impl Broker {
pub fn new(secrets: Vec<(Secret, Zeroizing<String>)>) -> Result<Self, BrokerError> {
let mut held = Vec::with_capacity(secrets.len());
for (secret, value) in secrets {
if value.is_empty() {
return Err(BrokerError::Empty(secret.id));
}
if value.chars().any(char::is_control) {
return Err(BrokerError::Control(secret.id));
}
held.push(Held {
placeholder: secret.placeholder(),
secret,
value,
});
}
Ok(Self { held })
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.held.is_empty()
}
#[must_use]
pub fn leak(&self, host: &str, bytes: &[u8]) -> Option<Leak<'_>> {
self.held
.iter()
.filter(|h| h.secret.host != host)
.find_map(|h| {
let carried = if contains(bytes, h.placeholder.as_bytes()) {
Carried::Placeholder
} else if contains(bytes, h.value.as_bytes()) {
Carried::Value
} else {
return None;
};
Some(Leak {
id: &h.secret.id,
owner: &h.secret.host,
carried,
})
})
}
pub(crate) fn withheld(&self, host: &str) -> Vec<&str> {
self.held
.iter()
.filter(|h| h.secret.host == host && !h.secret.plain_http)
.map(|h| h.secret.id.as_str())
.collect()
}
pub(crate) fn inject(&self, host: &str, head: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
let owned: Vec<&Held> = self
.held
.iter()
.filter(|h| h.secret.host == host && h.secret.plain_http)
.collect();
let lines = head.strip_suffix(b"\n\r\n")?;
if owned.is_empty() {
return None;
}
let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
let mut placed = vec![false; owned.len()];
for (n, line) in lines.split(|b| *b == b'\n').enumerate() {
let named = header_name(line).filter(|_| n > 0).and_then(|name| {
owned
.iter()
.position(|h| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
});
match named {
None => out.extend_from_slice(line),
Some(i) if contains(line, owned[i].placeholder.as_bytes()) => {
placed[i] = true;
let h = owned[i];
replace(line, h.placeholder.as_bytes(), h.value.as_bytes(), &mut out);
}
Some(_) => continue,
}
out.extend_from_slice(b"\n");
}
for (h, _) in owned.iter().zip(&placed).filter(|(_, p)| !**p) {
out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
out.extend_from_slice(h.value.as_bytes());
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(b"\r\n");
Some(out)
}
fn longest(&self) -> usize {
self.held
.iter()
.map(|h| h.placeholder.len().max(h.value.len()))
.max()
.unwrap_or(0)
}
}
pub(crate) struct Watch<'a> {
broker: &'a Broker,
host: &'a str,
tail: Zeroizing<Vec<u8>>,
}
impl<'a> Watch<'a> {
pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
Self {
broker,
host,
tail: Zeroizing::new(Vec::new()),
}
}
pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
if self.broker.is_empty() {
return None;
}
let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
window.extend_from_slice(&self.tail);
window.extend_from_slice(chunk);
let keep = self.broker.longest().saturating_sub(1).min(window.len());
self.tail.clear();
self.tail.extend_from_slice(&window[window.len() - keep..]);
self.broker.leak(self.host, &window)
}
}
fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
while let Some(at) = line.windows(from.len()).position(|w| w == from) {
out.extend_from_slice(&line[..at]);
out.extend_from_slice(to);
line = &line[at + from.len()..];
}
out.extend_from_slice(line);
}
fn header_name(line: &[u8]) -> Option<&[u8]> {
line.iter().position(|b| *b == b':').map(|at| &line[..at])
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
!needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
}
#[cfg(test)]
mod tests;