lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }