Which household a request is for, as a type that cannot name anything else.
One Durable Object serves one household, and the object is addressed by this name. The name is
therefore the isolation boundary between households, so it is checked once, here, and carried as
a [HouseholdId] from then on.
7use std::fmt;
The header a test harness names a household in. Only read when the deployment trusts it
(see [Households]).
11pub const HOUSEHOLD_HEADER: &str = "x-whiskers-household";
The longest id served. A Durable Object name may be 1024 bytes; a household's is far shorter.
14pub const MAX_LEN: usize = 63;
A household's name: 1 to 63 lowercase ASCII letters, digits or hyphens, not starting or ending with a hyphen (the same shape celld requires of a Worker's name, so one rule serves both hosts).
29impl fmt::Display for NotAnId { 30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 match self { 32 NotAnId::Empty => write!(f, "a household id is empty"), 33 NotAnId::TooLong => write!(f, "a household id is longer than {MAX_LEN} characters"), 34 NotAnId::BadShape => write!(f, "a household id is lowercase letters, digits and inner hyphens only"), 35 } 36 } 37} 38 39impl HouseholdId { 40 pub fn parse(text: &str) -> Result<Self, NotAnId> { 41 if text.is_empty() { 42 return Err(NotAnId::Empty); 43 } 44 if text.len() > MAX_LEN { 45 return Err(NotAnId::TooLong); 46 } 47 let plain = text.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'); 48 if !plain || text.starts_with('-') || text.ends_with('-') { 49 return Err(NotAnId::BadShape); 50 } 51 Ok(Self(text.to_owned())) 52 } 53 54 pub fn as_str(&self) -> &str { 55 &self.0 56 } 57}
How this deployment decides which household a request belongs to.
Whiskers' wire carries no credential today (the private network is the access control), so there is
nothing to derive a household from: a deployment serves one household, named in its
configuration. A harness that must start from an empty household for each test can instead be
trusted to name one in a header; that is a testing mode, set only in .dev.vars, which neither
wrangler deploy nor celld deploy reads. A header from a client is not authentication and must
never be trusted in a deployment.
Why a request names no household we can serve.
80impl Households {
From the two settings: HOUSEHOLD (required) and TRUST_HOUSEHOLD_HEADER ("1" to trust).
A configuration that does not name a valid household is an error for the operator.
Whether this deployment is in the harness's test mode, which is exactly when it trusts a household header: one switch, so the test-only routes cannot be on without it.
The household a request is for. header is the value of [HOUSEHOLD_HEADER], if the request had one.
95 pub fn of(&self, header: Option<&str>) -> Result<HouseholdId, Unnamed> { 96 match (self, header) { 97 (Households::NamedByHeader { .. }, Some(h)) => HouseholdId::parse(h).map_err(Unnamed::BadHeader), 98 (Households::NamedByHeader { otherwise: id } | Households::Only(id), _) => Ok(id.clone()), 99 } 100 } 101}
103#[cfg(test)] 104mod tests { 105 use super::*; 106 107 #[test] 108 fn an_id_is_a_plain_lowercase_name() { 109 assert!(HouseholdId::parse("home").is_ok()); 110 assert!(HouseholdId::parse("a-1-b").is_ok()); 111 assert_eq!(HouseholdId::parse(""), Err(NotAnId::Empty)); 112 assert_eq!(HouseholdId::parse(&"a".repeat(64)), Err(NotAnId::TooLong)); 113 assert!(HouseholdId::parse(&"a".repeat(63)).is_ok()); 114 for bad in ["Home", "-a", "a-", "a b", "a/b", "é", "a.b", "a_b"] { 115 assert_eq!(HouseholdId::parse(bad), Err(NotAnId::BadShape), "{bad:?}"); 116 } 117 } 118 119 #[test] 120 fn a_deployment_serves_its_one_household_whatever_a_client_sends() { 121 let h = Households::from_settings(Some("home"), None).unwrap(); 122 assert_eq!(h.of(None).unwrap().as_str(), "home"); 123 assert_eq!(h.of(Some("other")).unwrap().as_str(), "home", "an untrusted header is not read"); 124 assert_eq!(h.of(Some("NOT AN ID")).unwrap().as_str(), "home", "nor validated"); 125 let h = Households::from_settings(Some("home"), Some("0")).unwrap(); 126 assert_eq!(h.of(Some("other")).unwrap().as_str(), "home"); 127 } 128 129 #[test] 130 fn a_trusted_header_names_the_household_and_a_bad_one_is_refused() { 131 let h = Households::from_settings(Some("home"), Some("1")).unwrap(); 132 assert_eq!(h.of(Some("test-7")).unwrap().as_str(), "test-7"); 133 assert_eq!(h.of(None).unwrap().as_str(), "home"); 134 assert_eq!(h.of(Some("A")), Err(Unnamed::BadHeader(NotAnId::BadShape))); 135 } 136 137 #[test] 138 fn only_the_trusting_mode_is_a_test_mode() { 139 assert!(!Households::from_settings(Some("home"), None).unwrap().is_testing()); 140 assert!(!Households::from_settings(Some("home"), Some("true")).unwrap().is_testing(), "only \"1\" turns it on"); 141 assert!(Households::from_settings(Some("home"), Some("1")).unwrap().is_testing()); 142 } 143 144 #[test] 145 fn a_configuration_without_a_household_is_an_error() { 146 assert_eq!(Households::from_settings(None, None), Err(NotAnId::Empty)); 147 assert_eq!(Households::from_settings(Some("Bad"), Some("1")), Err(NotAnId::BadShape)); 148 } 149}