Skip to main content

nym_http_api_client/
url.rs

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