1use crate::{Region, RegionError};
2use regex::Regex;
3use std::sync::LazyLock;
4use url::Url;
5
6static REGION_HOST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
8 Regex::new(r"^(?:https://){0,1}([^\.]+)\.([^\.]+)\.viturhosted\.net/?$").expect("Invalid regex")
9});
10
11static CTS_REGION_HOST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13 Regex::new(r"^(?:https://){0,1}([^\.]+)\.([^\.]+)\.cts\.cipherstashmanaged\.net/?$")
14 .expect("Invalid regex")
15});
16
17static DOMAIN_NAME: &str = "viturhosted.net";
19
20static CTS_DOMAIN_NAME: &str = "cts.cipherstashmanaged.net";
22
23pub trait ServiceDiscovery {
25 fn name(&self) -> &'static str;
26 fn fqdn(region: Region) -> String;
27 fn endpoint(region: Region) -> Result<Url, RegionError>;
28}
29
30pub struct ZeroKmsServiceDiscovery;
32
33impl ServiceDiscovery for ZeroKmsServiceDiscovery {
34 fn name(&self) -> &'static str {
35 "zerokms"
36 }
37
38 fn fqdn(region: Region) -> String {
41 format!("{}.{DOMAIN_NAME}", region.identifier())
42 }
43
44 fn endpoint(region: Region) -> Result<Url, RegionError> {
50 #[cfg(feature = "test_utils")]
51 if let Ok(override_url) = std::env::var("ZEROKMS_DISCOVERY_OVERRIDE") {
52 return Url::parse(&override_url).map_err(|e| {
53 RegionError::InvalidRegion(format!(
54 "Invalid ZEROKMS_DISCOVERY_OVERRIDE URL '{}': {}",
55 override_url, e
56 ))
57 });
58 }
59
60 Url::parse(&format!("https://{}.{DOMAIN_NAME}/", region.identifier())).map_err(|e| {
61 RegionError::InvalidRegion(format!(
62 "Invalid service URL for ZeroKMS in region {}: {}",
63 region.identifier(),
64 e
65 ))
66 })
67 }
68}
69
70impl ZeroKmsServiceDiscovery {
71 pub fn region_from_host_fqdn(host_fqdn: &str) -> Result<Region, RegionError> {
75 REGION_HOST_REGEX
76 .captures(host_fqdn)
77 .and_then(|caps| Some((caps.get(1)?, caps.get(2)?)))
78 .map(|(r, p)| format!("{}.{}", r.as_str(), p.as_str()))
79 .ok_or_else(|| RegionError::InvalidHostFqdn(host_fqdn.to_string()))
80 .and_then(|ident| Region::new(&ident))
81 }
82}
83
84static SECRETS_DOMAIN_NAME: &str = "dashboard.cipherstash.com";
86
87pub struct SecretsServiceDiscovery;
89
90impl ServiceDiscovery for SecretsServiceDiscovery {
91 fn name(&self) -> &'static str {
92 "secrets"
93 }
94
95 fn fqdn(_region: Region) -> String {
99 SECRETS_DOMAIN_NAME.to_string()
100 }
101
102 fn endpoint(_region: Region) -> Result<Url, RegionError> {
106 Url::parse(&format!("https://{SECRETS_DOMAIN_NAME}/"))
107 .map_err(|e| RegionError::InvalidRegion(format!("Invalid Secrets service URL: {e}")))
108 }
109}
110
111pub struct CtsServiceDiscovery;
113
114impl ServiceDiscovery for CtsServiceDiscovery {
115 fn name(&self) -> &'static str {
116 "cts"
117 }
118
119 fn fqdn(region: Region) -> String {
120 format!("{}.{CTS_DOMAIN_NAME}", region.identifier())
121 }
122
123 fn endpoint(region: Region) -> Result<Url, RegionError> {
124 Url::parse(&format!(
125 "https://{}.{CTS_DOMAIN_NAME}/",
126 region.identifier()
127 ))
128 .map_err(|e| {
129 RegionError::InvalidRegion(format!(
130 "Invalid service URL for CTS in region {}: {}",
131 region.identifier(),
132 e
133 ))
134 })
135 }
136}
137
138impl CtsServiceDiscovery {
139 pub fn region_from_host_fqdn(host_fqdn: &str) -> Result<Region, RegionError> {
143 CTS_REGION_HOST_REGEX
144 .captures(host_fqdn)
145 .and_then(|caps| Some((caps.get(1)?, caps.get(2)?)))
146 .map(|(r, p)| format!("{}.{}", r.as_str(), p.as_str()))
147 .ok_or_else(|| RegionError::InvalidHostFqdn(host_fqdn.to_string()))
148 .and_then(|ident| Region::new(&ident))
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_region_from_host_fqdn() -> anyhow::Result<()> {
158 let host_fqdn = "us-west-1.aws.viturhosted.net";
159 let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
160 assert_eq!(region.identifier(), "us-west-1.aws");
161
162 Ok(())
163 }
164
165 #[test]
166 fn test_region_from_host_endpoint() -> anyhow::Result<()> {
167 let host_fqdn = "https://us-west-1.aws.viturhosted.net";
168 let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
169 assert_eq!(region.identifier(), "us-west-1.aws");
170 Ok(())
171 }
172
173 #[test]
174 fn test_region_from_host_endpoint_with_trailing_slash() -> anyhow::Result<()> {
175 let host_fqdn = "https://us-west-1.aws.viturhosted.net/";
176 let region = ZeroKmsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
177 assert_eq!(region.identifier(), "us-west-1.aws");
178 Ok(())
179 }
180
181 #[test]
182 fn test_cts_endpoint_ap_southeast_2() -> anyhow::Result<()> {
183 let region = Region::new("ap-southeast-2.aws")?;
184 let url = CtsServiceDiscovery::endpoint(region)?;
185 assert_eq!(
186 url.as_str(),
187 "https://ap-southeast-2.aws.cts.cipherstashmanaged.net/"
188 );
189 Ok(())
190 }
191
192 #[test]
193 fn test_cts_endpoint_us_east_1() -> anyhow::Result<()> {
194 let region = Region::new("us-east-1.aws")?;
195 let url = CtsServiceDiscovery::endpoint(region)?;
196 assert_eq!(
197 url.as_str(),
198 "https://us-east-1.aws.cts.cipherstashmanaged.net/"
199 );
200 Ok(())
201 }
202
203 #[test]
204 fn test_cts_region_from_host_fqdn() -> anyhow::Result<()> {
205 let host_fqdn = "us-east-1.aws.cts.cipherstashmanaged.net";
206 let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
207 assert_eq!(region.identifier(), "us-east-1.aws");
208 Ok(())
209 }
210
211 #[test]
212 fn test_cts_region_from_host_endpoint() -> anyhow::Result<()> {
213 let host_fqdn = "https://ap-southeast-2.aws.cts.cipherstashmanaged.net";
214 let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
215 assert_eq!(region.identifier(), "ap-southeast-2.aws");
216 Ok(())
217 }
218
219 #[test]
220 fn test_cts_region_from_host_endpoint_with_trailing_slash() -> anyhow::Result<()> {
221 let host_fqdn = "https://us-west-2.aws.cts.cipherstashmanaged.net/";
222 let region = CtsServiceDiscovery::region_from_host_fqdn(host_fqdn)?;
223 assert_eq!(region.identifier(), "us-west-2.aws");
224 Ok(())
225 }
226
227 #[test]
228 fn test_cts_fqdn_us_west_2() -> anyhow::Result<()> {
229 let region = Region::new("us-west-2.aws")?;
230 assert_eq!(
231 CtsServiceDiscovery::fqdn(region),
232 "us-west-2.aws.cts.cipherstashmanaged.net"
233 );
234 Ok(())
235 }
236
237 #[test]
238 fn test_zerokms_region_from_invalid_host() {
239 let result = ZeroKmsServiceDiscovery::region_from_host_fqdn("not-a-valid-host");
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn test_zerokms_region_from_wrong_domain() {
245 let result =
246 ZeroKmsServiceDiscovery::region_from_host_fqdn("us-east-1.aws.wrongdomain.net");
247 assert!(result.is_err());
248 }
249
250 #[test]
251 fn test_zerokms_region_from_missing_region_component() {
252 let result = ZeroKmsServiceDiscovery::region_from_host_fqdn("viturhosted.net");
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn test_cts_region_from_invalid_host() {
258 let result = CtsServiceDiscovery::region_from_host_fqdn("not-a-valid-host");
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_cts_region_from_wrong_domain() {
264 let result = CtsServiceDiscovery::region_from_host_fqdn("us-east-1.aws.wrongdomain.net");
265 assert!(result.is_err());
266 }
267
268 #[test]
269 fn test_cts_region_from_missing_region_component() {
270 let result = CtsServiceDiscovery::region_from_host_fqdn("cts.cipherstashmanaged.net");
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_secrets_endpoint_returns_global_url() -> anyhow::Result<()> {
276 let region = Region::new("ap-southeast-2.aws")?;
277 let url = SecretsServiceDiscovery::endpoint(region)?;
278 assert_eq!(
279 url.as_str(),
280 "https://dashboard.cipherstash.com/",
281 "secrets endpoint should be the global dashboard URL regardless of region"
282 );
283 Ok(())
284 }
285
286 #[test]
287 fn test_secrets_endpoint_ignores_region() -> anyhow::Result<()> {
288 let url_apse2 = SecretsServiceDiscovery::endpoint(Region::new("ap-southeast-2.aws")?)?;
289 let url_use1 = SecretsServiceDiscovery::endpoint(Region::new("us-east-1.aws")?)?;
290 assert_eq!(
291 url_apse2, url_use1,
292 "secrets endpoint should be the same for all regions"
293 );
294 Ok(())
295 }
296
297 #[test]
298 fn test_secrets_fqdn() -> anyhow::Result<()> {
299 let region = Region::new("us-east-1.aws")?;
300 assert_eq!(
301 SecretsServiceDiscovery::fqdn(region),
302 "dashboard.cipherstash.com"
303 );
304 Ok(())
305 }
306}