product-os-proxy 0.0.19

Product OS : Proxy builds on the work of hudsucker, taking it to the next level with a man-in-the-middle proxy server that can tunnel traffic through a VPN utilising Product OS : VPN.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Network proxy configuration including MITM proxy

use product_os_configuration::{ConfigError, ProductOSConfig};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// Default upstream connect timeout (ms) when unset (`0`).
pub const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 30_000;

/// Default per-request timeout (ms) when unset (`0`).
pub const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 120_000;

/// Local file format enum to avoid circular dependency on product-os-browser
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ProxyFileFormat {
    /// CSV format
    #[serde(rename = "csv")]
    #[default]
    Csv,
    /// JSON format
    #[serde(rename = "json")]
    Json,
}

/// Tunnel type for proxy
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum TunnelType {
    /// Tor network tunnel
    Tor,
    /// VPN tunnel
    #[default]
    Vpn,
}

/// Network proxy compression modes
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkProxyCompression {
    /// No compression
    None,
    /// Gzip compression
    #[default]
    Gzip,
    /// Brotli compression
    Brotli,
}

impl NetworkProxyCompression {
    /// Convert compression type to string representation
    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            NetworkProxyCompression::None => "none",
            NetworkProxyCompression::Gzip => "gzip",
            NetworkProxyCompression::Brotli => "br",
        }
    }
}

/// Manipulator action for headers/CSP
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkProxyManipulatorAction {
    /// Insert new value
    Insert,
    /// Update existing value
    Update,
    /// Update or insert (upsert)
    #[default]
    Upsert,
    /// Remove value
    Remove,
}

/// Network proxy configuration.
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct NetworkProxy {
    /// Enable proxy
    pub enable: bool,
    /// Proxy network configuration
    pub network: NetworkProxyNetwork,
    /// Enable tunneling through Tor/VPN
    pub enable_tunnel: bool,
    /// Tunnel settings
    pub tunnel_settings: NetworkProxyTunnel,
    /// Certificate authority for MITM
    pub certificate_authority: Option<NetworkProxyCertificateAuthority>,
    /// Use custom HTTP requester
    pub custom_requester: bool,
    /// Compression mode
    pub compression: NetworkProxyCompression,
    /// Enable content type matchers
    pub enable_content_type_matchers: bool,
    /// Enable content type filters
    pub enable_content_type_filters: bool,
    /// Enable domain filtering
    pub enable_domain_filter: bool,
    /// Enable response caching
    pub enable_cache: bool,
    /// Trust all SSL certificates
    pub trust_all_certificates: bool,
    /// Trust any certificate for specific hostname
    pub trust_any_certificate_for_hostname: bool,
    /// Cache size in bytes
    pub cache_size: i32,
    /// Block requests by file extension
    pub block_extensions: bool,
    /// Block requests by URL pattern
    pub block_urls: bool,
    /// Path to proxy configuration file
    pub configuration_path: Option<String>,
    /// Content matcher regex pattern
    pub content_matcher: String,
    /// Content filter regex pattern
    pub content_filter: String,
    /// Cache matcher pattern
    pub cache_matcher: String,
    /// Extension filter pattern
    pub extension_filter: String,
    /// Resource matcher pattern
    pub resource_matcher: String,
    /// Content replacer patterns
    pub content_replacers: Option<Vec<String>>,
    /// Content injector patterns
    pub content_injectors: Option<Vec<String>>,
    /// CSP header manipulators
    pub content_security_policy_manipulators: BTreeMap<String, NetworkProxyCSPManipulators>,
    /// Request header manipulators
    pub request_header_manipulators: BTreeMap<String, NetworkProxyHeaderManipulators>,
    /// Response header manipulators
    pub response_header_manipulators: BTreeMap<String, NetworkProxyHeaderManipulators>,
    /// URL filter pattern
    pub url_filter: String,
    /// Connection timeout in milliseconds
    pub connect_timeout: u64,
    /// Request timeout in milliseconds
    pub request_timeout: u64,
    /// Use user agent rotation
    pub use_user_agent_list: bool,
    /// Path to user agent list file
    pub user_agent_list_path: String,
    /// User agent list file format
    pub user_agent_list_type: ProxyFileFormat,
    /// Rotate user agent on each request
    pub rotate_user_agent: bool,
    /// User agent rotation frequency
    pub rotate_user_agent_frequency: u32,
}

