Skip to main content

yt_dlp/client/
proxy.rs

1//! Proxy configuration for HTTP/HTTPS/SOCKS5 proxies.
2//!
3//! This module provides proxy configuration for both reqwest HTTP client
4//! and yt-dlp command-line tool.
5
6use std::fmt;
7
8/// Proxy configuration supporting HTTP, HTTPS, and SOCKS5 proxies.
9///
10/// # Examples
11///
12/// ```rust,no_run
13/// use yt_dlp::client::proxy::{ProxyConfig, ProxyType};
14///
15/// // Simple HTTP proxy
16/// let proxy = ProxyConfig::new(ProxyType::Http, "http://proxy.example.com:8080");
17///
18/// // SOCKS5 proxy with authentication
19/// let proxy = ProxyConfig::new(ProxyType::Socks5, "socks5://proxy.example.com:1080")
20///     .with_auth("username", "password");
21///
22/// // With no-proxy list
23/// let proxy = ProxyConfig::new(ProxyType::Http, "http://proxy.example.com:8080")
24///     .with_no_proxy(vec!["localhost".to_string(), "127.0.0.1".to_string()]);
25/// ```
26#[derive(Clone, Debug)]
27pub struct ProxyConfig {
28    proxy_type: ProxyType,
29    url: String,
30    username: Option<String>,
31    password: Option<String>,
32    no_proxy: Vec<String>,
33}
34
35/// Type of proxy to use.
36#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
37pub enum ProxyType {
38    /// HTTP proxy
39    Http,
40    /// HTTPS proxy
41    Https,
42    /// SOCKS5 proxy
43    Socks5,
44}
45
46impl ProxyConfig {
47    /// Creates a new proxy configuration.
48    ///
49    /// # Arguments
50    ///
51    /// * `proxy_type` - The type of proxy (HTTP, HTTPS, or SOCKS5)
52    /// * `url` - The proxy URL (e.g., "http://proxy.example.com:8080")
53    ///
54    /// # Returns
55    ///
56    /// A new ProxyConfig instance
57    pub fn new(proxy_type: ProxyType, url: impl Into<String>) -> Self {
58        let url = url.into();
59
60        tracing::debug!(
61            proxy_type = ?proxy_type,
62            url = %url,
63            "🔧 Creating proxy config"
64        );
65
66        Self {
67            proxy_type,
68            url,
69            username: None,
70            password: None,
71            no_proxy: Vec::new(),
72        }
73    }
74
75    /// Adds authentication credentials to the proxy.
76    ///
77    /// # Arguments
78    ///
79    /// * `username` - The proxy username
80    /// * `password` - The proxy password
81    ///
82    /// # Returns
83    ///
84    /// Self for method chaining
85    pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
86        let username = username.into();
87
88        tracing::debug!(
89            username = %username,
90            "🔧 Adding proxy authentication"
91        );
92
93        self.username = Some(username);
94        self.password = Some(password.into());
95        self
96    }
97
98    /// Sets the list of domains that should bypass the proxy.
99    ///
100    /// # Arguments
101    ///
102    /// * `no_proxy` - List of domains to bypass (e.g., ["localhost", "127.0.0.1"])
103    ///
104    /// # Returns
105    ///
106    /// Self for method chaining
107    pub fn with_no_proxy(mut self, no_proxy: Vec<String>) -> Self {
108        tracing::debug!(
109            no_proxy = ?no_proxy,
110            count = no_proxy.len(),
111            "🔧 Setting no-proxy list"
112        );
113
114        self.no_proxy = no_proxy;
115        self
116    }
117
118    /// Returns the proxy type.
119    ///
120    /// # Returns
121    ///
122    /// A reference to the configured `ProxyType`.
123    pub fn proxy_type(&self) -> &ProxyType {
124        &self.proxy_type
125    }
126
127    /// Returns the proxy URL.
128    ///
129    /// # Returns
130    ///
131    /// The proxy URL as a string slice.
132    pub fn url(&self) -> &str {
133        &self.url
134    }
135
136    /// Returns the username if authentication is configured.
137    ///
138    /// # Returns
139    ///
140    /// The proxy username, or `None` if no authentication is set.
141    pub fn username(&self) -> Option<&str> {
142        self.username.as_deref()
143    }
144
145    /// Returns the password if authentication is configured.
146    ///
147    /// # Returns
148    ///
149    /// The proxy password, or `None` if no authentication is set.
150    pub fn password(&self) -> Option<&str> {
151        self.password.as_deref()
152    }
153
154    /// Returns the no-proxy list.
155    ///
156    /// # Returns
157    ///
158    /// A slice of domain strings that should bypass the proxy.
159    pub fn no_proxy(&self) -> &[String] {
160        &self.no_proxy
161    }
162
163    /// Builds the complete proxy URL with authentication if configured.
164    ///
165    /// # Returns
166    ///
167    /// The proxy URL with embedded authentication credentials if provided
168    pub fn build_url(&self) -> String {
169        tracing::debug!(has_auth = self.username.is_some(), "🔧 Building proxy URL");
170
171        if let (Some(username), Some(password)) = (&self.username, &self.password) {
172            // URL-encode username and password
173            let username_enc = url::form_urlencoded::byte_serialize(username.as_bytes()).collect::<String>();
174            let password_enc = url::form_urlencoded::byte_serialize(password.as_bytes()).collect::<String>();
175
176            // Extract scheme and host from URL
177            if let Some(idx) = self.url.find("://") {
178                let scheme = &self.url[..idx];
179                let rest = &self.url[idx + 3..];
180                format!("{}://{}:{}@{}", scheme, username_enc, password_enc, rest)
181            } else {
182                // No scheme, just add auth
183                format!("{}:{}@{}", username_enc, password_enc, self.url)
184            }
185        } else {
186            self.url.clone()
187        }
188    }
189
190    /// Converts to reqwest proxy format.
191    ///
192    /// # Returns
193    ///
194    /// Result containing the reqwest Proxy instance
195    ///
196    /// # Errors
197    ///
198    /// Returns error if the proxy URL is invalid
199    pub fn to_reqwest_proxy(&self) -> reqwest::Result<reqwest::Proxy> {
200        let url = self.build_url();
201
202        tracing::debug!(proxy_type = ?self.proxy_type, no_proxy_count = self.no_proxy.len(), "🔧 Converting to reqwest proxy");
203
204        match self.proxy_type {
205            ProxyType::Http => reqwest::Proxy::http(&url),
206            ProxyType::Https => reqwest::Proxy::https(&url),
207            ProxyType::Socks5 => reqwest::Proxy::all(&url),
208        }
209        .map(|mut proxy| {
210            // Add no-proxy domains
211            if !self.no_proxy.is_empty() {
212                proxy = proxy.no_proxy(reqwest::NoProxy::from_string(&self.no_proxy.join(",")));
213            }
214            proxy
215        })
216    }
217
218    /// Converts to yt-dlp proxy argument format.
219    ///
220    /// # Returns
221    ///
222    /// The proxy URL in the format expected by yt-dlp's `--proxy` argument.
223    /// Includes authentication credentials if configured.
224    pub fn to_ytdlp_arg(&self) -> String {
225        self.build_url()
226    }
227}
228
229impl fmt::Display for ProxyType {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            Self::Http => f.write_str("Http"),
233            Self::Https => f.write_str("Https"),
234            Self::Socks5 => f.write_str("Socks5"),
235        }
236    }
237}
238
239impl fmt::Display for ProxyConfig {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(
242            f,
243            "ProxyConfig(type={}, url={}, auth={})",
244            self.proxy_type,
245            self.url,
246            self.username.is_some()
247        )
248    }
249}