1use openmoat_core::{Decision, Secret, Verdict};
20use zeroize::Zeroizing;
21
22pub const RULE_SECRET: &str = "proxy-secret";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) enum Channel {
30 Plain(u16),
32 Terminated,
34}
35
36struct Held {
38 secret: Secret,
39 placeholder: String,
40 value: Zeroizing<String>,
41}
42
43impl Held {
44 fn injects(&self, host: &str, channel: Channel) -> bool {
46 self.secret.host == host
47 && match channel {
48 Channel::Plain(port) => {
49 self.secret.plain_http && self.secret.plain_http_port() == port
50 }
51 Channel::Terminated => self.secret.terminates(),
52 }
53 }
54}
55
56#[derive(Default)]
58pub struct Broker {
59 held: Vec<Held>,
60}
61
62impl std::fmt::Debug for Broker {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_list()
65 .entries(self.held.iter().map(|h| (&h.secret.id, &h.secret.host)))
66 .finish()
67 }
68}
69
70#[derive(Debug, thiserror::Error)]
72#[non_exhaustive]
73pub enum BrokerError {
74 #[error("secret `{0}` is empty")]
76 Empty(String),
77 #[error("secret `{0}` contains a control character")]
80 Control(String),
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Carried {
86 Placeholder,
88 Value,
90}
91
92#[derive(Debug, Clone, Copy)]
94pub struct Leak<'a> {
95 pub id: &'a str,
97 pub owner: &'a str,
99 pub carried: Carried,
101}
102
103impl Leak<'_> {
104 #[must_use]
106 pub fn decision(&self, host: &str) -> Decision {
107 let what = match self.carried {
108 Carried::Placeholder => "placeholder",
109 Carried::Value => "value",
110 };
111 Decision::single(
112 Verdict::Deny,
113 RULE_SECRET,
114 format!(
115 "request to {host} carries the {what} of secret `{}`, which only {} may receive",
116 self.id, self.owner
117 ),
118 )
119 }
120}
121
122impl Broker {
123 pub fn new(secrets: Vec<(Secret, Zeroizing<String>)>) -> Result<Self, BrokerError> {
126 let mut held = Vec::with_capacity(secrets.len());
127 for (secret, value) in secrets {
128 if value.is_empty() {
129 return Err(BrokerError::Empty(secret.id));
130 }
131 if value.chars().any(char::is_control) {
132 return Err(BrokerError::Control(secret.id));
133 }
134 held.push(Held {
135 placeholder: secret.placeholder(),
136 secret,
137 value,
138 });
139 }
140 Ok(Self { held })
141 }
142
143 #[must_use]
145 pub fn is_empty(&self) -> bool {
146 self.held.is_empty()
147 }
148
149 pub(crate) fn terminates(&self, host: &str) -> bool {
151 self.held
152 .iter()
153 .any(|h| h.secret.host == host && h.secret.terminates())
154 }
155
156 #[must_use]
159 pub fn leak(&self, host: &str, bytes: &[u8]) -> Option<Leak<'_>> {
160 self.held
161 .iter()
162 .filter(|h| h.secret.host != host)
163 .find_map(|h| {
164 let carried = if contains(bytes, h.placeholder.as_bytes()) {
165 Carried::Placeholder
166 } else if contains(bytes, h.value.as_bytes()) {
167 Carried::Value
168 } else {
169 return None;
170 };
171 Some(Leak {
172 id: &h.secret.id,
173 owner: &h.secret.host,
174 carried,
175 })
176 })
177 }
178
179 pub(crate) fn withheld(&self, host: &str, port: u16) -> Vec<String> {
183 self.held
184 .iter()
185 .filter(|h| h.secret.host == host)
186 .filter_map(|h| {
187 let id = &h.secret.id;
188 if !h.secret.plain_http {
189 Some(format!(
190 "secret `{id}` not injected: plain HTTP needs `plain_http: true`"
191 ))
192 } else if h.secret.plain_http_port() != port {
193 Some(format!(
194 "secret `{id}` not injected: it goes to port {} only, not {port}",
195 h.secret.plain_http_port()
196 ))
197 } else {
198 None
199 }
200 })
201 .collect()
202 }
203
204 pub(crate) fn inject(
212 &self,
213 host: &str,
214 channel: Channel,
215 head: &[u8],
216 ) -> Option<Zeroizing<Vec<u8>>> {
217 let owned: Vec<&Held> = self
218 .held
219 .iter()
220 .filter(|h| h.injects(host, channel))
221 .collect();
222 let lines = head.strip_suffix(b"\n\r\n")?;
224 if owned.is_empty() || echoed(head) {
225 return None;
226 }
227 let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
228 let mut placed = vec![false; owned.len()];
229 for (n, line) in lines.split(|b| *b == b'\n').enumerate() {
231 let named = header_name(line).filter(|_| n > 0).and_then(|name| {
232 owned
233 .iter()
234 .position(|h| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
235 });
236 match named {
237 None => out.extend_from_slice(line),
238 Some(i) if contains(line, owned[i].placeholder.as_bytes()) => {
239 placed[i] = true;
240 let h = owned[i];
241 replace(line, h.placeholder.as_bytes(), h.value.as_bytes(), &mut out);
242 }
243 Some(_) => continue,
246 }
247 out.extend_from_slice(b"\n");
248 }
249 for (h, _) in owned.iter().zip(&placed).filter(|(_, p)| !**p) {
250 out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
251 out.extend_from_slice(h.value.as_bytes());
252 out.extend_from_slice(b"\r\n");
253 }
254 out.extend_from_slice(b"\r\n");
255 Some(out)
256 }
257
258 fn longest(&self) -> usize {
262 self.held
263 .iter()
264 .map(|h| h.placeholder.len().max(h.value.len()))
265 .max()
266 .unwrap_or(0)
267 }
268}
269
270pub(crate) struct Watch<'a> {
272 broker: &'a Broker,
273 host: &'a str,
274 tail: Zeroizing<Vec<u8>>,
277}
278
279impl<'a> Watch<'a> {
280 pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
281 Self {
282 broker,
283 host,
284 tail: Zeroizing::new(Vec::new()),
285 }
286 }
287
288 pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
290 if self.broker.is_empty() {
291 return None;
292 }
293 let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
294 window.extend_from_slice(&self.tail);
295 window.extend_from_slice(chunk);
296 let keep = self.broker.longest().saturating_sub(1).min(window.len());
297 self.tail.clear();
298 self.tail.extend_from_slice(&window[window.len() - keep..]);
299 self.broker.leak(self.host, &window)
300 }
301}
302
303fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
305 while let Some(at) = line.windows(from.len()).position(|w| w == from) {
306 out.extend_from_slice(&line[..at]);
307 out.extend_from_slice(to);
308 line = &line[at + from.len()..];
309 }
310 out.extend_from_slice(line);
311}
312
313fn echoed(head: &[u8]) -> bool {
317 head.get(..6)
318 .is_some_and(|m| m.eq_ignore_ascii_case(b"TRACE ") || m.eq_ignore_ascii_case(b"TRACK "))
319}
320
321fn header_name(line: &[u8]) -> Option<&[u8]> {
323 line.iter().position(|b| *b == b':').map(|at| &line[..at])
324}
325
326fn contains(haystack: &[u8], needle: &[u8]) -> bool {
327 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
328}
329
330mod echo;
331mod route;
332
333pub(crate) use self::echo::Holdback;
334pub use self::route::RULE_SECRET_ROUTE;
335
336#[cfg(test)]
337mod tests;