policy.rs (8953B)
1 use alloc::string::String; 2 #[cfg(feature = "std")] 3 use alloc::string::ToString; 4 #[cfg(feature = "std")] 5 use alloc::vec; 6 use core::fmt; 7 use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueUri; 8 9 #[cfg(feature = "std")] 10 pub const RADROOTS_SIMPLEX_INTEROP_REQUIRE_UPSTREAM_ENV: &str = 11 "RADROOTS_SIMPLEX_INTEROP_REQUIRE_UPSTREAM"; 12 #[cfg(feature = "std")] 13 pub const RADROOTS_SIMPLEX_INTEROP_SMP_HOST_ENV: &str = "RADROOTS_SIMPLEX_INTEROP_SMP_HOST"; 14 #[cfg(feature = "std")] 15 pub const RADROOTS_SIMPLEX_INTEROP_SMP_PORT_ENV: &str = "RADROOTS_SIMPLEX_INTEROP_SMP_PORT"; 16 #[cfg(feature = "std")] 17 pub const RADROOTS_SIMPLEX_INTEROP_SMP_IDENTITY_ENV: &str = "RADROOTS_SIMPLEX_INTEROP_SMP_IDENTITY"; 18 19 #[derive(Debug, Clone, PartialEq, Eq)] 20 pub struct RadrootsSimplexInteropFixturePolicy { 21 pub namespace_prefix: &'static str, 22 } 23 24 impl Default for RadrootsSimplexInteropFixturePolicy { 25 fn default() -> Self { 26 Self { 27 namespace_prefix: "rr-synth/", 28 } 29 } 30 } 31 32 impl RadrootsSimplexInteropFixturePolicy { 33 pub fn assert_fixture_id(&self, id: &str) -> Result<(), RadrootsSimplexInteropPolicyError> { 34 if id.starts_with(self.namespace_prefix) { 35 return Ok(()); 36 } 37 Err(RadrootsSimplexInteropPolicyError::InvalidFixtureId( 38 id.into(), 39 )) 40 } 41 42 pub fn assert_queue_uri( 43 &self, 44 queue_uri: &RadrootsSimplexSmpQueueUri, 45 ) -> Result<(), RadrootsSimplexInteropPolicyError> { 46 for host in &queue_uri.server.hosts { 47 if host.ends_with(".invalid") || host.ends_with(".example") || host.ends_with(".test") { 48 continue; 49 } 50 return Err(RadrootsSimplexInteropPolicyError::InvalidFixtureHost( 51 host.clone(), 52 )); 53 } 54 Ok(()) 55 } 56 } 57 58 #[cfg(feature = "std")] 59 #[derive(Debug, Clone, PartialEq, Eq)] 60 pub struct RadrootsSimplexInteropLocalUpstream { 61 pub host: String, 62 pub port: u16, 63 pub server_identity: Option<String>, 64 } 65 66 #[cfg(feature = "std")] 67 impl RadrootsSimplexInteropLocalUpstream { 68 pub fn from_env() -> Option<Self> { 69 Self::from_env_values(false).ok().flatten() 70 } 71 72 pub fn required_from_env() -> Result<Option<Self>, RadrootsSimplexInteropPolicyError> { 73 Self::from_env_values(required_upstream_enabled()) 74 } 75 76 fn from_env_values(required: bool) -> Result<Option<Self>, RadrootsSimplexInteropPolicyError> { 77 let host = optional_env_value(RADROOTS_SIMPLEX_INTEROP_SMP_HOST_ENV); 78 let port = optional_env_value(RADROOTS_SIMPLEX_INTEROP_SMP_PORT_ENV); 79 let server_identity = optional_env_value(RADROOTS_SIMPLEX_INTEROP_SMP_IDENTITY_ENV); 80 Self::from_values(host, port, server_identity, required) 81 } 82 83 pub fn from_values( 84 host: Option<String>, 85 port: Option<String>, 86 server_identity: Option<String>, 87 required: bool, 88 ) -> Result<Option<Self>, RadrootsSimplexInteropPolicyError> { 89 let Some(host) = 90 required_or_optional(host, required, RADROOTS_SIMPLEX_INTEROP_SMP_HOST_ENV)? 91 else { 92 return Ok(None); 93 }; 94 let Some(port) = 95 required_or_optional(port, required, RADROOTS_SIMPLEX_INTEROP_SMP_PORT_ENV)? 96 else { 97 return Ok(None); 98 }; 99 let server_identity = match required_or_optional( 100 server_identity, 101 required, 102 RADROOTS_SIMPLEX_INTEROP_SMP_IDENTITY_ENV, 103 )? { 104 Some(value) => Some(value), 105 None => None, 106 }; 107 Ok(Some(Self { 108 host, 109 port: port.parse::<u16>().map_err(|_| { 110 RadrootsSimplexInteropPolicyError::InvalidLocalUpstreamPort(port.clone()) 111 })?, 112 server_identity, 113 })) 114 } 115 116 pub fn server_address( 117 &self, 118 ) -> Option<radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpServerAddress> { 119 Some( 120 radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpServerAddress { 121 server_identity: self.server_identity.clone()?, 122 hosts: vec![self.host.clone()], 123 port: Some(self.port), 124 }, 125 ) 126 } 127 128 pub fn assert_reachable(&self) -> Result<(), RadrootsSimplexInteropPolicyError> { 129 use std::net::{TcpStream, ToSocketAddrs}; 130 use std::time::Duration; 131 132 let mut addrs = (self.host.as_str(), self.port) 133 .to_socket_addrs() 134 .map_err(|source| { 135 RadrootsSimplexInteropPolicyError::LocalUpstreamIo(source.to_string()) 136 })?; 137 let Some(addr) = addrs.next() else { 138 return Err(RadrootsSimplexInteropPolicyError::LocalUpstreamIo( 139 "no socket addresses resolved".into(), 140 )); 141 }; 142 TcpStream::connect_timeout(&addr, Duration::from_millis(500)).map_err(|source| { 143 RadrootsSimplexInteropPolicyError::LocalUpstreamIo(source.to_string()) 144 })?; 145 Ok(()) 146 } 147 } 148 149 #[derive(Debug, Clone, PartialEq, Eq)] 150 pub enum RadrootsSimplexInteropPolicyError { 151 InvalidFixtureId(String), 152 InvalidFixtureHost(String), 153 MissingLocalUpstreamEnv(&'static str), 154 InvalidLocalUpstreamPort(String), 155 LocalUpstreamIo(String), 156 } 157 158 impl fmt::Display for RadrootsSimplexInteropPolicyError { 159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 160 match self { 161 Self::InvalidFixtureId(id) => { 162 write!( 163 f, 164 "interop fixture id `{id}` is outside the rr-synth namespace" 165 ) 166 } 167 Self::InvalidFixtureHost(host) => { 168 write!( 169 f, 170 "interop fixture host `{host}` is not in a synthetic domain" 171 ) 172 } 173 Self::MissingLocalUpstreamEnv(name) => { 174 write!( 175 f, 176 "required SimpleX upstream environment `{name}` is not set" 177 ) 178 } 179 Self::InvalidLocalUpstreamPort(port) => { 180 write!(f, "invalid SimpleX upstream port `{port}`") 181 } 182 Self::LocalUpstreamIo(message) => write!(f, "{message}"), 183 } 184 } 185 } 186 187 #[cfg(feature = "std")] 188 impl std::error::Error for RadrootsSimplexInteropPolicyError {} 189 190 #[cfg(feature = "std")] 191 fn optional_env_value(name: &str) -> Option<String> { 192 std::env::var(name) 193 .ok() 194 .map(|value| value.trim().to_owned()) 195 .filter(|value| !value.is_empty()) 196 } 197 198 #[cfg(feature = "std")] 199 fn required_or_optional( 200 value: Option<String>, 201 required: bool, 202 name: &'static str, 203 ) -> Result<Option<String>, RadrootsSimplexInteropPolicyError> { 204 match value { 205 Some(value) => Ok(Some(value)), 206 None if required => Err(RadrootsSimplexInteropPolicyError::MissingLocalUpstreamEnv( 207 name, 208 )), 209 None => Ok(None), 210 } 211 } 212 213 #[cfg(feature = "std")] 214 fn required_upstream_enabled() -> bool { 215 optional_env_value(RADROOTS_SIMPLEX_INTEROP_REQUIRE_UPSTREAM_ENV) 216 .map(|value| { 217 matches!( 218 value.as_str(), 219 "1" | "true" | "TRUE" | "required" | "REQUIRED" 220 ) 221 }) 222 .unwrap_or(false) 223 } 224 225 #[cfg(all(test, feature = "std"))] 226 mod tests { 227 use super::*; 228 229 #[test] 230 fn optional_upstream_config_returns_none_when_unset() { 231 assert_eq!( 232 RadrootsSimplexInteropLocalUpstream::from_values(None, None, None, false).unwrap(), 233 None 234 ); 235 } 236 237 #[test] 238 fn required_upstream_config_reports_first_missing_value() { 239 let error = 240 RadrootsSimplexInteropLocalUpstream::from_values(None, None, None, true).unwrap_err(); 241 assert!(matches!( 242 error, 243 RadrootsSimplexInteropPolicyError::MissingLocalUpstreamEnv( 244 RADROOTS_SIMPLEX_INTEROP_SMP_HOST_ENV 245 ) 246 )); 247 } 248 249 #[test] 250 fn required_upstream_config_requires_identity() { 251 let error = RadrootsSimplexInteropLocalUpstream::from_values( 252 Some("127.0.0.1".to_owned()), 253 Some("5223".to_owned()), 254 None, 255 true, 256 ) 257 .unwrap_err(); 258 assert!(matches!( 259 error, 260 RadrootsSimplexInteropPolicyError::MissingLocalUpstreamEnv( 261 RADROOTS_SIMPLEX_INTEROP_SMP_IDENTITY_ENV 262 ) 263 )); 264 } 265 266 #[test] 267 fn required_upstream_config_rejects_invalid_port() { 268 let error = RadrootsSimplexInteropLocalUpstream::from_values( 269 Some("127.0.0.1".to_owned()), 270 Some("not-a-port".to_owned()), 271 Some("server-identity".to_owned()), 272 true, 273 ) 274 .unwrap_err(); 275 assert!(matches!( 276 error, 277 RadrootsSimplexInteropPolicyError::InvalidLocalUpstreamPort(_) 278 )); 279 } 280 }