1use openmoat_core::{Decision, Secret, Verdict};
16use zeroize::Zeroizing;
17
18pub const RULE_SECRET: &str = "proxy-secret";
21
22struct Held {
24 secret: Secret,
25 placeholder: String,
26 value: Zeroizing<String>,
27}
28
29#[derive(Default)]
31pub struct Broker {
32 held: Vec<Held>,
33}
34
35impl std::fmt::Debug for Broker {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_list()
38 .entries(self.held.iter().map(|h| (&h.secret.id, &h.secret.host)))
39 .finish()
40 }
41}
42
43#[derive(Debug, thiserror::Error)]
45pub enum BrokerError {
46 #[error("secret `{0}` is empty")]
48 Empty(String),
49 #[error("secret `{0}` contains a control character")]
52 Control(String),
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Carried {
58 Placeholder,
60 Value,
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct Leak<'a> {
67 pub id: &'a str,
69 pub owner: &'a str,
71 pub carried: Carried,
73}
74
75impl Leak<'_> {
76 #[must_use]
78 pub fn decision(&self, host: &str) -> Decision {
79 let what = match self.carried {
80 Carried::Placeholder => "placeholder",
81 Carried::Value => "value",
82 };
83 Decision::single(
84 Verdict::Deny,
85 RULE_SECRET,
86 format!(
87 "request to {host} carries the {what} of secret `{}`, which only {} may receive",
88 self.id, self.owner
89 ),
90 )
91 }
92}
93
94impl Broker {
95 pub fn new(secrets: Vec<(Secret, Zeroizing<String>)>) -> Result<Self, BrokerError> {
98 let mut held = Vec::with_capacity(secrets.len());
99 for (secret, value) in secrets {
100 if value.is_empty() {
101 return Err(BrokerError::Empty(secret.id));
102 }
103 if value.chars().any(char::is_control) {
104 return Err(BrokerError::Control(secret.id));
105 }
106 held.push(Held {
107 placeholder: secret.placeholder(),
108 secret,
109 value,
110 });
111 }
112 Ok(Self { held })
113 }
114
115 #[must_use]
117 pub fn is_empty(&self) -> bool {
118 self.held.is_empty()
119 }
120
121 #[must_use]
124 pub fn leak(&self, host: &str, bytes: &[u8]) -> Option<Leak<'_>> {
125 self.held
126 .iter()
127 .filter(|h| h.secret.host != host)
128 .find_map(|h| {
129 let carried = if contains(bytes, h.placeholder.as_bytes()) {
130 Carried::Placeholder
131 } else if contains(bytes, h.value.as_bytes()) {
132 Carried::Value
133 } else {
134 return None;
135 };
136 Some(Leak {
137 id: &h.secret.id,
138 owner: &h.secret.host,
139 carried,
140 })
141 })
142 }
143
144 pub(crate) fn withheld(&self, host: &str) -> Vec<&str> {
147 self.held
148 .iter()
149 .filter(|h| h.secret.host == host && !h.secret.plain_http)
150 .map(|h| h.secret.id.as_str())
151 .collect()
152 }
153
154 pub(crate) fn inject(&self, host: &str, head: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
159 let owned: Vec<&Held> = self
160 .held
161 .iter()
162 .filter(|h| h.secret.host == host && h.secret.plain_http)
163 .collect();
164 let lines = head.strip_suffix(b"\n\r\n")?;
166 if owned.is_empty() {
167 return None;
168 }
169 let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
170 let mut present = vec![false; owned.len()];
171 let mut lines = lines.split(|b| *b == b'\n');
173 out.extend_from_slice(lines.next().unwrap_or_default());
174 for line in lines {
175 out.extend_from_slice(b"\n");
176 let name = line.split(|b| *b == b':').next().unwrap_or_default();
177 match owned
178 .iter()
179 .position(|h| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
180 {
181 Some(i) => {
182 present[i] = true;
183 let h = owned[i];
184 replace(line, h.placeholder.as_bytes(), h.value.as_bytes(), &mut out);
185 }
186 None => out.extend_from_slice(line),
187 }
188 }
189 out.extend_from_slice(b"\n");
190 for (h, _) in owned.iter().zip(&present).filter(|(_, p)| !**p) {
191 out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
192 out.extend_from_slice(h.value.as_bytes());
193 out.extend_from_slice(b"\r\n");
194 }
195 out.extend_from_slice(b"\r\n");
196 Some(out)
197 }
198
199 fn longest(&self) -> usize {
203 self.held
204 .iter()
205 .map(|h| h.placeholder.len().max(h.value.len()))
206 .max()
207 .unwrap_or(0)
208 }
209}
210
211pub(crate) struct Watch<'a> {
213 broker: &'a Broker,
214 host: &'a str,
215 tail: Zeroizing<Vec<u8>>,
218}
219
220impl<'a> Watch<'a> {
221 pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
222 Self {
223 broker,
224 host,
225 tail: Zeroizing::new(Vec::new()),
226 }
227 }
228
229 pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
231 if self.broker.is_empty() {
232 return None;
233 }
234 let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
235 window.extend_from_slice(&self.tail);
236 window.extend_from_slice(chunk);
237 let keep = self.broker.longest().saturating_sub(1).min(window.len());
238 self.tail.clear();
239 self.tail.extend_from_slice(&window[window.len() - keep..]);
240 self.broker.leak(self.host, &window)
241 }
242}
243
244fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
246 while let Some(at) = line.windows(from.len()).position(|w| w == from) {
247 out.extend_from_slice(&line[..at]);
248 out.extend_from_slice(to);
249 line = &line[at + from.len()..];
250 }
251 out.extend_from_slice(line);
252}
253
254fn contains(haystack: &[u8], needle: &[u8]) -> bool {
255 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
256}
257
258#[cfg(test)]
259mod tests;