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}