Skip to main content

qubit_http/options/
proxy_options.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9
10use qubit_config::{ConfigReader, ConfigResult};
11
12use super::HttpConfigError;
13
14use super::proxy_type::ProxyType;
15
16/// Outbound proxy configuration applied when building the reqwest client.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ProxyOptions {
19    /// Whether proxy is enabled.
20    pub enabled: bool,
21    /// Proxy type.
22    pub proxy_type: ProxyType,
23    /// Proxy host.
24    pub host: Option<String>,
25    /// Proxy port.
26    pub port: Option<u16>,
27    /// Proxy username.
28    pub username: Option<String>,
29    /// Proxy password.
30    pub password: Option<String>,
31}
32
33impl Default for ProxyOptions {
34    /// Proxy disabled; type HTTP; no host, port, or credentials.
35    ///
36    /// # Returns
37    /// Default [`ProxyOptions`].
38    fn default() -> Self {
39        Self {
40            enabled: false,
41            proxy_type: ProxyType::Http,
42            host: None,
43            port: None,
44            username: None,
45            password: None,
46        }
47    }
48}
49
50struct ProxyConfigInput {
51    enabled: Option<bool>,
52    proxy_type: Option<String>,
53    host: Option<String>,
54    port: Option<u16>,
55    username: Option<String>,
56    password: Option<String>,
57}
58
59fn read_proxy_config<R>(config: &R) -> ConfigResult<ProxyConfigInput>
60where
61    R: ConfigReader + ?Sized,
62{
63    Ok(ProxyConfigInput {
64        enabled: config.get_optional("enabled")?,
65        proxy_type: config.get_optional_string("proxy_type")?,
66        host: config.get_optional_string("host")?,
67        port: config.get_optional("port")?,
68        username: config.get_optional_string("username")?,
69        password: config.get_optional_string("password")?,
70    })
71}
72
73impl ProxyOptions {
74    /// Reads proxy settings from `config` using **relative** keys.
75    ///
76    /// # Parameters
77    /// - `config`: Any [`ConfigReader`] (e.g. `config.prefix_view("proxy")`).
78    ///
79    /// Keys read:
80    /// - `enabled`
81    /// - `proxy_type`
82    /// - `host`
83    /// - `port`
84    /// - `username`
85    /// - `password`
86    ///
87    /// # Returns
88    /// Populated [`ProxyOptions`] or [`HttpConfigError`].
89    pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
90    where
91        R: ConfigReader + ?Sized,
92    {
93        let raw = read_proxy_config(config).map_err(HttpConfigError::from)?;
94
95        let mut opts = ProxyOptions::default();
96        if let Some(v) = raw.enabled {
97            opts.enabled = v;
98        }
99        if let Some(s) = raw.proxy_type {
100            opts.proxy_type = parse_proxy_type("proxy_type", &s)?;
101        }
102        opts.host = raw.host;
103        if let Some(p) = raw.port {
104            opts.port = Some(p);
105        }
106        opts.username = raw.username;
107        opts.password = raw.password;
108
109        Ok(opts)
110    }
111
112    /// Validates proxy options for internal consistency.
113    ///
114    /// # Returns
115    /// - `Ok(())` when disabled or when enabled with valid host/port and credential pairing.
116    /// - `Err(HttpConfigError)` if proxy is enabled but host/port invalid, or password without username.
117    pub fn validate(&self) -> Result<(), HttpConfigError> {
118        if self.enabled {
119            if self.host.is_none() {
120                return Err(HttpConfigError::missing(
121                    "proxy.host",
122                    "Proxy is enabled but host is missing",
123                ));
124            }
125            match self.port {
126                None => {
127                    return Err(HttpConfigError::missing(
128                        "proxy.port",
129                        "Proxy is enabled but port is missing",
130                    ));
131                }
132                Some(0) => {
133                    return Err(HttpConfigError::invalid_value(
134                        "proxy.port",
135                        "Proxy port must be greater than 0",
136                    ));
137                }
138                _ => {}
139            }
140        }
141        if self.username.is_none() && self.password.is_some() {
142            return Err(HttpConfigError::missing(
143                "proxy.username",
144                "Proxy password is configured but username is missing",
145            ));
146        }
147        Ok(())
148    }
149}
150
151fn parse_proxy_type(path: &str, s: &str) -> Result<ProxyType, HttpConfigError> {
152    match s.to_lowercase().as_str() {
153        "http" => Ok(ProxyType::Http),
154        "https" => Ok(ProxyType::Https),
155        "socks5" | "socks5h" => Ok(ProxyType::Socks5),
156        other => Err(HttpConfigError::invalid_value(
157            path,
158            format!(
159                "Unknown proxy type '{}'; expected http, https, or socks5",
160                other
161            ),
162        )),
163    }
164}