nym-http-api-client 2.0.0

Nym's HTTP API client, examples, and tests
Documentation
//! Url handling for the HTTP API client.
//!
//! This module provides a `FrontedUrl` struct that wraps around the `url::Url` type and adds
//! functionality for handling front domains, which are used for reverse proxying.

use std::fmt::Display;

use itertools::Itertools;
pub use url::ParseError;
use url::form_urlencoded;

/// A trait to try to convert some type into a `FrontedUrl`.
pub trait IntoUrl {
    /// Parse as a valid `FrontedUrl`
    fn to_url(self) -> Result<Url, ParseError>;

    /// Returns the string representation of the URL.
    fn as_str(&self) -> &str;
}

impl IntoUrl for &str {
    fn to_url(self) -> Result<Url, ParseError> {
        let url = url::Url::parse(self)?;
        Ok(url.into())
    }

    fn as_str(&self) -> &str {
        self
    }
}

impl IntoUrl for String {
    fn to_url(self) -> Result<Url, ParseError> {
        let url = url::Url::parse(&self)?;
        Ok(url.into())
    }

    fn as_str(&self) -> &str {
        self
    }
}

impl IntoUrl for reqwest::Url {
    fn to_url(self) -> Result<Url, ParseError> {
        Ok(self.into())
    }

    fn as_str(&self) -> &str {
        self.as_str()
    }
}

/// When configuring fronting, some configurations will require a specific backend host
/// to be used for the request to be properly reverse proxied.
///
/// This type only describes a host and its configured front domains - it carries no rotation
/// state. A [`crate::Client`] tracks, per configured host, which front (if any) is currently in
/// use; see `crate::rotation::RotationManager`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Url {
    url: url::Url,
    fronts: Option<Vec<url::Url>>,
}

impl IntoUrl for Url {
    fn to_url(self) -> Result<Url, ParseError> {
        Ok(self)
    }

    fn as_str(&self) -> &str {
        self.url.as_str()
    }
}

impl Display for Url {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.url)
    }
}

impl From<Url> for url::Url {
    fn from(val: Url) -> Self {
        val.url
    }
}

impl From<reqwest::Url> for Url {
    fn from(url: url::Url) -> Self {
        Self { url, fronts: None }
    }
}

impl From<&reqwest::Url> for Url {
    fn from(url: &url::Url) -> Self {
        Self {
            url: url.clone(),
            fronts: None,
        }
    }
}

impl AsRef<url::Url> for Url {
    fn as_ref(&self) -> &url::Url {
        &self.url
    }
}

impl AsMut<url::Url> for Url {
    fn as_mut(&mut self) -> &mut url::Url {
        &mut self.url
    }
}

impl std::str::FromStr for Url {
    type Err = url::ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let url = url::Url::parse(s)?;
        Ok(Self { url, fronts: None })
    }
}

impl Url {
    /// Create a new `FrontedUrl` instance with the given something that can be parsed as a URL
    /// and optional tunneling domains
    pub fn new<U: reqwest::IntoUrl>(
        url: U,
        fronts: Option<Vec<U>>,
    ) -> Result<Self, reqwest::Error> {
        let mut url = Self {
            url: url.into_url()?,
            fronts: None,
        };

        // ensure that the provided URLs are valid
        if let Some(front_domains) = fronts {
            let f: Vec<reqwest::Url> = front_domains
                .into_iter()
                .map(|front| front.into_url())
                .try_collect()?;
            url.fronts = Some(f);
        }

        Ok(url)
    }

    /// Parse an absolute URL from a string.
    pub fn parse(s: &str) -> Result<Self, ParseError> {
        let url = url::Url::parse(s)?;
        Ok(Self { url, fronts: None })
    }

    /// Returns the underlying URL
    pub fn inner_url(&self) -> &url::Url {
        &self.url
    }

    /// Returns true if the URL has a front domain set
    pub fn has_front(&self) -> bool {
        if let Some(fronts) = &self.fronts {
            return !fronts.is_empty();
        }
        false
    }

    /// Return the string representation of the first configured front host (domain or IP
    /// address) for this URL, if any.
    ///
    /// This does not reflect any client's front-rotation state - it is only meaningful before
    /// rotation has happened, or for hosts with a single configured front. For the front a
    /// [`crate::Client`] is actually using, see its rotation-aware accessors instead.
    pub fn first_front_str(&self) -> Option<&str> {
        self.fronts.as_ref()?.first()?.host_str()
    }

    /// Returns the fronts
    pub fn fronts(&self) -> Option<&[url::Url]> {
        self.fronts.as_deref()
    }

    /// Return the string representation of the host (domain or IP address) for this URL, if any.
    pub fn host_str(&self) -> Option<&str> {
        self.url.host_str()
    }

    /// Return the serialization of this URL.
    ///
    /// This is fast since that serialization is already stored in the inner url::Url struct.
    /// Note that this does not reflect any client's front-rotation state.
    pub fn as_str(&self) -> &str {
        self.url.as_str()
    }

    /// Return the scheme of this URL, lower-cased, as an ASCII string without the ‘:’ delimiter.
    pub fn scheme(&self) -> &str {
        self.url.scheme()
    }

    /// Parse the URL’s query string, if any, as application/x-www-form-urlencoded and return an
    /// iterator of (key, value) pairs.
    pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
        self.url.query_pairs()
    }

    /// Manipulate this URL’s query string, viewed as a sequence of name/value pairs in
    /// application/x-www-form-urlencoded syntax.
    pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> {
        self.url.query_pairs_mut()
    }

    /// Change this URL’s query string. If `query` is `None`, this URL’s query string will be cleared.
    pub fn set_query(&mut self, query: Option<&str>) {
        self.url.set_query(query);
    }

    /// Change this URL’s path.
    pub fn set_path(&mut self, path: &str) {
        self.url.set_path(path);
    }

    /// Change this URL’s scheme.
    pub fn set_scheme(&mut self, scheme: &str) {
        self.url.set_scheme(scheme).unwrap();
    }

    /// Change this URL’s host.
    ///
    /// Removing the host (calling this with None) will also remove any username, password, and port number.
    pub fn set_host(&mut self, host: &str) {
        self.url.set_host(Some(host)).unwrap();
    }

    /// Change this URL’s port number.
    ///
    /// Note that default port numbers are not reflected in the serialization.
    ///
    /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`.
    pub fn set_port(&mut self, port: u16) {
        self.url.set_port(Some(port)).unwrap();
    }

    /// Return an object with methods to manipulate this URL’s path segments.
    ///
    /// Return Err(()) if this URL is cannot-be-a-base.
    pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
        self.url.path_segments()
    }

    /// Return an object with methods to manipulate this URL’s path segments.
    ///
    /// Return Err(()) if this URL is cannot-be-a-base.
    #[allow(clippy::result_unit_err)]
    pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> {
        self.url.path_segments_mut()
    }
}