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