mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-10 12:07:33 +02:00
feat: better proxy management
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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!())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user