nym_http_api_client/
url.rs

1//! Url handling for the HTTP API client.
2//!
3//! This module provides a `Url` struct that wraps around the `url::Url` type and adds
4//! functionality for handling front domains, which are used for reverse proxying.
5
6use std::fmt::Display;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicUsize, Ordering};
9
10use itertools::Itertools;
11pub use url::ParseError;
12use url::form_urlencoded;
13
14/// A trait to try to convert some type into a `Url`.
15pub trait IntoUrl {
16    /// Parse as a valid `Url`
17    fn to_url(self) -> Result<Url, ParseError>;
18
19    /// Returns the string representation of the URL.
20    fn as_str(&self) -> &str;
21}
22
23impl IntoUrl for &str {
24    fn to_url(self) -> Result<Url, ParseError> {
25        let url = url::Url::parse(self)?;
26        Ok(url.into())
27    }
28
29    fn as_str(&self) -> &str {
30        self
31    }
32}
33
34impl IntoUrl for String {
35    fn to_url(self) -> Result<Url, ParseError> {
36        let url = url::Url::parse(&self)?;
37        Ok(url.into())
38    }
39
40    fn as_str(&self) -> &str {
41        self
42    }
43}
44
45impl IntoUrl for reqwest::Url {
46    fn to_url(self) -> Result<Url, ParseError> {
47        Ok(self.into())
48    }
49
50    fn as_str(&self) -> &str {
51        self.as_str()
52    }
53}
54
55/// When configuring fronting, some configurations will require a specific backend host
56/// to be used for the request to be properly reverse proxied.
57#[derive(Debug, Clone)]
58pub struct Url {
59    url: url::Url,
60    fronts: Option<Vec<url::Url>>,
61    current_front: Arc<AtomicUsize>,
62}
63
64impl IntoUrl for Url {
65    fn to_url(self) -> Result<Url, ParseError> {
66        Ok(self)
67    }
68
69    fn as_str(&self) -> &str {
70        self.url.as_str()
71    }
72}
73
74impl PartialEq for Url {
75    fn eq(&self, other: &Self) -> bool {
76        let current = self.current_front.load(Ordering::Relaxed);
77        let other_current = other.current_front.load(Ordering::Relaxed);
78
79        self.fronts == other.fronts && self.url == other.url && current == other_current
80    }
81}
82
83impl Eq for Url {}
84
85impl std::hash::Hash for Url {
86    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87        let current = self.current_front.load(Ordering::Relaxed);
88        self.fronts.hash(state);
89        self.url.hash(state);
90        current.hash(state);
91    }
92}
93
94impl Display for Url {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self.fronts {
97            Some(ref fronts) => {
98                let current = self.current_front.load(Ordering::Relaxed);
99                if let Some(front) = fronts.get(current) {
100                    write!(f, "{front}=>{}", self.url)
101                } else {
102                    write!(f, "{}", self.url)
103                }
104            }
105            None => write!(f, "{}", self.url),
106        }
107    }
108}
109
110impl From<Url> for url::Url {
111    fn from(val: Url) -> Self {
112        val.url
113    }
114}
115
116impl From<reqwest::Url> for Url {
117    fn from(url: url::Url) -> Self {
118        Self {
119            url,
120            fronts: None,
121            current_front: Arc::new(AtomicUsize::new(0)),
122        }
123    }
124}
125
126impl AsRef<url::Url> for Url {
127    fn as_ref(&self) -> &url::Url {
128        &self.url
129    }
130}
131
132impl AsMut<url::Url> for Url {
133    fn as_mut(&mut self) -> &mut url::Url {
134        &mut self.url
135    }
136}
137
138impl std::str::FromStr for Url {
139    type Err = url::ParseError;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        let url = url::Url::parse(s)?;
143        Ok(Self {
144            url,
145            fronts: None,
146            current_front: Arc::new(AtomicUsize::new(0)),
147        })
148    }
149}
150
151impl Url {
152    /// Create a new `Url` instance with the given something that can be parsed as a  URL and
153    /// optional tunneling domains
154    pub fn new<U: reqwest::IntoUrl>(
155        url: U,
156        fronts: Option<Vec<U>>,
157    ) -> Result<Self, reqwest::Error> {
158        let mut url = Self {
159            url: url.into_url()?,
160            fronts: None,
161            current_front: Arc::new(AtomicUsize::new(0)),
162        };
163
164        // ensure that the provided URLs are valid
165        if let Some(front_domains) = fronts {
166            let f: Vec<reqwest::Url> = front_domains
167                .into_iter()
168                .map(|front| front.into_url())
169                .try_collect()?;
170            url.fronts = Some(f);
171        }
172
173        Ok(url)
174    }
175
176    /// Parse an absolute URL from a string.
177    pub fn parse(s: &str) -> Result<Self, ParseError> {
178        let url = url::Url::parse(s)?;
179        Ok(Self {
180            url,
181            fronts: None,
182            current_front: Arc::new(AtomicUsize::new(0)),
183        })
184    }
185
186    /// Returns the underlying URL
187    pub fn inner_url(&self) -> &url::Url {
188        &self.url
189    }
190
191    /// Returns true if the URL has a front domain set
192    pub fn has_front(&self) -> bool {
193        if let Some(fronts) = &self.fronts {
194            return !fronts.is_empty();
195        }
196        false
197    }
198
199    /// Return the string representation of the current front host (domain or IP address) for this
200    /// URL, if any.
201    pub fn front_str(&self) -> Option<&str> {
202        let current = self.current_front.load(Ordering::Relaxed);
203        self.fronts
204            .as_ref()
205            .and_then(|fronts| fronts.get(current))
206            .and_then(|url| url.host_str())
207    }
208
209    /// Returns the fronts
210    pub fn fronts(&self) -> Option<&[url::Url]> {
211        self.fronts.as_deref()
212    }
213
214    /// Return the string representation of the host (domain or IP address) for this URL, if any.
215    pub fn host_str(&self) -> Option<&str> {
216        self.url.host_str()
217    }
218
219    /// Return the serialization of this URL.
220    ///
221    /// This is fast since that serialization is already stored in the inner url::Url struct.
222    pub fn as_str(&self) -> &str {
223        self.url.as_str()
224    }
225
226    /// Returns true if updating the front wraps back to the first front, or if no fronts are set
227    pub fn update(&self) -> bool {
228        if let Some(fronts) = &self.fronts
229            && fronts.len() > 1
230        {
231            let current = self.current_front.load(Ordering::Relaxed);
232            let next = (current + 1) % fronts.len();
233            self.current_front.store(next, Ordering::Relaxed);
234            return next == 0;
235        }
236        true
237    }
238
239    /// Return the scheme of this URL, lower-cased, as an ASCII string without the ‘:’ delimiter.
240    pub fn scheme(&self) -> &str {
241        self.url.scheme()
242    }
243
244    /// Parse the URL’s query string, if any, as application/x-www-form-urlencoded and return an
245    /// iterator of (key, value) pairs.
246    pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
247        self.url.query_pairs()
248    }
249
250    /// Manipulate this URL’s query string, viewed as a sequence of name/value pairs in
251    /// application/x-www-form-urlencoded syntax.
252    pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> {
253        self.url.query_pairs_mut()
254    }
255
256    /// Change this URL’s query string. If `query` is `None`, this URL’s query string will be cleared.
257    pub fn set_query(&mut self, query: Option<&str>) {
258        self.url.set_query(query);
259    }
260
261    /// Change this URL’s path.
262    pub fn set_path(&mut self, path: &str) {
263        self.url.set_path(path);
264    }
265
266    /// Change this URL’s scheme.
267    pub fn set_scheme(&mut self, scheme: &str) {
268        self.url.set_scheme(scheme).unwrap();
269    }
270
271    /// Change this URL’s host.
272    ///
273    /// Removing the host (calling this with None) will also remove any username, password, and port number.
274    pub fn set_host(&mut self, host: &str) {
275        self.url.set_host(Some(host)).unwrap();
276    }
277
278    /// Change this URL’s port number.
279    ///
280    /// Note that default port numbers are not reflected in the serialization.
281    ///
282    /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`.
283    pub fn set_port(&mut self, port: u16) {
284        self.url.set_port(Some(port)).unwrap();
285    }
286
287    /// Return an object with methods to manipulate this URL’s path segments.
288    ///
289    /// Return Err(()) if this URL is cannot-be-a-base.
290    pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
291        self.url.path_segments()
292    }
293
294    /// Return an object with methods to manipulate this URL’s path segments.
295    ///
296    /// Return Err(()) if this URL is cannot-be-a-base.
297    #[allow(clippy::result_unit_err)]
298    pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> {
299        self.url.path_segments_mut()
300    }
301}