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