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)]
45#[non_exhaustive]
46pub enum BrokerError {
47 #[error("secret `{0}` is empty")]
49 Empty(String),
50 #[error("secret `{0}` contains a control character")]
53 Control(String),
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Carried {
59 Placeholder,
61 Value,
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct Leak<'a> {
68 pub id: &'a str,
70 pub owner: &'a str,
72 pub carried: Carried,
74}
75
76impl Leak<'_> {
77 #[must_use]
79 pub fn decision(&self, host: &str) -> Decision {
80 let what = match self.carried {
81 Carried::Placeholder => "placeholder",
82 Carried::Value => "value",
83 };
84 Decision::single(
85 Verdict::Deny,
86 RULE_SECRET,
87 format!(
88 "request to {host} carries the {what} of secret `{}`, which only {} may receive",
89 self.id, self.owner
90 ),
91 )
92 }
93}
94
95impl Broker {
96 pub fn new(secrets: Vec<(Secret, Zeroizing<String>)>) -> Result<Self, BrokerError> {
99 let mut held = Vec::with_capacity(secrets.len());
100 for (secret, value) in secrets {
101 if value.is_empty() {
102 return Err(BrokerError::Empty(secret.id));
103 }
104 if value.chars().any(char::is_control) {
105 return Err(BrokerError::Control(secret.id));
106 }
107 held.push(Held {
108 placeholder: secret.placeholder(),
109 secret,
110 value,
111 });
112 }
113 Ok(Self { held })
114 }
115
116 #[must_use]
118 pub fn is_empty(&self) -> bool {
119 self.held.is_empty()
120 }
121
122 #[must_use]
125 pub fn leak(&self, host: &str, bytes: &[u8]) -> Option<Leak<'_>> {
126 self.held
127 .iter()
128 .filter(|h| h.secret.host != host)
129 .find_map(|h| {
130 let carried = if contains(bytes, h.placeholder.as_bytes()) {
131 Carried::Placeholder
132 } else if contains(bytes, h.value.as_bytes()) {
133 Carried::Value
134 } else {
135 return None;
136 };
137 Some(Leak {
138 id: &h.secret.id,
139 owner: &h.secret.host,
140 carried,
141 })
142 })
143 }
144
145 pub(crate) fn withheld(&self, host: &str) -> Vec<&str> {
148 self.held
149 .iter()
150 .filter(|h| h.secret.host == host && !h.secret.plain_http)
151 .map(|h| h.secret.id.as_str())
152 .collect()
153 }
154
155 pub(crate) fn inject(&self, host: &str, head: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
161 let owned: Vec<&Held> = self
162 .held
163 .iter()
164 .filter(|h| h.secret.host == host && h.secret.plain_http)
165 .collect();
166 let lines = head.strip_suffix(b"\n\r\n")?;
168 if owned.is_empty() {
169 return None;
170 }
171 let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
172 let mut placed = vec![false; owned.len()];
173 for (n, line) in lines.split(|b| *b == b'\n').enumerate() {
175 let named = header_name(line).filter(|_| n > 0).and_then(|name| {
176 owned
177 .iter()
178 .position(|h| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
179 });
180 match named {
181 None => out.extend_from_slice(line),
182 Some(i) if contains(line, owned[i].placeholder.as_bytes()) => {
183 placed[i] = true;
184 let h = owned[i];
185 replace(line, h.placeholder.as_bytes(), h.value.as_bytes(), &mut out);
186 }
187 Some(_) => continue,
190 }
191 out.extend_from_slice(b"\n");
192 }
193 for (h, _) in owned.iter().zip(&placed).filter(|(_, p)| !**p) {
194 out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
195 out.extend_from_slice(h.value.as_bytes());
196 out.extend_from_slice(b"\r\n");
197 }
198 out.extend_from_slice(b"\r\n");
199 Some(out)
200 }
201
202 fn longest(&self) -> usize {
206 self.held
207 .iter()
208 .map(|h| h.placeholder.len().max(h.value.len()))
209 .max()
210 .unwrap_or(0)
211 }
212}
213
214pub(crate) struct Watch<'a> {
216 broker: &'a Broker,
217 host: &'a str,
218 tail: Zeroizing<Vec<u8>>,
221}
222
223impl<'a> Watch<'a> {
224 pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
225 Self {
226 broker,
227 host,
228 tail: Zeroizing::new(Vec::new()),
229 }
230 }
231
232 pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
234 if self.broker.is_empty() {
235 return None;
236 }
237 let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
238 window.extend_from_slice(&self.tail);
239 window.extend_from_slice(chunk);
240 let keep = self.broker.longest().saturating_sub(1).min(window.len());
241 self.tail.clear();
242 self.tail.extend_from_slice(&window[window.len() - keep..]);
243 self.broker.leak(self.host, &window)
244 }
245}
246
247fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
249 while let Some(at) = line.windows(from.len()).position(|w| w == from) {
250 out.extend_from_slice(&line[..at]);
251 out.extend_from_slice(to);
252 line = &line[at + from.len()..];
253 }
254 out.extend_from_slice(line);
255}
256
257fn header_name(line: &[u8]) -> Option<&[u8]> {
259 line.iter().position(|b| *b == b':').map(|at| &line[..at])
260}
261
262fn contains(haystack: &[u8], needle: &[u8]) -> bool {
263 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
264}
265
266#[cfg(test)]
267mod tests;