profile.rs (9094B)
1 //! Validated product relay profiles and directional access policy. 2 3 use crate::{Error, RelayUrl, RelayUrlPolicy}; 4 use std::collections::BTreeSet; 5 6 /// Directional access authorized for one configured relay. 7 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 8 #[non_exhaustive] 9 pub enum RelayAccess { 10 /// Queries and subscriptions are allowed; publication is never attempted. 11 ReadOnly, 12 /// Queries, subscriptions, and publication are allowed. 13 ReadWrite, 14 } 15 16 impl RelayAccess { 17 /// Returns whether the profile authorizes event reads. 18 #[must_use] 19 pub const fn can_read(self) -> bool { 20 true 21 } 22 23 /// Returns whether the profile authorizes event publication. 24 #[must_use] 25 pub const fn can_write(self) -> bool { 26 matches!(self, Self::ReadWrite) 27 } 28 } 29 30 /// Host environment whose network trust rules produced a relay profile. 31 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 32 #[non_exhaustive] 33 pub enum RelayProfileKind { 34 /// Public-Internet profile. 35 Public, 36 /// Development-only profile restricted to exact loopback destinations. 37 Simulator, 38 /// Physical-device profile using public TLS or literal private-network endpoints. 39 Device, 40 } 41 42 /// One canonical relay endpoint with explicit network and access policy. 43 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 44 pub struct RelayEndpoint { 45 url: RelayUrl, 46 policy: RelayUrlPolicy, 47 access: RelayAccess, 48 } 49 50 impl RelayEndpoint { 51 /// Parses one endpoint with explicit destination and access policy. 52 pub fn new( 53 value: impl AsRef<str>, 54 policy: RelayUrlPolicy, 55 access: RelayAccess, 56 ) -> Result<Self, Error> { 57 Ok(Self { 58 url: RelayUrl::parse(value, policy)?, 59 policy, 60 access, 61 }) 62 } 63 64 /// Returns the canonical relay URL. 65 #[must_use] 66 pub const fn url(&self) -> &RelayUrl { 67 &self.url 68 } 69 70 /// Returns the destination policy applied before and after DNS resolution. 71 #[must_use] 72 pub const fn policy(&self) -> RelayUrlPolicy { 73 self.policy 74 } 75 76 /// Returns the read/write authority declared by the profile. 77 #[must_use] 78 pub const fn access(&self) -> RelayAccess { 79 self.access 80 } 81 } 82 83 /// Complete validated relay selection for one host environment. 84 #[derive(Clone, Debug, Eq, PartialEq)] 85 pub struct RelayProfile { 86 kind: RelayProfileKind, 87 endpoints: Vec<RelayEndpoint>, 88 } 89 90 impl RelayProfile { 91 /// Builds an explicit profile with directional access per endpoint. 92 /// 93 /// This constructor does not inject a bundled endpoint. Callers provide the 94 /// complete validated set they intend to use. 95 pub fn explicit<I>(kind: RelayProfileKind, endpoints: I) -> Result<Self, Error> 96 where 97 I: IntoIterator<Item = RelayEndpoint>, 98 { 99 let endpoints = endpoints 100 .into_iter() 101 .take(crate::client::MAX_RELAYS + 1) 102 .collect::<Vec<_>>(); 103 Self::validated(kind, endpoints) 104 } 105 106 fn validated(kind: RelayProfileKind, endpoints: Vec<RelayEndpoint>) -> Result<Self, Error> { 107 if endpoints.is_empty() { 108 return Err(Error::EmptyRelaySet); 109 } 110 if endpoints.len() > crate::client::MAX_RELAYS { 111 return Err(Error::TooManyRelays { 112 max: crate::client::MAX_RELAYS, 113 actual: endpoints.len(), 114 }); 115 } 116 let mut seen = BTreeSet::new(); 117 for endpoint in &endpoints { 118 let policy_allowed = match kind { 119 RelayProfileKind::Public => endpoint.policy == RelayUrlPolicy::Public, 120 RelayProfileKind::Simulator => endpoint.policy == RelayUrlPolicy::Local, 121 RelayProfileKind::Device => matches!( 122 endpoint.policy, 123 RelayUrlPolicy::Public | RelayUrlPolicy::PrivateNetwork 124 ), 125 }; 126 if !policy_allowed { 127 return Err(Error::RelayProfilePolicyMismatch); 128 } 129 if !seen.insert(endpoint.url.clone()) { 130 return Err(Error::DuplicateRelayUrl); 131 } 132 } 133 Ok(Self { kind, endpoints }) 134 } 135 136 /// Returns the selected host-environment profile. 137 #[must_use] 138 pub const fn kind(&self) -> RelayProfileKind { 139 self.kind 140 } 141 142 /// Returns endpoints in deterministic profile order. 143 #[must_use] 144 pub fn endpoints(&self) -> &[RelayEndpoint] { 145 self.endpoints.as_slice() 146 } 147 } 148 149 #[cfg(test)] 150 pub(crate) fn test_profile<I, S>( 151 kind: RelayProfileKind, 152 policy: RelayUrlPolicy, 153 values: I, 154 ) -> Result<RelayProfile, Error> 155 where 156 I: IntoIterator<Item = S>, 157 S: AsRef<str>, 158 { 159 let endpoints = values 160 .into_iter() 161 .map(|value| RelayEndpoint::new(value, policy, RelayAccess::ReadWrite)) 162 .collect::<Result<Vec<_>, _>>()?; 163 RelayProfile::explicit(kind, endpoints) 164 } 165 166 #[cfg(test)] 167 mod tests { 168 use super::*; 169 170 #[test] 171 fn explicit_profile_requires_every_public_destination_and_authority() { 172 let profile = RelayProfile::explicit( 173 RelayProfileKind::Public, 174 [RelayEndpoint::new( 175 "wss://write.example", 176 RelayUrlPolicy::Public, 177 RelayAccess::ReadWrite, 178 ) 179 .expect("endpoint")], 180 ) 181 .expect("public profile"); 182 assert_eq!(profile.kind(), RelayProfileKind::Public); 183 assert_eq!(profile.endpoints().len(), 1); 184 assert_eq!(profile.endpoints()[0].url().as_str(), "wss://write.example"); 185 assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite); 186 } 187 188 #[test] 189 fn explicit_profile_preserves_directional_access_without_injecting_endpoints() { 190 let profile = RelayProfile::explicit( 191 RelayProfileKind::Public, 192 [ 193 RelayEndpoint::new( 194 "wss://read.example", 195 RelayUrlPolicy::Public, 196 RelayAccess::ReadOnly, 197 ) 198 .expect("read endpoint"), 199 RelayEndpoint::new( 200 "wss://write.example", 201 RelayUrlPolicy::Public, 202 RelayAccess::ReadWrite, 203 ) 204 .expect("write endpoint"), 205 ], 206 ) 207 .expect("explicit profile"); 208 209 assert_eq!(profile.endpoints().len(), 2); 210 assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadOnly); 211 assert_eq!(profile.endpoints()[1].access(), RelayAccess::ReadWrite); 212 assert!(!profile.endpoints()[0].access().can_write()); 213 assert!(profile.endpoints()[1].access().can_write()); 214 } 215 216 #[test] 217 fn profile_kind_rejects_mismatched_explicit_destination_policy() { 218 let public = RelayEndpoint::new( 219 "wss://relay.example", 220 RelayUrlPolicy::Public, 221 RelayAccess::ReadOnly, 222 ) 223 .expect("public endpoint"); 224 let local = RelayEndpoint::new( 225 "ws://127.0.0.1:7447", 226 RelayUrlPolicy::Local, 227 RelayAccess::ReadWrite, 228 ) 229 .expect("local endpoint"); 230 let private = RelayEndpoint::new( 231 "wss://10.0.0.5:7447", 232 RelayUrlPolicy::PrivateNetwork, 233 RelayAccess::ReadWrite, 234 ) 235 .expect("private endpoint"); 236 237 assert!(RelayProfile::explicit(RelayProfileKind::Public, [public.clone()]).is_ok()); 238 assert!(RelayProfile::explicit(RelayProfileKind::Simulator, [local.clone()]).is_ok()); 239 assert!(RelayProfile::explicit(RelayProfileKind::Device, [public]).is_ok()); 240 assert!(RelayProfile::explicit(RelayProfileKind::Device, [private]).is_ok()); 241 assert_eq!( 242 RelayProfile::explicit(RelayProfileKind::Public, [local]).unwrap_err(), 243 Error::RelayProfilePolicyMismatch 244 ); 245 } 246 247 #[test] 248 fn empty_and_duplicate_profiles_remain_rejected() { 249 assert!( 250 RelayProfile::explicit(RelayProfileKind::Public, Vec::<RelayEndpoint>::new()).is_err() 251 ); 252 let endpoint = RelayEndpoint::new( 253 "wss://relay.example", 254 RelayUrlPolicy::Public, 255 RelayAccess::ReadOnly, 256 ) 257 .expect("endpoint"); 258 assert!( 259 RelayProfile::explicit(RelayProfileKind::Public, [endpoint.clone(), endpoint]).is_err() 260 ); 261 } 262 263 #[test] 264 fn excessive_and_infinite_endpoint_iterators_terminate_at_the_public_bound() { 265 let endpoint = RelayEndpoint::new( 266 "wss://relay.example", 267 RelayUrlPolicy::Public, 268 RelayAccess::ReadOnly, 269 ) 270 .expect("endpoint"); 271 assert_eq!( 272 RelayProfile::explicit(RelayProfileKind::Public, std::iter::repeat(endpoint),) 273 .unwrap_err(), 274 Error::TooManyRelays { 275 max: crate::client::MAX_RELAYS, 276 actual: crate::client::MAX_RELAYS + 1, 277 } 278 ); 279 } 280 }