impl NetworkProxy {
    /// Replace zero timeouts with safe defaults (`0` means unlimited in the MITM stack).
    pub fn apply_timeout_defaults(&mut self) {
        if self.connect_timeout == 0 {
            self.connect_timeout = DEFAULT_CONNECT_TIMEOUT_MS;
        }
        if self.request_timeout == 0 {
            self.request_timeout = DEFAULT_REQUEST_TIMEOUT_MS;
        }
    }
}

impl ProductOSConfig for NetworkProxy {
    const SECTION_KEY: &'static str = "proxy";

    fn validate(&self) -> Result<(), ConfigError> {
        let mut errors = Vec::new();
        if self.connect_timeout == 0 {
            errors.push(format!(
                "connect_timeout must be greater than 0 (or call apply_timeout_defaults())"
            ));
        }
        if self.request_timeout == 0 {
            errors.push(format!(
                "request_timeout must be greater than 0 (or call apply_timeout_defaults())"
            ));
        }
        if errors.is_empty() {
            Ok(())
        } else {
            Err(ConfigError::ValidationError(errors))
        }
    }
}

/// Network proxy server configuration
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyNetwork {
    /// Use secure connections
    pub secure: bool,
    /// Proxy hostname
    pub host: String,
    /// Proxy port
    pub port: u16,
    /// Listen on all network interfaces
    pub listen_all_interfaces: bool,
    /// Allow insecure connections
    pub allow_insecure: bool,
    /// Insecure connection port
    pub insecure_port: u16,
}

/// Proxy tunnel configuration
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyTunnel {
    /// Type of tunnel
    pub tunnel_type: TunnelType,
    /// Number of tunnel pipes
    pub pipe_count: u16,
}

/// Behavior when root CA is not trusted at proxy startup.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum NetworkProxyOnMissingTrust {
    /// Do not check trust at startup.
    Off,
    /// Log a warning and continue.
    Warn,
    /// Guide the user through installation (interactive or logged instructions).
    #[default]
    Guide,
    /// Refuse to start until the CA is trusted.
    Block,
}

/// Trust store target for CA installation and verification.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum NetworkProxyTrustTarget {
    /// Operating system trust store.
    System,
    /// Firefox NSS database.
    Firefox,
    /// Chromium profile NSS database.
    ChromiumProfile,
    /// macOS login keychain (no sudo).
    MacosUserKeychain,
}

impl NetworkProxyTrustTarget {
    /// Parse a config string target name.
    #[must_use]
    pub fn parse(s: &str) -> Option<Self> {
        match s.to_ascii_lowercase().as_str() {
            "system" => Some(Self::System),
            "firefox" => Some(Self::Firefox),
            "chromium-profile" | "chromiumprofile" | "chromium" => Some(Self::ChromiumProfile),
            "macos-user" | "macosuser" | "user-keychain" | "userkeychain" => {
                Some(Self::MacosUserKeychain)
            }
            _ => None,
        }
    }
}

/// Managed on-disk root CA settings.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityManaged {
    /// Enable managed CA load-or-create.
    #[serde(default)]
    pub enabled: bool,
    /// Storage directory for `ca.pem` and `ca.key`.
    pub storage_path: Option<String>,
    /// Root CA common name override.
    pub common_name: Option<String>,
    /// Serve `GET /_product-os/ca.pem` and setup page from the proxy.
    #[serde(default)]
    pub serve_cert_endpoint: bool,
}

impl Default for NetworkProxyCertificateAuthorityManaged {
    fn default() -> Self {
        Self {
            enabled: false,
            storage_path: None,
            common_name: None,
            serve_cert_endpoint: false,
        }
    }
}

