Clarify active Windows client architecture

This commit is contained in:
2026-07-07 21:19:41 +03:00
parent a0f41baa36
commit 59f2264a2e
55 changed files with 19554 additions and 8 deletions

View File

@@ -0,0 +1,242 @@
use crate::models::{
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol,
ProxyProtocol, Target,
};
#[cfg(test)]
use crate::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
#[cfg(not(test))]
use crate::adapters::proxy_router::{
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
ProxyRouterRequest,
};
use serde::{Deserialize, Serialize};
pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre";
pub const PROXIFYRE_OUTPUT_FILE: &str = "proxifyre-app-config.json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProxiFyreAdapter {
log_level: String,
bypass_lan: bool,
}
impl ProxiFyreAdapter {
pub fn new(log_level: impl Into<String>, bypass_lan: bool) -> Self {
Self {
log_level: log_level.into(),
bypass_lan,
}
}
pub fn generate_proxifyre_config(
&self,
request: ProxyRouterRequest<'_>,
) -> Result<ProxiFyreConfig, ProxyRouterError> {
let mut proxies = Vec::new();
for profile in request.profiles.iter().filter(|profile| profile.enabled) {
let target = find_target(profile, request.targets)?;
ensure_target_supported(profile, target, request.components)?;
let app_names = app_names_for_profile(profile);
if app_names.is_empty() {
return Err(ProxyRouterError::new(
ProxyRouterErrorKind::EmptyProfileItems,
format!("В профиле '{}' нет приложений для маршрутизации", profile.id),
));
}
proxies.push(ProxiFyreProxy {
app_names,
socks5_proxy_endpoint: format!("{}:{}", target.host, target.port),
supported_protocols: protocols_for_profile(profile),
});
}
Ok(ProxiFyreConfig {
log_level: self.log_level.clone(),
bypass_lan: self.bypass_lan,
proxies,
})
}
}
impl Default for ProxiFyreAdapter {
fn default() -> Self {
Self::new("Info", true)
}
}
impl ProxyRouterAdapter for ProxiFyreAdapter {
fn id(&self) -> &'static str {
PROXIFYRE_ADAPTER_ID
}
fn output_file_name(&self) -> &'static str {
PROXIFYRE_OUTPUT_FILE
}
fn generate_config(
&self,
request: ProxyRouterRequest<'_>,
) -> Result<ProxyRouterGeneratedConfig, ProxyRouterError> {
let config = self.generate_proxifyre_config(request)?;
let enabled_profiles = config.proxies.len();
let routed_apps = config
.proxies
.iter()
.map(|proxy| proxy.app_names.len())
.sum();
let contents = serde_json::to_string_pretty(&config).map_err(|error| {
ProxyRouterError::new(
ProxyRouterErrorKind::Serialization,
format!("Не удалось сериализовать конфиг ProxiFyre: {error}"),
)
})?;
Ok(ProxyRouterGeneratedConfig {
adapter_id: self.id().to_string(),
output_file_name: self.output_file_name().to_string(),
contents,
enabled_profiles,
routed_apps,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProxiFyreConfig {
#[serde(rename = "logLevel")]
pub log_level: String,
#[serde(rename = "bypassLan")]
pub bypass_lan: bool,
pub proxies: Vec<ProxiFyreProxy>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProxiFyreProxy {
#[serde(rename = "appNames")]
pub app_names: Vec<String>,
#[serde(rename = "socks5ProxyEndpoint")]
pub socks5_proxy_endpoint: String,
#[serde(rename = "supportedProtocols")]
pub supported_protocols: Vec<String>,
}
fn find_target<'a>(
profile: &Profile,
targets: &'a [Target],
) -> Result<&'a Target, ProxyRouterError> {
targets
.iter()
.find(|target| target.id == profile.target_id)
.ok_or_else(|| {
ProxyRouterError::new(
ProxyRouterErrorKind::MissingTarget,
format!(
"Профиль '{}' ссылается на отсутствующую цель '{}'",
profile.id, profile.target_id
),
)
})
}
fn ensure_target_supported(
profile: &Profile,
target: &Target,
components: &[ComponentStatus],
) -> Result<(), ProxyRouterError> {
if target.protocol != ProxyProtocol::Socks5 {
return Err(ProxyRouterError::new(
ProxyRouterErrorKind::UnsupportedTargetProtocol,
format!(
"Цель '{}' использует HTTP, но ProxiFyre требует SOCKS5",
target.id
),
));
}
if let Some(required_component) = &target.requires_component {
let Some(status) = components
.iter()
.find(|component| &component.id == required_component)
else {
return Err(ProxyRouterError::new(
ProxyRouterErrorKind::MissingRequiredComponent,
format!(
"Цель '{}' профиля '{}' требует отсутствующий компонент '{}'",
target.id,
profile.id,
component_id_label(required_component)
),
));
};
if !component_is_running(status) {
return Err(ProxyRouterError::new(
ProxyRouterErrorKind::RequiredComponentNotRunning,
format!(
"Цель '{}' профиля '{}' требует запущенный компонент '{}'",
target.id,
profile.id,
component_id_label(required_component)
),
));
}
}
Ok(())
}
fn component_is_running(status: &ComponentStatus) -> bool {
status.installed && status.running && status.state == ComponentState::Running
}
fn app_names_for_profile(profile: &Profile) -> Vec<String> {
let mut names = Vec::new();
for item in &profile.items {
let value = item.value.trim();
if value.is_empty() {
continue;
}
let app_name = match item.item_type {
ProfileItemType::Process | ProfileItemType::Folder | ProfileItemType::Exe => value,
};
if !names.iter().any(|existing| existing == app_name) {
names.push(app_name.to_string());
}
}
names
}
fn protocols_for_profile(profile: &Profile) -> Vec<String> {
let mut protocols = Vec::new();
for protocol in &profile.protocols {
let value = match protocol {
Protocol::Tcp => "TCP",
Protocol::Udp => "UDP",
};
if !protocols.iter().any(|existing| existing == value) {
protocols.push(value.to_string());
}
}
protocols
}
fn component_id_label(component_id: &ComponentId) -> &'static str {
match component_id {
ComponentId::ControlApp => "control-app",
ComponentId::Proxyfier => "proxyfier",
ComponentId::Singbox => "singbox",
}
}