mod authority;
pub(crate) mod config;
pub(crate) use authority::{Lease, Registry};
pub(crate) use config::FrozenCredential;
pub use config::{Capability, CredentialSource, Profile};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use zeroize::Zeroizing;
pub(crate) struct DispatchGuard {
pub lease: std::sync::Arc<Lease>,
pub capability: Capability,
pub name: String,
pub credential: Option<std::sync::Arc<Credential>>,
pub descriptor: serde_json::Value,
pub success_status: Vec<u16>,
}
impl DispatchGuard {
pub fn admit(&self, request: &reqwest::Request) -> Result<(), String> {
self.capability.check_request(request)?;
self.lease
.admit_send(&self.name, self.capability.max_requests)
}
pub fn credential_reference(&self) -> Result<Option<(HeaderName, String)>, String> {
if self.credential.is_none() {
return Ok(None);
}
let header = HeaderName::from_bytes(self.capability.header.as_bytes())
.map_err(|_| "invalid credential header")?;
let alias = self.capability.credential.as_deref().unwrap_or_default();
let identity = self
.descriptor
.get("credential")
.cloned()
.unwrap_or_default();
let digest = crate::reasoning::prepared::digest_json(&identity)?;
Ok(Some((
header,
format!("symbi-outbound-credential-ref-v1:{alias}:{digest}"),
)))
}
pub fn delivers_body(&self, status: u16) -> bool {
(200..300).contains(&status) || self.success_status.contains(&status)
}
pub fn response(&self, headers: &HeaderMap, body: &[u8]) -> Result<(), String> {
if let Some(credential) = &self.credential {
credential.check_response(headers, body)?;
}
self.lease.check_live()
}
}
pub struct Credential(Zeroizing<String>);
impl Credential {
pub fn new(value: String) -> Result<Self, String> {
let value = Zeroizing::new(value);
if value.is_empty() || value.len() > 4096 || value.bytes().any(|b| b.is_ascii_control()) {
return Err("outbound credential is empty, oversized or contains controls".into());
}
Ok(Self(value))
}
pub(crate) fn inject(
&self,
headers: &mut HeaderMap,
name: &str,
prefix: &str,
) -> Result<(), String> {
let name =
HeaderName::from_bytes(name.as_bytes()).map_err(|_| "invalid credential header")?;
let value = Zeroizing::new(format!("{prefix}{}", self.0.as_str()));
let mut value =
HeaderValue::from_str(&value).map_err(|_| "invalid credential header value")?;
value.set_sensitive(true);
headers.insert(name, value);
Ok(())
}
pub(crate) fn check_response(&self, headers: &HeaderMap, body: &[u8]) -> Result<(), String> {
let needle = Needle::new(self.0.as_bytes());
let header_secret = Zeroizing::new(self.0.to_ascii_lowercase());
let header_needle = Needle::new(header_secret.as_bytes());
let header_value_needle = Needle::new(self.0.trim_matches(' ').as_bytes());
if needle.found_in(body)
|| headers.iter().any(|(name, value)| {
header_needle.found_in(name.as_str().as_bytes())
|| header_value_needle.found_in(value.as_bytes())
})
{
return Err("outbound response contains a protected credential".into());
}
Ok(())
}
}
struct Needle<'a> {
pattern: &'a [u8],
failure: Vec<usize>,
}
impl<'a> Needle<'a> {
fn new(pattern: &'a [u8]) -> Self {
let mut failure = vec![0; pattern.len()];
let mut k = 0;
for i in 1..pattern.len() {
while k > 0 && pattern[i] != pattern[k] {
k = failure[k - 1];
}
if pattern[i] == pattern[k] {
k += 1;
}
failure[i] = k;
}
Self { pattern, failure }
}
fn found_in(&self, haystack: &[u8]) -> bool {
if self.pattern.is_empty() {
return true;
}
let mut k = 0;
for &byte in haystack {
while k > 0 && byte != self.pattern[k] {
k = self.failure[k - 1];
}
if byte == self.pattern[k] {
k += 1;
if k == self.pattern.len() {
return true;
}
}
}
false
}
}
#[cfg(test)]
mod tests;
#[cfg(all(test, unix))]
mod dispatch_tests;