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