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, port: u16) -> Vec<String> {
149 self.held
150 .iter()
151 .filter(|h| h.secret.host == host)
152 .filter_map(|h| {
153 let id = &h.secret.id;
154 if !h.secret.plain_http {
155 Some(format!(
156 "secret `{id}` not injected: plain HTTP needs `plain_http: true`"
157 ))
158 } else if h.secret.plain_http_port() != port {
159 Some(format!(
160 "secret `{id}` not injected: it goes to port {} only, not {port}",
161 h.secret.plain_http_port()
162 ))
163 } else {
164 None
165 }
166 })
167 .collect()
168 }
169
170 pub(crate) fn inject(&self, host: &str, port: u16, head: &[u8]) -> Option<Zeroizing<Vec<u8>>> {
177 let owned: Vec<&Held> = self
178 .held
179 .iter()
180 .filter(|h| {
181 h.secret.host == host && h.secret.plain_http && h.secret.plain_http_port() == port
182 })
183 .collect();
184 let lines = head.strip_suffix(b"\n\r\n")?;
186 if owned.is_empty() || echoed(head) {
187 return None;
188 }
189 let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
190 let mut placed = vec![false; owned.len()];
191 for (n, line) in lines.split(|b| *b == b'\n').enumerate() {
193 let named = header_name(line).filter(|_| n > 0).and_then(|name| {
194 owned
195 .iter()
196 .position(|h| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
197 });
198 match named {
199 None => out.extend_from_slice(line),
200 Some(i) if contains(line, owned[i].placeholder.as_bytes()) => {
201 placed[i] = true;
202 let h = owned[i];
203 replace(line, h.placeholder.as_bytes(), h.value.as_bytes(), &mut out);
204 }
205 Some(_) => continue,
208 }
209 out.extend_from_slice(b"\n");
210 }
211 for (h, _) in owned.iter().zip(&placed).filter(|(_, p)| !**p) {
212 out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
213 out.extend_from_slice(h.value.as_bytes());
214 out.extend_from_slice(b"\r\n");
215 }
216 out.extend_from_slice(b"\r\n");
217 Some(out)
218 }
219
220 fn longest(&self) -> usize {
224 self.held
225 .iter()
226 .map(|h| h.placeholder.len().max(h.value.len()))
227 .max()
228 .unwrap_or(0)
229 }
230}
231
232pub(crate) struct Watch<'a> {
234 broker: &'a Broker,
235 host: &'a str,
236 tail: Zeroizing<Vec<u8>>,
239}
240
241impl<'a> Watch<'a> {
242 pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
243 Self {
244 broker,
245 host,
246 tail: Zeroizing::new(Vec::new()),
247 }
248 }
249
250 pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
252 if self.broker.is_empty() {
253 return None;
254 }
255 let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
256 window.extend_from_slice(&self.tail);
257 window.extend_from_slice(chunk);
258 let keep = self.broker.longest().saturating_sub(1).min(window.len());
259 self.tail.clear();
260 self.tail.extend_from_slice(&window[window.len() - keep..]);
261 self.broker.leak(self.host, &window)
262 }
263}
264
265fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
267 while let Some(at) = line.windows(from.len()).position(|w| w == from) {
268 out.extend_from_slice(&line[..at]);
269 out.extend_from_slice(to);
270 line = &line[at + from.len()..];
271 }
272 out.extend_from_slice(line);
273}
274
275fn echoed(head: &[u8]) -> bool {
279 head.get(..6)
280 .is_some_and(|m| m.eq_ignore_ascii_case(b"TRACE ") || m.eq_ignore_ascii_case(b"TRACK "))
281}
282
283fn header_name(line: &[u8]) -> Option<&[u8]> {
285 line.iter().position(|b| *b == b':').map(|at| &line[..at])
286}
287
288fn contains(haystack: &[u8], needle: &[u8]) -> bool {
289 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
290}
291
292#[cfg(test)]
293mod tests;