1use std::sync::{
7 Arc, LazyLock, RwLock,
8 atomic::{AtomicBool, Ordering},
9};
10use tracing::warn;
11
12use crate::{Client, ClientBuilder};
13
14static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
15 LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
16
17#[derive(Debug)]
19pub(crate) struct Front {
20 pub(crate) policy: Arc<RwLock<FrontPolicy>>,
21 enabled: AtomicBool,
22}
23
24impl Clone for Front {
25 fn clone(&self) -> Self {
26 Self {
27 policy: self.policy.clone(),
28 enabled: AtomicBool::new(false),
29 }
30 }
31}
32
33impl Front {
34 pub(crate) fn new(policy: FrontPolicy) -> Self {
35 Self {
36 enabled: AtomicBool::new(false),
37 policy: Arc::new(RwLock::new(policy)),
38 }
39 }
40
41 pub(crate) fn off() -> Self {
42 Self::new(FrontPolicy::Off)
43 }
44
45 pub(crate) fn shared() -> Self {
46 let policy = SHARED_FRONTING_POLICY.clone();
47 Self {
48 enabled: AtomicBool::new(false),
49 policy,
50 }
51 }
52
53 pub(crate) fn set_policy(&self, policy: FrontPolicy) {
54 *self.policy.write().unwrap() = policy;
55 self.enabled.store(false, Ordering::Relaxed);
56 }
57
58 pub(crate) fn is_enabled(&self) -> bool {
59 match *self.policy.read().unwrap() {
60 FrontPolicy::Off => false,
61 FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
62 FrontPolicy::Always => true,
63 }
64 }
65
66 pub(crate) fn retry_enable(&self) {
69 if self.is_enabled() {
70 return;
71 }
72 if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
73 self.enabled.store(true, Ordering::Relaxed);
74 }
75 }
76}
77
78#[derive(Debug, Default, PartialEq, Clone)]
79pub enum FrontPolicy {
81 Always,
83 OnRetry,
85 #[default]
86 Off,
88}
89
90impl ClientBuilder {
91 pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
94 let front = if let Some(p) = policy {
95 Front::new(p)
96 } else {
97 Front::shared()
98 };
99
100 if !self.urls.iter().any(|url| url.has_front()) {
102 warn!(
103 "fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
104 self.urls
105 );
106 }
107
108 self.front = front;
109
110 self
111 }
112}
113
114impl Client {
115 pub fn set_front_policy(&mut self, policy: FrontPolicy) {
122 self.front.set_policy(policy)
123 }
124
125 pub fn use_shared_front_policy(&mut self) {
127 self.front = Front::shared();
128 }
129
130 pub fn set_shared_front_policy(policy: FrontPolicy) {
138 *SHARED_FRONTING_POLICY.write().unwrap() = policy;
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::{ApiClientCore, NO_PARAMS, Url};
146
147 impl Front {
148 pub(crate) fn policy(&self) -> FrontPolicy {
149 self.policy.read().unwrap().clone()
150 }
151 }
152
153 #[test]
155 fn set_policy_independent_client() {
156 let url1 = Url::new(
157 "https://validator.global.ssl.fastly.net",
158 Some(vec!["https://yelp.global.ssl.fastly.net"]),
159 )
160 .unwrap();
161
162 let mut client1 = ClientBuilder::new(url1.clone())
163 .unwrap()
164 .with_fronting(Some(FrontPolicy::Off))
165 .build()
166 .unwrap();
167 assert!(client1.front.policy() == FrontPolicy::Off);
168
169 let client2 = ClientBuilder::new(url1.clone())
170 .unwrap()
171 .with_fronting(Some(FrontPolicy::OnRetry))
172 .build()
173 .unwrap();
174
175 client1.set_front_policy(FrontPolicy::Always);
177 assert!(client1.front.policy() == FrontPolicy::Always);
178
179 assert!(client2.front.policy() == FrontPolicy::OnRetry);
182
183 let req = client1
186 .create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
187 .unwrap()
188 .build()
189 .unwrap();
190
191 let expected_host = url1.host_str().unwrap();
192 assert!(
193 req.headers()
194 .get(reqwest::header::HOST)
195 .is_some_and(|h| h.to_str().unwrap() == expected_host),
196 "{:?} != {:?}",
197 expected_host,
198 req,
199 );
200
201 let expected_front = url1.front_str().unwrap();
202 assert!(
203 req.url()
204 .host()
205 .is_some_and(|url| url.to_string() == expected_front),
206 "{:?} != {:?}",
207 expected_front,
208 req,
209 );
210 }
211
212 #[test]
216 #[cfg(any())] fn set_policy_shared_client() {
218 let url1 = Url::new(
219 "https://validator.global.ssl.fastly.net",
220 Some(vec!["https://yelp.global.ssl.fastly.net"]),
221 )
222 .unwrap();
223
224 Client::set_shared_front_policy(FrontPolicy::Off);
225 assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
226
227 let client1 = ClientBuilder::new(url1.clone())
228 .unwrap()
229 .with_fronting(None)
230 .build()
231 .unwrap();
232 assert!(client1.front.policy() == FrontPolicy::Off);
233
234 let mut client2 = ClientBuilder::new(url1.clone())
235 .unwrap()
236 .with_fronting(Some(FrontPolicy::Off))
237 .build()
238 .unwrap();
239
240 Client::set_shared_front_policy(FrontPolicy::Always);
242 assert!(client1.front.policy() == FrontPolicy::Always);
243
244 assert!(client2.front.policy() == FrontPolicy::Off);
246
247 let req = client1
250 .create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
251 .unwrap()
252 .build()
253 .unwrap();
254
255 let expected_host = url1.host_str().unwrap();
256 assert!(
257 req.headers()
258 .get(reqwest::header::HOST)
259 .is_some_and(|h| h.to_str().unwrap() == expected_host),
260 "{:?} != {:?}",
261 expected_host,
262 req,
263 );
264
265 let expected_front = url1.front_str().unwrap();
266 assert!(
267 req.url()
268 .host()
269 .is_some_and(|url| url.to_string() == expected_front),
270 "{:?} != {:?}",
271 expected_front,
272 req,
273 );
274
275 client2.use_shared_front_policy();
277 assert!(client2.front.policy() == FrontPolicy::Always);
278
279 Client::set_shared_front_policy(FrontPolicy::OnRetry);
282 assert!(client1.front.policy() == FrontPolicy::OnRetry);
283 assert!(client2.front.policy() == FrontPolicy::OnRetry);
284
285 assert!(!client1.front.is_enabled());
286 assert!(!client2.front.is_enabled());
287
288 client1.front.retry_enable();
289 assert!(client1.front.is_enabled());
290 assert!(!client2.front.is_enabled());
291 }
292
293 #[tokio::test]
294 async fn nym_api_works() {
295 let url1 = Url::new(
296 "https://validator.global.ssl.fastly.net",
297 Some(vec!["https://yelp.global.ssl.fastly.net"]),
298 )
299 .unwrap(); let client = ClientBuilder::new(url1)
307 .expect("bad url")
308 .with_fronting(Some(FrontPolicy::Always))
309 .build()
310 .expect("failed to build client");
311
312 let response = client
313 .send_request::<_, (), &str, &str>(
314 reqwest::Method::GET,
315 &["api", "v1", "network", "details"],
316 NO_PARAMS,
317 None,
318 )
319 .await
320 .expect("failed get request");
321
322 assert_eq!(response.status(), 200);
324 }
325
326 #[tokio::test]
327 async fn fallback_on_failure() {
328 let url1 = Url::new(
329 "https://fake-domain.nymtech.net",
330 Some(vec![
331 "https://fake-front-1.nymtech.net",
332 "https://fake-front-2.nymtech.net",
333 ]),
334 )
335 .unwrap();
336 let url2 = Url::new(
337 "https://validator.global.ssl.fastly.net",
338 Some(vec!["https://yelp.global.ssl.fastly.net"]),
339 )
340 .unwrap(); let client = ClientBuilder::new_with_urls(vec![url1, url2])
343 .expect("bad url")
344 .with_fronting(Some(FrontPolicy::Always))
345 .build()
346 .expect("failed to build client");
347
348 assert_eq!(
350 client.current_url().as_str(),
351 "https://fake-domain.nymtech.net/",
352 );
353 assert_eq!(
354 client.current_url().front_str(),
355 Some("fake-front-1.nymtech.net"),
356 );
357
358 let result = client
359 .send_request::<_, (), &str, &str>(
360 reqwest::Method::GET,
361 &["api", "v1", "network", "details"],
362 NO_PARAMS,
363 None,
364 )
365 .await;
366 assert!(result.is_err());
367
368 assert_eq!(
370 client.current_url().as_str(),
371 "https://fake-domain.nymtech.net/",
372 );
373 assert_eq!(
374 client.current_url().front_str(),
375 Some("fake-front-2.nymtech.net"),
376 );
377
378 let result = client
379 .send_request::<_, (), &str, &str>(
380 reqwest::Method::GET,
381 &["api", "v1", "network", "details"],
382 NO_PARAMS,
383 None,
384 )
385 .await;
386 assert!(result.is_err());
387
388 assert_eq!(
390 client.current_url().as_str(),
391 "https://validator.global.ssl.fastly.net/",
392 );
393 assert_eq!(
394 client.current_url().front_str(),
395 Some("yelp.global.ssl.fastly.net"),
396 );
397
398 let response = client
399 .send_request::<_, (), &str, &str>(
400 reqwest::Method::GET,
401 &["api", "v1", "network", "details"],
402 NO_PARAMS,
403 None,
404 )
405 .await
406 .expect("failed get request");
407
408 assert_eq!(response.status(), 200);
409 }
410}