Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157use std::sync::Arc;use std::time::Duration;
use bobbin_runtime::{Clock, RuntimeHasher};use http::HeaderMap;use jacquard_common::DefaultStr;use jacquard_common::types::nsid::Nsid;use thiserror::Error;use url::Url;
use crate::breaker::FailureThreshold;use crate::host::{KnotHost, KnotHostError};use crate::{KnotHttpConfig, KnotProxy, KnotProxyConfig, KnotProxyError, ProxyResponse};
const MIRROR_HTTP: KnotHttpConfig = KnotHttpConfig { connect_timeout: Duration::from_secs(3), read_timeout: Duration::from_secs(5),};
#[derive(Debug, Error)]pub enum MirrorProxyError { #[error("mirror url: {0}")] Host(#[from] KnotHostError), #[error("mirror http client: {0}")] Http(#[from] reqwest::Error), #[error("mirror url must be a bare origin, got {0}")] NotAnOrigin(String),}
pub struct MirrorProxy { proxy: KnotProxy, host: KnotHost,}
impl MirrorProxy { pub fn new( url: &Url, clock: Arc<dyn Clock>, hasher: RuntimeHasher, ) -> Result<Self, MirrorProxyError> { Self::with_breaker( url, clock, hasher, FailureThreshold::new(3).expect("nonzero literal"), KnotProxyConfig::default().cooldown, ) }
/// `failure_threshold` failures in a row stop requests to the mirror for `cooldown` pub fn with_breaker( url: &Url, clock: Arc<dyn Clock>, hasher: RuntimeHasher, failure_threshold: FailureThreshold, cooldown: Duration, ) -> Result<Self, MirrorProxyError> { let host = KnotHost::parse(url.as_str())?; match beyond_origin(url) { Some(extra) => Err(MirrorProxyError::NotAnOrigin(extra)), None => Ok(Self { proxy: KnotProxy::new( KnotProxyConfig { allow_private_hosts: true, require_https: false, failure_threshold, cooldown, }, MIRROR_HTTP, clock, hasher, )?, host, }), } }
pub fn host(&self) -> &KnotHost { &self.host }
pub async fn forward_raw( &self, nsid: &Nsid<DefaultStr>, query: &[(&str, &str)], headers: HeaderMap, ) -> Result<ProxyResponse, KnotProxyError> { self.proxy.forward(&self.host, nsid, query, headers).await }}
fn beyond_origin(url: &Url) -> Option<String> { [ (!matches!(url.path(), "" | "/")).then(|| format!("the path {}", url.path())), url.query().map(|query| format!("the query {query}")), url.fragment() .map(|fragment| format!("the fragment {fragment}")), (!url.username().is_empty()).then(|| format!("the username {}", url.username())), url.password().map(|_| "a password".to_owned()), ] .into_iter() .flatten() .next()}
#[cfg(test)]mod tests { use super::*; use bobbin_runtime::SystemClock; fn mirror_at(raw: &str) -> Result<MirrorProxy, MirrorProxyError> { MirrorProxy::new( &Url::parse(raw).unwrap(), Arc::new(SystemClock::new()), RuntimeHasher::default(), ) }
#[test] fn a_mirror_url_with_more_than_an_origin_is_refused_without_echoing_a_password() { [ "https://nel.pet/api", "https://nel.pet/?a=1", "https://nel.pet/#x", "https://nel@nel.pet/", "https://nel:hunter2@nel.pet/", "https://:hunter2@nel.pet/", ] .iter() .for_each(|raw| { let Err(err) = mirror_at(raw) else { panic!("{raw} must error out, since KnotHost::parse would truncate it"); }; assert!( matches!(err, MirrorProxyError::NotAnOrigin(_)), "{raw} gave {err}", ); assert!( !err.to_string().contains("hunter2"), "an operator's password must stay out of the log, got {err}", ); }); assert!( mirror_at("https://nel.pet/").is_ok(), "a bare origin is what an operator will configure", ); }
#[test] fn a_mirror_dials_where_the_knot_policy_refuses() { let mirror = mirror_at("http://127.0.0.1:9/").expect("an operator picks the mirror"); assert!( mirror.proxy.allows_private_hosts() && !mirror.proxy.requires_https(), "the knot policy doesn't bind an operator-configured mirror", ); }}