feat: better proxy management

This commit is contained in:
zhom
2026-02-16 10:03:27 +04:00
parent 777be9b9dc
commit bb8356eeef
19 changed files with 1066 additions and 324 deletions
+4 -4
View File
@@ -334,7 +334,7 @@ pub struct BrowserRelease {
pub is_prerelease: bool,
}
/// Wayfern version info from https://download.wayfern.com/version.json
/// Wayfern version info from https://donutbrowser.com/wayfern.json
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WayfernVersionInfo {
pub version: String,
@@ -1115,7 +1115,7 @@ impl ApiClient {
Ok(())
}
/// Fetch Wayfern version info from https://download.wayfern.com/version.json
/// Fetch Wayfern version info from https://donutbrowser.com/wayfern.json
pub async fn fetch_wayfern_version_with_caching(
&self,
no_caching: bool,
@@ -1128,8 +1128,8 @@ impl ApiClient {
}
}
log::info!("Fetching Wayfern version from https://download.wayfern.com/version.json");
let url = "https://download.wayfern.com/version.json";
log::info!("Fetching Wayfern version from https://donutbrowser.com/wayfern.json");
let url = "https://donutbrowser.com/wayfern.json";
let response = self
.client
+140 -1
View File
@@ -73,6 +73,12 @@ struct SyncTokenResponse {
sync_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationItem {
pub code: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CloudProxyConfigResponse {
@@ -650,7 +656,11 @@ impl CloudAuthManager {
password: config.password,
};
match PROXY_MANAGER.upsert_cloud_proxy(settings) {
Ok(_) => log::debug!("Cloud proxy synced successfully"),
Ok(_) => {
log::debug!("Cloud proxy synced successfully");
// Propagate credential changes to derived location proxies
PROXY_MANAGER.update_cloud_derived_proxies();
}
Err(e) => log::warn!("Failed to upsert cloud proxy: {e}"),
}
}
@@ -663,6 +673,106 @@ impl CloudAuthManager {
}
}
/// Fetch country list from the cloud backend
pub async fn fetch_countries(&self) -> Result<Vec<LocationItem>, String> {
self
.api_call_with_retry(|access_token| {
let url = format!("{CLOUD_API_URL}/api/proxy/locations/countries");
let client = self.client.clone();
async move {
let response = client
.get(&url)
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await
.map_err(|e| format!("Failed to fetch countries: {e}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Countries fetch failed ({status}): {body}"));
}
response
.json::<Vec<LocationItem>>()
.await
.map_err(|e| format!("Failed to parse countries: {e}"))
}
})
.await
}
/// Fetch state list for a country from the cloud backend
pub async fn fetch_states(&self, country: &str) -> Result<Vec<LocationItem>, String> {
let country = country.to_string();
self
.api_call_with_retry(move |access_token| {
let url = format!(
"{CLOUD_API_URL}/api/proxy/locations/states?country={}",
country
);
let client = reqwest::Client::new();
async move {
let response = client
.get(&url)
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await
.map_err(|e| format!("Failed to fetch states: {e}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("States fetch failed ({status}): {body}"));
}
response
.json::<Vec<LocationItem>>()
.await
.map_err(|e| format!("Failed to parse states: {e}"))
}
})
.await
}
/// Fetch city list for a country+state from the cloud backend
pub async fn fetch_cities(
&self,
country: &str,
state: &str,
) -> Result<Vec<LocationItem>, String> {
let country = country.to_string();
let state = state.to_string();
self
.api_call_with_retry(move |access_token| {
let url = format!(
"{CLOUD_API_URL}/api/proxy/locations/cities?country={}&state={}",
country, state
);
let client = reqwest::Client::new();
async move {
let response = client
.get(&url)
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await
.map_err(|e| format!("Failed to fetch cities: {e}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Cities fetch failed ({status}): {body}"));
}
response
.json::<Vec<LocationItem>>()
.await
.map_err(|e| format!("Failed to parse cities: {e}"))
}
})
.await
}
/// Background loop that refreshes the sync token periodically
pub async fn start_sync_token_refresh_loop(app_handle: tauri::AppHandle) {
loop {
@@ -755,6 +865,35 @@ pub async fn cloud_has_active_subscription() -> Result<bool, String> {
Ok(CLOUD_AUTH.has_active_paid_subscription().await)
}
#[tauri::command]
pub async fn cloud_get_countries() -> Result<Vec<LocationItem>, String> {
CLOUD_AUTH.fetch_countries().await
}
#[tauri::command]
pub async fn cloud_get_states(country: String) -> Result<Vec<LocationItem>, String> {
CLOUD_AUTH.fetch_states(&country).await
}
#[tauri::command]
pub async fn cloud_get_cities(country: String, state: String) -> Result<Vec<LocationItem>, String> {
CLOUD_AUTH.fetch_cities(&country, &state).await
}
#[tauri::command]
pub async fn create_cloud_location_proxy(
name: String,
country: String,
state: Option<String>,
city: Option<String>,
) -> Result<crate::proxy_manager::StoredProxy, String> {
// If no cloud proxy exists yet, attempt to sync it first
if !PROXY_MANAGER.has_cloud_proxy() {
CLOUD_AUTH.sync_cloud_proxy().await;
}
PROXY_MANAGER.create_cloud_location_proxy(name, country, state, city)
}
#[derive(Debug, Serialize)]
pub struct CloudProxyUsage {
pub used_mb: i64,
+4
View File
@@ -1221,6 +1221,10 @@ pub fn run() {
cloud_auth::cloud_refresh_profile,
cloud_auth::cloud_logout,
cloud_auth::cloud_get_proxy_usage,
cloud_auth::cloud_get_countries,
cloud_auth::cloud_get_states,
cloud_auth::cloud_get_cities,
cloud_auth::create_cloud_location_proxy,
cloud_auth::restart_sync_service
])
.run(tauri::generate_context!())
+173 -2
View File
@@ -105,6 +105,14 @@ pub struct StoredProxy {
pub last_sync: Option<u64>,
#[serde(default)]
pub is_cloud_managed: bool,
#[serde(default)]
pub is_cloud_derived: bool,
#[serde(default)]
pub geo_country: Option<String>,
#[serde(default)]
pub geo_state: Option<String>,
#[serde(default)]
pub geo_city: Option<String>,
}
impl StoredProxy {
@@ -116,6 +124,10 @@ impl StoredProxy {
sync_enabled: false,
last_sync: None,
is_cloud_managed: false,
is_cloud_derived: false,
geo_country: None,
geo_state: None,
geo_city: None,
}
}
@@ -400,6 +412,12 @@ impl ProxyManager {
Ok(stored_proxy)
}
// Check if a cloud-managed proxy exists
pub fn has_cloud_proxy(&self) -> bool {
let stored_proxies = self.stored_proxies.lock().unwrap();
stored_proxies.contains_key(CLOUD_PROXY_ID)
}
// Upsert the cloud-managed proxy (create or update)
pub fn upsert_cloud_proxy(&self, proxy_settings: ProxySettings) -> Result<StoredProxy, String> {
let mut stored_proxies = self.stored_proxies.lock().unwrap();
@@ -424,6 +442,10 @@ impl ProxyManager {
sync_enabled: false,
last_sync: None,
is_cloud_managed: true,
is_cloud_derived: false,
geo_country: None,
geo_state: None,
geo_city: None,
};
stored_proxies.insert(CLOUD_PROXY_ID.to_string(), cloud_proxy.clone());
drop(stored_proxies);
@@ -455,6 +477,155 @@ impl ProxyManager {
}
}
// Build a geo-targeted username from base username and location parts
fn build_geo_username(
base_username: &str,
country: &str,
state: &Option<String>,
city: &Option<String>,
) -> String {
let mut username = format!("{}-country-{}", base_username, country);
if let Some(state) = state {
username = format!("{}-state-{}", username, state);
}
if let Some(city) = city {
username = format!("{}-city-{}", username, city);
}
username
}
// Create a cloud-derived location proxy from the base cloud proxy credentials
pub fn create_cloud_location_proxy(
&self,
name: String,
country: String,
state: Option<String>,
city: Option<String>,
) -> Result<StoredProxy, String> {
// Get base cloud proxy credentials
let base_proxy = {
let stored_proxies = self.stored_proxies.lock().unwrap();
stored_proxies
.get(CLOUD_PROXY_ID)
.cloned()
.ok_or_else(|| "No cloud proxy available. Please log in first.".to_string())?
};
let base_username = base_proxy
.proxy_settings
.username
.as_ref()
.ok_or_else(|| "Cloud proxy has no username".to_string())?;
let geo_username = Self::build_geo_username(base_username, &country, &state, &city);
let proxy_settings = ProxySettings {
proxy_type: base_proxy.proxy_settings.proxy_type.clone(),
host: base_proxy.proxy_settings.host.clone(),
port: base_proxy.proxy_settings.port,
username: Some(geo_username),
password: base_proxy.proxy_settings.password.clone(),
};
// Check if name already exists
{
let stored_proxies = self.stored_proxies.lock().unwrap();
if stored_proxies.values().any(|p| p.name == name) {
return Err(format!("Proxy with name '{}' already exists", name));
}
}
let stored_proxy = StoredProxy {
id: uuid::Uuid::new_v4().to_string(),
name,
proxy_settings,
sync_enabled: false,
last_sync: None,
is_cloud_managed: false,
is_cloud_derived: true,
geo_country: Some(country),
geo_state: state,
geo_city: city,
};
{
let mut stored_proxies = self.stored_proxies.lock().unwrap();
stored_proxies.insert(stored_proxy.id.clone(), stored_proxy.clone());
}
if let Err(e) = self.save_proxy(&stored_proxy) {
log::warn!("Failed to save location proxy: {e}");
}
if let Err(e) = events::emit_empty("proxies-changed") {
log::error!("Failed to emit proxies-changed event: {e}");
}
Ok(stored_proxy)
}
// Update all cloud-derived proxies when base cloud proxy credentials change
pub fn update_cloud_derived_proxies(&self) {
let base_proxy = {
let stored_proxies = self.stored_proxies.lock().unwrap();
match stored_proxies.get(CLOUD_PROXY_ID) {
Some(p) => p.clone(),
None => return, // No cloud proxy, nothing to update
}
};
let base_username = match &base_proxy.proxy_settings.username {
Some(u) => u.clone(),
None => return,
};
let mut updated = false;
let mut stored_proxies = self.stored_proxies.lock().unwrap();
for proxy in stored_proxies.values_mut() {
if !proxy.is_cloud_derived {
continue;
}
let country = match &proxy.geo_country {
Some(c) => c.clone(),
None => continue,
};
let geo_username =
Self::build_geo_username(&base_username, &country, &proxy.geo_state, &proxy.geo_city);
proxy.proxy_settings.username = Some(geo_username);
proxy.proxy_settings.password = base_proxy.proxy_settings.password.clone();
proxy.proxy_settings.host = base_proxy.proxy_settings.host.clone();
proxy.proxy_settings.port = base_proxy.proxy_settings.port;
updated = true;
}
if updated {
// Save all updated proxies
let proxies_to_save: Vec<StoredProxy> = stored_proxies
.values()
.filter(|p| p.is_cloud_derived)
.cloned()
.collect();
drop(stored_proxies);
for proxy in &proxies_to_save {
if let Err(e) = self.save_proxy(proxy) {
log::warn!("Failed to save updated derived proxy {}: {e}", proxy.id);
}
}
if let Err(e) = events::emit_empty("proxies-changed") {
log::error!("Failed to emit proxies-changed event: {e}");
}
log::debug!("Updated {} cloud-derived proxies", proxies_to_save.len());
}
}
// Get all stored proxies
pub fn get_stored_proxies(&self) -> Vec<StoredProxy> {
let stored_proxies = self.stored_proxies.lock().unwrap();
@@ -678,7 +849,7 @@ impl ProxyManager {
let stored_proxies = self.stored_proxies.lock().unwrap();
let proxies: Vec<ExportedProxy> = stored_proxies
.values()
.filter(|p| !p.is_cloud_managed)
.filter(|p| !p.is_cloud_managed && !p.is_cloud_derived)
.map(|p| ExportedProxy {
name: p.name.clone(),
proxy_type: p.proxy_settings.proxy_type.clone(),
@@ -704,7 +875,7 @@ impl ProxyManager {
let stored_proxies = self.stored_proxies.lock().unwrap();
stored_proxies
.values()
.filter(|p| !p.is_cloud_managed)
.filter(|p| !p.is_cloud_managed && !p.is_cloud_derived)
.map(|p| Self::build_proxy_url(&p.proxy_settings))
.collect::<Vec<_>>()
.join("\n")
+65 -46
View File
@@ -1050,34 +1050,41 @@ pub async fn set_profile_sync_enabled(
// If enabling, first check that sync settings are configured
if enabled {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
// Cloud auth provides sync settings dynamically — skip local checks
let cloud_logged_in = crate::cloud_auth::CLOUD_AUTH.is_logged_in().await;
if settings.sync_server_url.is_none() {
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"status": "error",
"error": "Sync server not configured. Please configure sync settings first."
}),
);
return Err("Sync server not configured. Please configure sync settings first.".to_string());
}
if !cloud_logged_in {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"status": "error",
"error": "Sync token not configured. Please configure sync settings first."
}),
);
return Err("Sync token not configured. Please configure sync settings first.".to_string());
if settings.sync_server_url.is_none() {
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"status": "error",
"error": "Sync server not configured. Please configure sync settings first."
}),
);
return Err(
"Sync server not configured. Please configure sync settings first.".to_string(),
);
}
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"status": "error",
"error": "Sync token not configured. Please configure sync settings first."
}),
);
return Err("Sync token not configured. Please configure sync settings first.".to_string());
}
}
}
@@ -1240,18 +1247,24 @@ pub async fn set_proxy_sync_enabled(
// If enabling, check that sync settings are configured
if enabled {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
let cloud_logged_in = crate::cloud_auth::CLOUD_AUTH.is_logged_in().await;
if settings.sync_server_url.is_none() {
return Err("Sync server not configured. Please configure sync settings first.".to_string());
}
if !cloud_logged_in {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
return Err("Sync token not configured. Please configure sync settings first.".to_string());
if settings.sync_server_url.is_none() {
return Err(
"Sync server not configured. Please configure sync settings first.".to_string(),
);
}
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
return Err("Sync token not configured. Please configure sync settings first.".to_string());
}
}
}
@@ -1318,18 +1331,24 @@ pub async fn set_group_sync_enabled(
// If enabling, check that sync settings are configured
if enabled {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
let cloud_logged_in = crate::cloud_auth::CLOUD_AUTH.is_logged_in().await;
if settings.sync_server_url.is_none() {
return Err("Sync server not configured. Please configure sync settings first.".to_string());
}
if !cloud_logged_in {
let manager = SettingsManager::instance();
let settings = manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
return Err("Sync token not configured. Please configure sync settings first.".to_string());
if settings.sync_server_url.is_none() {
return Err(
"Sync server not configured. Please configure sync settings first.".to_string(),
);
}
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
if token.is_none() {
return Err("Sync token not configured. Please configure sync settings first.".to_string());
}
}
}