use product_os_configuration::{ConfigError, ProductOSConfig};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 30_000;
pub const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 120_000;
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ProxyFileFormat {
#[serde(rename = "csv")]
#[default]
Csv,
#[serde(rename = "json")]
Json,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum TunnelType {
Tor,
#[default]
Vpn,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkProxyCompression {
None,
#[default]
Gzip,
Brotli,
}
impl NetworkProxyCompression {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
NetworkProxyCompression::None => "none",
NetworkProxyCompression::Gzip => "gzip",
NetworkProxyCompression::Brotli => "br",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkProxyManipulatorAction {
Insert,
Update,
#[default]
Upsert,
Remove,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct NetworkProxy {
pub enable: bool,
pub network: NetworkProxyNetwork,
pub enable_tunnel: bool,
pub tunnel_settings: NetworkProxyTunnel,
pub certificate_authority: Option<NetworkProxyCertificateAuthority>,
pub custom_requester: bool,
pub compression: NetworkProxyCompression,
pub enable_content_type_matchers: bool,
pub enable_content_type_filters: bool,
pub enable_domain_filter: bool,
pub enable_cache: bool,
pub trust_all_certificates: bool,
pub trust_any_certificate_for_hostname: bool,
pub cache_size: i32,
pub block_extensions: bool,
pub block_urls: bool,
pub configuration_path: Option<String>,
pub content_matcher: String,
pub content_filter: String,
pub cache_matcher: String,
pub extension_filter: String,
pub resource_matcher: String,
pub content_replacers: Option<Vec<String>>,
pub content_injectors: Option<Vec<String>>,
pub content_security_policy_manipulators: BTreeMap<String, NetworkProxyCSPManipulators>,
pub request_header_manipulators: BTreeMap<String, NetworkProxyHeaderManipulators>,
pub response_header_manipulators: BTreeMap<String, NetworkProxyHeaderManipulators>,
pub url_filter: String,
pub connect_timeout: u64,
pub request_timeout: u64,
pub use_user_agent_list: bool,
pub user_agent_list_path: String,
pub user_agent_list_type: ProxyFileFormat,
pub rotate_user_agent: bool,
pub rotate_user_agent_frequency: u32,
}
impl NetworkProxy {
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))
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyNetwork {
pub secure: bool,
pub host: String,
pub port: u16,
pub listen_all_interfaces: bool,
pub allow_insecure: bool,
pub insecure_port: u16,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyTunnel {
pub tunnel_type: TunnelType,
pub pipe_count: u16,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum NetworkProxyOnMissingTrust {
Off,
Warn,
#[default]
Guide,
Block,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum NetworkProxyTrustTarget {
System,
Firefox,
ChromiumProfile,
MacosUserKeychain,
}
impl NetworkProxyTrustTarget {
#[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,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityManaged {
#[serde(default)]
pub enabled: bool,
pub storage_path: Option<String>,
pub common_name: Option<String>,
#[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,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityTrust {
#[serde(default)]
pub verify_on_startup: bool,
#[serde(default)]
pub on_missing_trust: NetworkProxyOnMissingTrust,
#[serde(default = "default_true")]
pub allow_auto_install: bool,
#[serde(default = "default_trust_targets")]
pub targets: Vec<String>,
#[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,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthority {
pub files: Option<NetworkProxyCertificateAuthorityFiles>,
#[serde(default)]
pub managed: Option<NetworkProxyCertificateAuthorityManaged>,
#[serde(default)]
pub trust: Option<NetworkProxyCertificateAuthorityTrust>,
pub attributes: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCertificateAuthorityFiles {
pub cert_file: String,
pub key_file: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyHeaderManipulators {
pub header_name_matcher: Option<String>,
pub header_value_matcher: Option<String>,
pub action: NetworkProxyManipulatorAction,
pub name: Option<String>,
pub value: Option<String>,
pub value_match: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyCSPManipulators {
pub directive_name_matcher: Option<String>,
pub directive_value_matcher: Option<String>,
pub action: NetworkProxyManipulatorAction,
pub name: Option<String>,
pub value: Option<String>,
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);
}
}