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 use serial_test::serial;
147
148 impl Front {
149 pub(crate) fn policy(&self) -> FrontPolicy {
150 self.policy.read().unwrap().clone()
151 }
152 }
153
154 #[test]
156 fn set_policy_independent_client() {
157 let url1 = Url::new(
158 "https://validator.global.ssl.fastly.net",
159 Some(vec!["https://yelp.global.ssl.fastly.net"]),
160 )
161 .unwrap();
162
163 let mut client1 = ClientBuilder::new(url1.clone())
164 .unwrap()
165 .with_fronting(Some(FrontPolicy::Off))
166 .build()
167 .unwrap();
168 assert!(client1.front.policy() == FrontPolicy::Off);
169
170 let client2 = ClientBuilder::new(url1.clone())
171 .unwrap()
172 .with_fronting(Some(FrontPolicy::OnRetry))
173 .build()
174 .unwrap();
175
176 client1.set_front_policy(FrontPolicy::Always);
178 assert!(client1.front.policy() == FrontPolicy::Always);
179
180 assert!(client2.front.policy() == FrontPolicy::OnRetry);
183
184 let req = client1
187 .create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
188 .unwrap()
189 .build()
190 .unwrap();
191
192 let expected_host = url1.host_str().unwrap();
193 assert!(
194 req.headers()
195 .get(reqwest::header::HOST)
196 .is_some_and(|h| h.to_str().unwrap() == expected_host),
197 "{:?} != {:?}",
198 expected_host,
199 req,
200 );
201
202 let expected_front = url1.front_str().unwrap();
203 assert!(
204 req.url()
205 .host()
206 .is_some_and(|url| url.to_string() == expected_front),
207 "{:?} != {:?}",
208 expected_front,
209 req,
210 );
211 }
212
213 #[test]
217 #[serial]
218 fn set_policy_shared_client() {
219 struct RestoreFrontPolicy(FrontPolicy);
223 impl Drop for RestoreFrontPolicy {
224 fn drop(&mut self) {
225 *SHARED_FRONTING_POLICY.write().unwrap() = self.0.clone();
226 }
227 }
228 let _restore_policy = RestoreFrontPolicy(SHARED_FRONTING_POLICY.read().unwrap().clone());
229
230 let url1 = Url::new(
231 "https://validator.global.ssl.fastly.net",
232 Some(vec!["https://yelp.global.ssl.fastly.net"]),
233 )
234 .unwrap();
235
236 Client::set_shared_front_policy(FrontPolicy::Off);
237 assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
238
239 let client1 = ClientBuilder::new(url1.clone())
240 .unwrap()
241 .with_fronting(None)
242 .build()
243 .unwrap();
244 assert!(client1.front.policy() == FrontPolicy::Off);
245
246 let mut client2 = ClientBuilder::new(url1.clone())
247 .unwrap()
248 .with_fronting(Some(FrontPolicy::Off))
249 .build()
250 .unwrap();
251
252 Client::set_shared_front_policy(FrontPolicy::Always);
254 assert!(client1.front.policy() == FrontPolicy::Always);
255
256 assert!(client2.front.policy() == FrontPolicy::Off);
258
259 let req = client1
262 .create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
263 .unwrap()
264 .build()
265 .unwrap();
266
267 let expected_host = url1.host_str().unwrap();
268 assert!(
269 req.headers()
270 .get(reqwest::header::HOST)
271 .is_some_and(|h| h.to_str().unwrap() == expected_host),
272 "{:?} != {:?}",
273 expected_host,
274 req,
275 );
276
277 let expected_front = url1.front_str().unwrap();
278 assert!(
279 req.url()
280 .host()
281 .is_some_and(|url| url.to_string() == expected_front),
282 "{:?} != {:?}",
283 expected_front,
284 req,
285 );
286
287 client2.use_shared_front_policy();
289 assert!(client2.front.policy() == FrontPolicy::Always);
290
291 Client::set_shared_front_policy(FrontPolicy::OnRetry);
294 assert!(client1.front.policy() == FrontPolicy::OnRetry);
295 assert!(client2.front.policy() == FrontPolicy::OnRetry);
296
297 assert!(!client1.front.is_enabled());
298 assert!(!client2.front.is_enabled());
299
300 client1.front.retry_enable();
301 assert!(client1.front.is_enabled());
302 assert!(!client2.front.is_enabled());
303 }
304
305 #[tokio::test]
306 #[serial]
307 async fn nym_api_works() {
308 let url1 = Url::new(
312 "https://validator.global.ssl.fastly.net",
313 Some(vec!["https://yelp.global.ssl.fastly.net"]),
314 )
315 .unwrap(); let client = ClientBuilder::new(url1)
323 .expect("bad url")
324 .with_fronting(Some(FrontPolicy::Always))
325 .build()
326 .expect("failed to build client");
327
328 let response = client
329 .send_request::<_, (), &str, &str>(
330 reqwest::Method::GET,
331 &["api", "v1", "network", "details"],
332 NO_PARAMS,
333 None,
334 )
335 .await
336 .expect("failed get request");
337
338 assert_eq!(response.status(), 200);
340 }
341
342 #[tokio::test]
343 #[serial]
344 async fn fallback_on_failure() {
345 let url1 = Url::new(
350 "https://fake-domain.nymtech.net",
351 Some(vec![
352 "https://fake-front-1.nymtech.net",
353 "https://fake-front-2.nymtech.net",
354 ]),
355 )
356 .unwrap();
357 let url2 = Url::new(
358 "https://validator.global.ssl.fastly.net",
359 Some(vec!["https://yelp.global.ssl.fastly.net"]),
360 )
361 .unwrap(); let client = ClientBuilder::new_with_urls(vec![url1, url2])
364 .expect("bad url")
365 .with_fronting(Some(FrontPolicy::Always))
366 .build()
367 .expect("failed to build client");
368
369 assert_eq!(
371 client.current_url().as_str(),
372 "https://fake-domain.nymtech.net/",
373 );
374 assert_eq!(
375 client.current_url().front_str(),
376 Some("fake-front-1.nymtech.net"),
377 );
378
379 let result = client
380 .send_request::<_, (), &str, &str>(
381 reqwest::Method::GET,
382 &["api", "v1", "network", "details"],
383 NO_PARAMS,
384 None,
385 )
386 .await;
387 assert!(result.is_err());
388
389 assert_eq!(
391 client.current_url().as_str(),
392 "https://fake-domain.nymtech.net/",
393 );
394 assert_eq!(
395 client.current_url().front_str(),
396 Some("fake-front-2.nymtech.net"),
397 );
398
399 let result = client
400 .send_request::<_, (), &str, &str>(
401 reqwest::Method::GET,
402 &["api", "v1", "network", "details"],
403 NO_PARAMS,
404 None,
405 )
406 .await;
407 assert!(result.is_err());
408
409 assert_eq!(
411 client.current_url().as_str(),
412 "https://validator.global.ssl.fastly.net/",
413 );
414 assert_eq!(
415 client.current_url().front_str(),
416 Some("yelp.global.ssl.fastly.net"),
417 );
418
419 let response = client
420 .send_request::<_, (), &str, &str>(
421 reqwest::Method::GET,
422 &["api", "v1", "network", "details"],
423 NO_PARAMS,
424 None,
425 )
426 .await
427 .expect("failed get request");
428
429 assert_eq!(response.status(), 200);
430 }
431}