/// Trust verification and installation settings.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityTrust {
    /// Verify CA trust before starting the proxy.
    #[serde(default)]
    pub verify_on_startup: bool,
    /// Behavior when the CA is not trusted.
    #[serde(default)]
    pub on_missing_trust: NetworkProxyOnMissingTrust,
    /// Allow automatic trust installation after explicit user consent.
    #[serde(default = "default_true")]
    pub allow_auto_install: bool,
    /// Trust store targets to verify and install into.
    #[serde(default = "default_trust_targets")]
    pub targets: Vec<String>,
    /// After store trust checks, verify HTTPS via CONNECT through the running proxy.
    #[serde(default)]
    pub active_https_probe: bool,
}

fn default_true() -> bool {
    true
}

fn default_trust_targets() -> Vec<String> {
    vec!["system".to_string()]
}

impl Default for NetworkProxyCertificateAuthorityTrust {
    fn default() -> Self {
        Self {
            verify_on_startup: false,
            on_missing_trust: NetworkProxyOnMissingTrust::Guide,
            allow_auto_install: true,
            targets: default_trust_targets(),
            active_https_probe: false,
        }
    }
}

/// Certificate authority configuration for MITM proxy
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthority {
    /// Certificate authority file paths
    pub files: Option<NetworkProxyCertificateAuthorityFiles>,
    /// Managed CA persistence and serving options
    #[serde(default)]
    pub managed: Option<NetworkProxyCertificateAuthorityManaged>,
    /// Trust verification and installation options
    #[serde(default)]
    pub trust: Option<NetworkProxyCertificateAuthorityTrust>,
    /// Certificate attributes (legacy)
    pub attributes: Option<String>,
}

/// Certificate authority file paths
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityFiles {
    /// Path to CA certificate file
    pub cert_file: String,
    /// Path to CA private key file
    pub key_file: String,
}

/// HTTP header manipulator configuration
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyHeaderManipulators {
    /// Regex pattern to match header name
    pub header_name_matcher: Option<String>,
    /// Regex pattern to match header value
    pub header_value_matcher: Option<String>,
    /// Manipulator action
    pub action: NetworkProxyManipulatorAction,
    /// Header name for insert/update
    pub name: Option<String>,
    /// Header value for insert/update
    pub value: Option<String>,
    /// Value to match for update
    pub value_match: Option<String>,
}

/// CSP directive manipulator configuration
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCSPManipulators {
    /// Regex pattern to match directive name
    pub directive_name_matcher: Option<String>,
    /// Regex pattern to match directive value
    pub directive_value_matcher: Option<String>,
    /// Manipulator action
    pub action: NetworkProxyManipulatorAction,
    /// Directive name for insert/update
    pub name: Option<String>,
    /// Directive value for insert/update
    pub value: Option<String>,
    /// Value to match for update
    pub value_match: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use product_os_configuration::ProductOSConfig;

    #[test]
    fn apply_timeout_defaults_replaces_zero() {
        let mut proxy = NetworkProxy::default();
        assert_eq!(proxy.connect_timeout, 0);
        assert_eq!(proxy.request_timeout, 0);
        proxy.apply_timeout_defaults();
        assert_eq!(proxy.connect_timeout, DEFAULT_CONNECT_TIMEOUT_MS);
        assert_eq!(proxy.request_timeout, DEFAULT_REQUEST_TIMEOUT_MS);
        assert!(proxy.validate().is_ok());
    }

    #[test]
    fn validate_rejects_zero_timeouts() {
        let proxy = NetworkProxy::default();
        assert!(proxy.validate().is_err());
    }

    #[test]
    fn network_proxy_deserializes_camel_case_fields() {
        let mut proxy = NetworkProxy::default();
        proxy.connect_timeout = 15_000;
        proxy.request_timeout = 60_000;
        proxy.network.host = "127.0.0.1".into();
        proxy.network.port = 8080;
        proxy.apply_timeout_defaults();
        assert!(proxy.validate().is_ok());
        assert_eq!(proxy.connect_timeout, 15_000);
        assert_eq!(proxy.request_timeout, 60_000);
    }
}