Skip to main content

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 From<&reqwest::Url> for Url {
127    fn from(url: &url::Url) -> Self {
128        Self {
129            url: url.clone(),
130            fronts: None,
131            current_front: Arc::new(AtomicUsize::new(0)),
132        }
133    }
134}
135
136impl AsRef<url::Url> for Url {
137    fn as_ref(&self) -> &url::Url {
138        &self.url
139    }
140}
141
142impl AsMut<url::Url> for Url {
143    fn as_mut(&mut self) -> &mut url::Url {
144        &mut self.url
145    }
146}
147
148impl std::str::FromStr for Url {
149    type Err = url::ParseError;
150
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        let url = url::Url::parse(s)?;
153        Ok(Self {
154            url,
155            fronts: None,
156            current_front: Arc::new(AtomicUsize::new(0)),
157        })
158    }
159}
160
161impl Url {
162    /// Create a new `Url` instance with the given something that can be parsed as a  URL and
163    /// optional tunneling domains
164    pub fn new<U: reqwest::IntoUrl>(
165        url: U,
166        fronts: Option<Vec<U>>,
167    ) -> Result<Self, reqwest::Error> {
168        let mut url = Self {
169            url: url.into_url()?,
170            fronts: None,
171            current_front: Arc::new(AtomicUsize::new(0)),
172        };
173
174        // ensure that the provided URLs are valid
175        if let Some(front_domains) = fronts {
176            let f: Vec<reqwest::Url> = front_domains
177                .into_iter()
178                .map(|front| front.into_url())
179                .try_collect()?;
180            url.fronts = Some(f);
181        }
182
183        Ok(url)
184    }
185
186    /// Parse an absolute URL from a string.
187    pub fn parse(s: &str) -> Result<Self, ParseError> {
188        let url = url::Url::parse(s)?;
189        Ok(Self {
190            url,
191            fronts: None,
192            current_front: Arc::new(AtomicUsize::new(0)),
193        })
194    }
195
196    /// Returns the underlying URL
197    pub fn inner_url(&self) -> &url::Url {
198        &self.url
199    }
200
201    /// Returns true if the URL has a front domain set
202    pub fn has_front(&self) -> bool {
203        if let Some(fronts) = &self.fronts {
204            return !fronts.is_empty();
205        }
206        false
207    }
208
209    /// Return the string representation of the current front host (domain or IP address) for this
210    /// URL, if any.
211    pub fn front_str(&self) -> Option<&str> {
212        let current = self.current_front.load(Ordering::Relaxed);
213        self.fronts
214            .as_ref()
215            .and_then(|fronts| fronts.get(current))
216            .and_then(|url| url.host_str())
217    }
218
219    /// Returns the fronts
220    pub fn fronts(&self) -> Option<&[url::Url]> {
221        self.fronts.as_deref()
222    }
223
224    /// Return the string representation of the host (domain or IP address) for this URL, if any.
225    pub fn host_str(&self) -> Option<&str> {
226        self.url.host_str()
227    }
228
229    /// Return the serialization of this URL.
230    ///
231    /// This is fast since that serialization is already stored in the inner url::Url struct.
232    pub fn as_str(&self) -> &str {
233        self.url.as_str()
234    }
235
236    /// Returns true if updating the front wraps back to the first front, or if no fronts are set
237    pub fn update(&self) -> bool {
238        if let Some(fronts) = &self.fronts
239            && fronts.len() > 1
240        {
241            let current = self.current_front.load(Ordering::Relaxed);
242            let next = (current + 1) % fronts.len();
243            self.current_front.store(next, Ordering::Relaxed);
244            return next == 0;
245        }
246        true
247    }
248
249    /// Return the scheme of this URL, lower-cased, as an ASCII string without the ‘:’ delimiter.
250    pub fn scheme(&self) -> &str {
251        self.url.scheme()
252    }
253
254    /// Parse the URL’s query string, if any, as application/x-www-form-urlencoded and return an
255    /// iterator of (key, value) pairs.
256    pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
257        self.url.query_pairs()
258    }
259
260    /// Manipulate this URL’s query string, viewed as a sequence of name/value pairs in
261    /// application/x-www-form-urlencoded syntax.
262    pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> {
263        self.url.query_pairs_mut()
264    }
265
266    /// Change this URL’s query string. If `query` is `None`, this URL’s query string will be cleared.
267    pub fn set_query(&mut self, query: Option<&str>) {
268        self.url.set_query(query);
269    }
270
271    /// Change this URL’s path.
272    pub fn set_path(&mut self, path: &str) {
273        self.url.set_path(path);
274    }
275
276    /// Change this URL’s scheme.
277    pub fn set_scheme(&mut self, scheme: &str) {
278        self.url.set_scheme(scheme).unwrap();
279    }
280
281    /// Change this URL’s host.
282    ///
283    /// Removing the host (calling this with None) will also remove any username, password, and port number.
284    pub fn set_host(&mut self, host: &str) {
285        self.url.set_host(Some(host)).unwrap();
286    }
287
288    /// Change this URL’s port number.
289    ///
290    /// Note that default port numbers are not reflected in the serialization.
291    ///
292    /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`.
293    pub fn set_port(&mut self, port: u16) {
294        self.url.set_port(Some(port)).unwrap();
295    }
296
297    /// Return an object with methods to manipulate this URL’s path segments.
298    ///
299    /// Return Err(()) if this URL is cannot-be-a-base.
300    pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
301        self.url.path_segments()
302    }
303
304    /// Return an object with methods to manipulate this URL’s path segments.
305    ///
306    /// Return Err(()) if this URL is cannot-be-a-base.
307    #[allow(clippy::result_unit_err)]
308    pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> {
309        self.url.path_segments_mut()
310    }
311}