1use std::sync::{Arc, RwLock};
20
21use openmoat_core::{Decision, Secret, Verdict};
22use zeroize::Zeroizing;
23
24use self::value::{Slot, read};
25
26pub const RULE_SECRET: &str = "proxy-secret";
29
30pub const RULE_SECRET_EXPIRED: &str = "proxy-secret-expired";
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub(crate) enum Channel {
39 Plain(u16),
41 Terminated,
43}
44
45struct Held {
47 secret: Secret,
48 placeholder: String,
49 slot: Arc<RwLock<Slot>>,
50}
51
52impl Held {
53 fn live(&self) -> Option<Zeroizing<String>> {
55 read(&self.slot).live().cloned()
56 }
57
58 fn value_in(&self, bytes: &[u8]) -> bool {
60 read(&self.slot)
61 .known()
62 .any(|v| contains(bytes, v.as_bytes()))
63 }
64
65 fn longest_value(&self) -> usize {
67 read(&self.slot).known().map(|v| v.len()).max().unwrap_or(0)
68 }
69
70 fn injects(&self, host: &str, channel: Channel) -> bool {
72 self.secret.host == host
73 && match channel {
74 Channel::Plain(port) => {
75 self.secret.plain_http && self.secret.plain_http_port() == port
76 }
77 Channel::Terminated => self.secret.terminates(),
78 }
79 }
80}
81
82#[derive(Default)]
84pub struct Broker {
85 held: Vec<Held>,
86}
87
88impl std::fmt::Debug for Broker {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_list()
91 .entries(self.held.iter().map(|h| (&h.secret.id, &h.secret.host)))
92 .finish()
93 }
94}
95
96#[derive(Debug, thiserror::Error)]
98#[non_exhaustive]
99pub enum BrokerError {
100 #[error("secret `{0}` is empty")]
102 Empty(String),
103 #[error("secret `{0}` contains a control character")]
106 Control(String),
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Carried {
112 Placeholder,
114 Value,
116}
117
118#[derive(Debug, Clone, Copy)]
120pub struct Leak<'a> {
121 pub id: &'a str,
123 pub owner: &'a str,
125 pub carried: Carried,
127}
128
129impl Leak<'_> {
130 #[must_use]
132 pub fn decision(&self, host: &str) -> Decision {
133 let what = match self.carried {
134 Carried::Placeholder => "placeholder",
135 Carried::Value => "value",
136 };
137 Decision::single(
138 Verdict::Deny,
139 RULE_SECRET,
140 format!(
141 "request to {host} carries the {what} of secret `{}`, which only {} may receive",
142 self.id, self.owner
143 ),
144 )
145 }
146}
147
148impl Broker {
149 pub fn new(secrets: Vec<(Secret, Zeroizing<String>)>) -> Result<Self, BrokerError> {
152 let mut held = Vec::with_capacity(secrets.len());
153 for (secret, value) in secrets {
154 usable(&secret.id, &value)?;
155 held.push(Held {
156 placeholder: secret.placeholder(),
157 secret,
158 slot: Slot::fixed(value),
159 });
160 }
161 Ok(Self { held })
162 }
163
164 #[must_use]
167 pub fn rotation(&self, id: &str) -> Option<Rotation> {
168 self.held
169 .iter()
170 .find(|h| h.secret.id == id)
171 .map(|h| Rotation::new(id, &h.slot))
172 }
173
174 #[must_use]
176 pub fn is_empty(&self) -> bool {
177 self.held.is_empty()
178 }
179
180 pub(crate) fn terminates(&self, host: &str) -> bool {
182 self.held
183 .iter()
184 .any(|h| h.secret.host == host && h.secret.terminates())
185 }
186
187 #[must_use]
190 pub fn leak(&self, host: &str, bytes: &[u8]) -> Option<Leak<'_>> {
191 self.held
192 .iter()
193 .filter(|h| h.secret.host != host)
194 .find_map(|h| {
195 let carried = if contains(bytes, h.placeholder.as_bytes()) {
196 Carried::Placeholder
197 } else if h.value_in(bytes) {
198 Carried::Value
199 } else {
200 return None;
201 };
202 Some(Leak {
203 id: &h.secret.id,
204 owner: &h.secret.host,
205 carried,
206 })
207 })
208 }
209
210 pub(crate) fn withheld(&self, host: &str, port: u16) -> Vec<String> {
214 self.held
215 .iter()
216 .filter(|h| h.secret.host == host)
217 .filter_map(|h| {
218 let id = &h.secret.id;
219 if !h.secret.plain_http {
220 Some(format!(
221 "secret `{id}` not injected: plain HTTP needs `plain_http: true`"
222 ))
223 } else if h.secret.plain_http_port() != port {
224 Some(format!(
225 "secret `{id}` not injected: it goes to port {} only, not {port}",
226 h.secret.plain_http_port()
227 ))
228 } else {
229 None
230 }
231 })
232 .collect()
233 }
234
235 pub(crate) fn inject(
244 &self,
245 host: &str,
246 channel: Channel,
247 head: &[u8],
248 ) -> Option<Zeroizing<Vec<u8>>> {
249 let owned: Vec<(&Held, Zeroizing<String>)> = self
250 .held
251 .iter()
252 .filter(|h| h.injects(host, channel))
253 .filter_map(|h| Some((h, h.live()?)))
254 .collect();
255 let lines = head.strip_suffix(b"\n\r\n")?;
257 if owned.is_empty() || echoed(head) {
258 return None;
259 }
260 let mut out = Zeroizing::new(Vec::with_capacity(head.len() + 256));
261 let mut placed = vec![false; owned.len()];
262 for (n, line) in lines.split(|b| *b == b'\n').enumerate() {
264 let named = header_name(line).filter(|_| n > 0).and_then(|name| {
265 owned
266 .iter()
267 .position(|(h, _)| name.eq_ignore_ascii_case(h.secret.header.as_bytes()))
268 });
269 match named {
270 None => out.extend_from_slice(line),
271 Some(i) if contains(line, owned[i].0.placeholder.as_bytes()) => {
272 placed[i] = true;
273 let (h, value) = &owned[i];
274 replace(line, h.placeholder.as_bytes(), value.as_bytes(), &mut out);
275 }
276 Some(_) => continue,
279 }
280 out.extend_from_slice(b"\n");
281 }
282 for ((h, value), _) in owned.iter().zip(&placed).filter(|(_, p)| !**p) {
283 out.extend_from_slice(format!("{}: ", h.secret.header).as_bytes());
284 out.extend_from_slice(value.as_bytes());
285 out.extend_from_slice(b"\r\n");
286 }
287 out.extend_from_slice(b"\r\n");
288 Some(out)
289 }
290
291 fn longest(&self) -> usize {
295 self.held
296 .iter()
297 .map(|h| h.placeholder.len().max(h.longest_value()))
298 .max()
299 .unwrap_or(0)
300 }
301}
302
303fn usable(id: &str, value: &str) -> Result<(), BrokerError> {
305 if value.is_empty() {
306 return Err(BrokerError::Empty(id.to_owned()));
307 }
308 if value.chars().any(char::is_control) {
309 return Err(BrokerError::Control(id.to_owned()));
310 }
311 Ok(())
312}
313
314pub(crate) struct Watch<'a> {
316 broker: &'a Broker,
317 host: &'a str,
318 tail: Zeroizing<Vec<u8>>,
321}
322
323impl<'a> Watch<'a> {
324 pub(crate) fn new(broker: &'a Broker, host: &'a str) -> Self {
325 Self {
326 broker,
327 host,
328 tail: Zeroizing::new(Vec::new()),
329 }
330 }
331
332 pub(crate) fn next(&mut self, chunk: &[u8]) -> Option<Leak<'a>> {
334 if self.broker.is_empty() {
335 return None;
336 }
337 let mut window = Zeroizing::new(Vec::with_capacity(self.tail.len() + chunk.len()));
338 window.extend_from_slice(&self.tail);
339 window.extend_from_slice(chunk);
340 let keep = self.broker.longest().saturating_sub(1).min(window.len());
341 self.tail.clear();
342 self.tail.extend_from_slice(&window[window.len() - keep..]);
343 self.broker.leak(self.host, &window)
344 }
345}
346
347fn replace(mut line: &[u8], from: &[u8], to: &[u8], out: &mut Vec<u8>) {
349 while let Some(at) = line.windows(from.len()).position(|w| w == from) {
350 out.extend_from_slice(&line[..at]);
351 out.extend_from_slice(to);
352 line = &line[at + from.len()..];
353 }
354 out.extend_from_slice(line);
355}
356
357fn echoed(head: &[u8]) -> bool {
361 head.get(..6)
362 .is_some_and(|m| m.eq_ignore_ascii_case(b"TRACE ") || m.eq_ignore_ascii_case(b"TRACK "))
363}
364
365fn header_name(line: &[u8]) -> Option<&[u8]> {
367 line.iter().position(|b| *b == b':').map(|at| &line[..at])
368}
369
370fn contains(haystack: &[u8], needle: &[u8]) -> bool {
371 !needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
372}
373
374mod echo;
375mod route;
376mod value;
377
378pub(crate) use self::echo::{Echo, Holdback, value_came_back};
379pub use self::route::RULE_SECRET_ROUTE;
380pub use self::value::Rotation;
381
382#[cfg(test)]
383mod tests;