nym_http_api_client/
fronted.rs

1// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4//! Utilities for and implementation of request tunneling
5
6use std::sync::atomic::{AtomicBool, Ordering};
7use tracing::warn;
8
9use crate::ClientBuilder;
10
11// #[cfg(feature = "tunneling")]
12#[derive(Debug)]
13pub(crate) struct Front {
14    pub(crate) policy: FrontPolicy,
15    enabled: AtomicBool,
16}
17
18impl Clone for Front {
19    fn clone(&self) -> Self {
20        Self {
21            policy: self.policy.clone(),
22            enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
23        }
24    }
25}
26
27impl Front {
28    pub(crate) fn new(policy: FrontPolicy) -> Self {
29        Self {
30            enabled: AtomicBool::new(policy == FrontPolicy::Always),
31            policy,
32        }
33    }
34
35    pub(crate) fn is_enabled(&self) -> bool {
36        match self.policy {
37            FrontPolicy::Off => false,
38            FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
39            FrontPolicy::Always => true,
40        }
41    }
42
43    // Used to indicate that the client hit an error that should trigger the retry policy
44    // to enable fronting.
45    pub(crate) fn retry_enable(&self) {
46        if self.is_enabled() {
47            return;
48        }
49        if matches!(self.policy, FrontPolicy::OnRetry) {
50            self.enabled.store(true, Ordering::Relaxed);
51        }
52    }
53}
54
55#[derive(Debug, Default, PartialEq, Clone)]
56#[cfg(feature = "tunneling")]
57/// Policy for when to use domain fronting for HTTP requests.
58pub enum FrontPolicy {
59    /// Always use domain fronting for all requests.
60    Always,
61    /// Only use domain fronting when retrying failed requests.
62    OnRetry,
63    #[default]
64    /// Never use domain fronting.
65    Off,
66}
67
68impl ClientBuilder {
69    /// Enable and configure request tunneling for API requests.
70    #[cfg(feature = "tunneling")]
71    pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
72        let front = Front::new(policy);
73
74        // Check if any of the supplied urls even support fronting
75        if !self.urls.iter().any(|url| url.has_front()) {
76            warn!(
77                "fronting is enabled, but none of the supplied urls have configured fronting domains"
78            );
79        }
80
81        self.front = Some(front);
82
83        self
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::{ApiClientCore, NO_PARAMS, Url};
91
92    #[tokio::test]
93    async fn nym_api_works() {
94        let url1 = Url::new(
95            "https://validator.global.ssl.fastly.net",
96            Some(vec!["https://yelp.global.ssl.fastly.net"]),
97        )
98        .unwrap(); // fastly
99
100        // let url2 = Url::new(
101        //     "https://validator.nymtech.net",
102        //     Some(vec!["https://cdn77.com"]),
103        // ).unwrap(); // cdn77
104
105        let client = ClientBuilder::new(url1)
106            .expect("bad url")
107            .with_fronting(FrontPolicy::Always)
108            .build()
109            .expect("failed to build client");
110
111        let response = client
112            .send_request::<_, (), &str, &str>(
113                reqwest::Method::GET,
114                &["api", "v1", "network", "details"],
115                NO_PARAMS,
116                None,
117            )
118            .await
119            .expect("failed get request");
120
121        // println!("{response:?}");
122        assert_eq!(response.status(), 200);
123    }
124
125    #[tokio::test]
126    async fn fallback_on_failure() {
127        let url1 = Url::new(
128            "https://fake-domain.nymtech.net",
129            Some(vec![
130                "https://fake-front-1.nymtech.net",
131                "https://fake-front-2.nymtech.net",
132            ]),
133        )
134        .unwrap();
135        let url2 = Url::new(
136            "https://validator.global.ssl.fastly.net",
137            Some(vec!["https://yelp.global.ssl.fastly.net"]),
138        )
139        .unwrap(); // fastly
140
141        let client = ClientBuilder::new_with_urls(vec![url1, url2])
142            .expect("bad url")
143            .with_fronting(FrontPolicy::Always)
144            .build()
145            .expect("failed to build client");
146
147        // Check that the initial configuration has the broken domain and front.
148        assert_eq!(
149            client.current_url().as_str(),
150            "https://fake-domain.nymtech.net/",
151        );
152        assert_eq!(
153            client.current_url().front_str(),
154            Some("fake-front-1.nymtech.net"),
155        );
156
157        let result = client
158            .send_request::<_, (), &str, &str>(
159                reqwest::Method::GET,
160                &["api", "v1", "network", "details"],
161                NO_PARAMS,
162                None,
163            )
164            .await;
165        assert!(result.is_err());
166
167        // Check that the host configuration updated the front on error.
168        assert_eq!(
169            client.current_url().as_str(),
170            "https://fake-domain.nymtech.net/",
171        );
172        assert_eq!(
173            client.current_url().front_str(),
174            Some("fake-front-2.nymtech.net"),
175        );
176
177        let result = client
178            .send_request::<_, (), &str, &str>(
179                reqwest::Method::GET,
180                &["api", "v1", "network", "details"],
181                NO_PARAMS,
182                None,
183            )
184            .await;
185        assert!(result.is_err());
186
187        // Check that the host configuration updated the domain and front on error.
188        assert_eq!(
189            client.current_url().as_str(),
190            "https://validator.global.ssl.fastly.net/",
191        );
192        assert_eq!(
193            client.current_url().front_str(),
194            Some("yelp.global.ssl.fastly.net"),
195        );
196
197        let response = client
198            .send_request::<_, (), &str, &str>(
199                reqwest::Method::GET,
200                &["api", "v1", "network", "details"],
201                NO_PARAMS,
202                None,
203            )
204            .await
205            .expect("failed get request");
206
207        assert_eq!(response.status(), 200);
208    }
209}