rmqtt-auth-http 0.22.0

HTTP authentication uses a custom HTTP API as the data source, enabling flexible and complex auth logic based on its responses.
Documentation
//! Configuration types for the HTTP authentication / ACL plugin.
//!
//! Defines [`PluginConfig`], [`Retry`], [`Req`], and [`ContentType`] used to
//! configure outbound HTTP requests for authentication and access control.

use std::str::FromStr;
use std::time::Duration;

use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE};
use reqwest::{Method, Url};
use serde::de::{self, Deserializer};
use serde::ser;
use serde::{Deserialize, Serialize};

use rmqtt::{hook::Priority, utils::deserialize_duration, Result};

type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;

/// Top-level configuration for the HTTP auth/ACL plugin.
///
/// Specifies HTTP endpoints for authentication and ACL checks, along with
/// request retry, headers, and error-handling behaviour.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PluginConfig {
    ///Disconnect if publishing is rejected
    #[serde(default = "PluginConfig::disconnect_if_pub_rejected_default")]
    pub disconnect_if_pub_rejected: bool,

    #[serde(default = "PluginConfig::disconnect_if_expiry_default")]
    pub disconnect_if_expiry: bool,

    ///Hook priority
    #[serde(default = "PluginConfig::priority_default")]
    pub priority: Priority,

    ///#Return 'Deny' if http request error otherwise 'Ignore'
    #[serde(default = "PluginConfig::deny_if_error_default")]
    pub deny_if_error: bool,

    #[serde(default = "PluginConfig::http_timeout_default", deserialize_with = "deserialize_duration")]
    pub http_timeout: Duration,
    #[serde(
        default,
        serialize_with = "PluginConfig::serialize_http_headers",
        deserialize_with = "PluginConfig::deserialize_http_headers"
    )]
    pub http_headers: (HeaderMap, HashMap<String, String>),
    #[serde(default)]
    pub http_retry: Retry,

    pub http_auth_req: Option<Req>,
    pub http_acl_req: Option<Req>,
}

impl PluginConfig {
    /// Returns the configured HTTP headers, or `None` if empty.
    pub fn headers(&self) -> Option<&HeaderMap> {
        let (headers, _) = &self.http_headers;
        if !headers.is_empty() {
            Some(headers)
        } else {
            None
        }
    }

    fn disconnect_if_pub_rejected_default() -> bool {
        true
    }

    fn disconnect_if_expiry_default() -> bool {
        false
    }

    fn priority_default() -> Priority {
        100
    }

    fn deny_if_error_default() -> bool {
        true
    }

    fn http_timeout_default() -> Duration {
        Duration::from_secs(5)
    }

    #[inline]
    fn serialize_http_headers<S>(
        headers: &(HeaderMap, HashMap<String, String>),
        s: S,
    ) -> std::result::Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        let (_, headers) = headers;
        headers.serialize(s)
    }

    /// Deserializes HTTP headers from a string-to-string map, parsing each
    /// entry into a `HeaderName` / `HeaderValue` pair.
    #[inline]
    pub fn deserialize_http_headers<'de, D>(
        deserializer: D,
    ) -> std::result::Result<(HeaderMap, HashMap<String, String>), D::Error>
    where
        D: Deserializer<'de>,
    {
        let hs: HashMap<String, String> = HashMap::deserialize(deserializer)?;
        let mut headers = HeaderMap::new();
        for (k, v) in &hs {
            let name = HeaderName::from_str(k).map_err(de::Error::custom)?;
            let value = HeaderValue::from_str(v).map_err(de::Error::custom)?;
            headers.insert(name, value);
        }
        Ok((headers, hs))
    }

    /// Serializes the configuration to a JSON value.
    #[inline]
    pub fn to_json(&self) -> Result<serde_json::Value> {
        Ok(serde_json::to_value(self)?)
    }
}

