Skip to main content

qubit_http/options/
proxy_options.rs

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