/// Retry policy for HTTP requests.
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct Retry {
    #[serde(default = "Retry::times_default")]
    pub times: usize,
    //= 3
    #[serde(default = "Retry::interval_default", deserialize_with = "deserialize_duration")]
    pub interval: Duration,
    //= "1s"
    #[serde(default = "Retry::backoff_default")]
    pub backoff: f32, //= 2.0
}

impl Retry {
    fn times_default() -> usize {
        3
    }
    fn interval_default() -> Duration {
        Duration::from_secs(1)
    }
    fn backoff_default() -> f32 {
        2.0
    }
}

/// Content type variant for HTTP request bodies.
#[derive(Debug, Clone)]
pub enum ContentType {
    Json,
    Form,
}

type Headers = (Option<ContentType>, HeaderMap, HashMap<String, String>);

/// An HTTP request definition for authentication or ACL checks.
///
/// Includes URL, HTTP method, custom headers, and query/form parameters.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Req {
    #[serde(serialize_with = "Req::serialize_url", deserialize_with = "Req::deserialize_url")]
    pub url: Url,
    #[serde(serialize_with = "Req::serialize_method", deserialize_with = "Req::deserialize_method")]
    pub method: Method,
    #[serde(
        default,
        serialize_with = "Req::serialize_headers",
        deserialize_with = "Req::deserialize_headers"
    )]
    pub headers: Headers,
    pub params: HashMap<String, String>,
}

impl Req {
    /// Returns `true` if the request method is GET.
    pub fn is_get(&self) -> bool {
        self.method == Method::GET
    }

    /// Returns the request-specific HTTP headers, or `None` if empty.
    pub fn headers(&self) -> Option<&HeaderMap> {
        let (_, headers, _) = &self.headers;
        if !headers.is_empty() {
            Some(headers)
        } else {
            None
        }
    }

    /// Returns `true` if the request body content type is JSON.
    pub fn json_body(&self) -> bool {
        matches!(self.headers, (Some(ContentType::Json), _, _))
    }

    #[inline]
    fn serialize_url<S>(url: &Url, s: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        url.to_string().serialize(s)
    }

    /// Deserializes a URL from its string representation.
    #[inline]
    pub fn deserialize_url<'de, D>(deserializer: D) -> std::result::Result<Url, D::Error>
    where
        D: Deserializer<'de>,
    {
        let v = String::deserialize(deserializer)?;
        Url::from_str(&v).map_err(de::Error::custom)
    }

    #[inline]
    fn serialize_method<S>(method: &Method, s: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        method.to_string().serialize(s)
    }

    /// Deserializes an HTTP method from its uppercase string representation.
    #[inline]
    pub fn deserialize_method<'de, D>(deserializer: D) -> std::result::Result<Method, D::Error>
    where
        D: Deserializer<'de>,
    {
        let v = String::deserialize(deserializer)?.to_uppercase();
        Method::from_str(&v).map_err(de::Error::custom)
    }

    #[inline]
    fn serialize_headers<S>(headers: &Headers, s: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        let (_, _, headers) = headers;
        headers.serialize(s)
    }

    /// Deserializes HTTP headers and detects the content type
    /// (JSON or form-encoded) from the `Content-Type` header.
    #[inline]
    pub fn deserialize_headers<'de, D>(deserializer: D) -> std::result::Result<Headers, D::Error>
    where
        D: Deserializer<'de>,
    {
        let hs: HashMap<String, String> = HashMap::deserialize(deserializer)?;
        let mut headers = HeaderMap::new();
        for (k, v) in &hs {
            let name = HeaderName::from_str(k).map_err(de::Error::custom)?;
            let value = HeaderValue::from_str(v).map_err(de::Error::custom)?;
            headers.insert(name, value);
        }
        let c_type = headers.remove(CONTENT_TYPE).and_then(|h| {
            let h = h.to_str().unwrap_or_default();
            if h.contains("application/x-www-form-urlencoded") {
                Some(ContentType::Form)
            } else if h.contains("application/json") {
                Some(ContentType::Json)
            } else {
                None
            }
        });
        Ok((c_type, headers, hs))
    }
}