mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 01:07:53 +02:00
refactor: launch all browsers via proxy
This commit is contained in:
+2
-10
@@ -52,7 +52,7 @@ program
|
||||
},
|
||||
) => {
|
||||
if (action === "start") {
|
||||
let upstreamUrl: string;
|
||||
let upstreamUrl: string | undefined;
|
||||
|
||||
// Build upstream URL from individual components if provided
|
||||
if (options.host && options.proxyPort && options.type) {
|
||||
@@ -69,16 +69,8 @@ program
|
||||
upstreamUrl = `${protocol}://${auth}${options.host}:${options.proxyPort}`;
|
||||
} else if (options.upstream) {
|
||||
upstreamUrl = options.upstream;
|
||||
} else {
|
||||
console.error(
|
||||
"Error: Either --upstream URL or --host, --proxy-port, and --type are required",
|
||||
);
|
||||
console.log(
|
||||
"Example: proxy start --host proxy.example.com --proxy-port 9000 --type http --username user --password pass",
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
// If no upstream is provided, create a direct proxy
|
||||
|
||||
try {
|
||||
const config = await startProxyProcess(upstreamUrl, {
|
||||
|
||||
@@ -2,23 +2,23 @@ import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import getPort from "get-port";
|
||||
import {
|
||||
type ProxyConfig,
|
||||
deleteProxyConfig,
|
||||
generateProxyId,
|
||||
getProxyConfig,
|
||||
isProcessRunning,
|
||||
listProxyConfigs,
|
||||
type ProxyConfig,
|
||||
saveProxyConfig,
|
||||
} from "./proxy-storage";
|
||||
|
||||
/**
|
||||
* Start a proxy in a separate process
|
||||
* @param upstreamUrl The upstream proxy URL
|
||||
* @param upstreamUrl The upstream proxy URL (optional for direct proxy)
|
||||
* @param options Optional configuration
|
||||
* @returns Promise resolving to the proxy configuration
|
||||
*/
|
||||
export async function startProxyProcess(
|
||||
upstreamUrl: string,
|
||||
upstreamUrl?: string,
|
||||
options: { port?: number; ignoreProxyCertificate?: boolean } = {},
|
||||
): Promise<ProxyConfig> {
|
||||
// Generate a unique ID for this proxy
|
||||
@@ -30,7 +30,7 @@ export async function startProxyProcess(
|
||||
// Create the proxy configuration
|
||||
const config: ProxyConfig = {
|
||||
id,
|
||||
upstreamUrl,
|
||||
upstreamUrl: upstreamUrl || "DIRECT",
|
||||
localPort: port,
|
||||
ignoreProxyCertificate: options.ignoreProxyCertificate ?? false,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import tmp from "tmp";
|
||||
|
||||
export interface ProxyConfig {
|
||||
id: string;
|
||||
upstreamUrl: string;
|
||||
upstreamUrl: string; // Can be "DIRECT" for direct proxy
|
||||
localPort?: number;
|
||||
ignoreProxyCertificate?: boolean;
|
||||
localUrl?: string;
|
||||
|
||||
@@ -19,6 +19,10 @@ export async function runProxyWorker(id: string): Promise<void> {
|
||||
port: config.localPort,
|
||||
host: "127.0.0.1",
|
||||
prepareRequestFunction: () => {
|
||||
// If upstreamUrl is "DIRECT", don't use upstream proxy
|
||||
if (config.upstreamUrl === "DIRECT") {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
upstreamProxyUrl: config.upstreamUrl,
|
||||
ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate ?? false,
|
||||
|
||||
@@ -137,7 +137,7 @@ impl BrowserRunner {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
url: Option<String>,
|
||||
local_proxy_settings: Option<&ProxySettings>,
|
||||
_local_proxy_settings: Option<&ProxySettings>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check if browser is disabled due to ongoing update
|
||||
let auto_updater = crate::auto_updater::AutoUpdater::instance();
|
||||
@@ -154,60 +154,42 @@ impl BrowserRunner {
|
||||
// Handle camoufox profiles using nodecar launcher
|
||||
if profile.browser == "camoufox" {
|
||||
if let Some(mut camoufox_config) = profile.camoufox_config.clone() {
|
||||
// Handle proxy settings for camoufox
|
||||
if let Some(proxy_id) = &profile.proxy_id {
|
||||
if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) {
|
||||
println!("Starting proxy for Camoufox profile: {}", profile.name);
|
||||
// Always start a local proxy for Camoufox (for traffic monitoring and geoip support)
|
||||
let upstream_proxy = profile
|
||||
.proxy_id
|
||||
.as_ref()
|
||||
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
|
||||
|
||||
// Start the proxy and get local proxy settings
|
||||
let local_proxy = PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
&stored_proxy,
|
||||
0, // Use 0 as temporary PID, will be updated later
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start proxy for Camoufox: {e}"))?;
|
||||
println!(
|
||||
"Starting local proxy for Camoufox profile: {} (upstream: {})",
|
||||
profile.name,
|
||||
upstream_proxy
|
||||
.as_ref()
|
||||
.map(|p| format!("{}:{}", p.host, p.port))
|
||||
.unwrap_or_else(|| "DIRECT".to_string())
|
||||
);
|
||||
|
||||
// Format proxy URL for camoufox
|
||||
let proxy_url = format!(
|
||||
"{}://{}:{}",
|
||||
if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" {
|
||||
&stored_proxy.proxy_type
|
||||
} else {
|
||||
"http"
|
||||
},
|
||||
local_proxy.host,
|
||||
local_proxy.port
|
||||
);
|
||||
// Start the proxy and get local proxy settings
|
||||
let local_proxy = PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
0, // Use 0 as temporary PID, will be updated later
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?;
|
||||
|
||||
// Add username and password if available
|
||||
let proxy_url = if let (Some(username), Some(password)) =
|
||||
(&stored_proxy.username, &stored_proxy.password)
|
||||
{
|
||||
format!(
|
||||
"{}://{}:{}@{}:{}",
|
||||
if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" {
|
||||
&stored_proxy.proxy_type
|
||||
} else {
|
||||
"http"
|
||||
},
|
||||
username,
|
||||
password,
|
||||
local_proxy.host,
|
||||
local_proxy.port
|
||||
)
|
||||
} else {
|
||||
proxy_url
|
||||
};
|
||||
// Format proxy URL for camoufox - always use HTTP for the local proxy
|
||||
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
|
||||
|
||||
// Set proxy in camoufox config
|
||||
camoufox_config.proxy = Some(proxy_url);
|
||||
// Set proxy in camoufox config
|
||||
camoufox_config.proxy = Some(proxy_url);
|
||||
|
||||
println!("Configured proxy for Camoufox: {:?}", camoufox_config.proxy);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Configured local proxy for Camoufox: {:?}",
|
||||
camoufox_config.proxy
|
||||
);
|
||||
|
||||
// Use the existing config or create a test config if none exists
|
||||
let final_config = if camoufox_config.timezone.is_some()
|
||||
@@ -289,18 +271,14 @@ impl BrowserRunner {
|
||||
// Continue anyway, the error might not be critical
|
||||
}
|
||||
|
||||
// For Chromium browsers, use local proxy settings if available
|
||||
// For Firefox browsers, proxy settings are handled via PAC files
|
||||
let stored_proxy_settings = profile
|
||||
// Get stored proxy settings for later use (removed as we handle this in proxy startup)
|
||||
let _stored_proxy_settings = profile
|
||||
.proxy_id
|
||||
.as_ref()
|
||||
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
|
||||
let proxy_for_launch_args = match browser_type {
|
||||
BrowserType::Chromium | BrowserType::Brave => {
|
||||
local_proxy_settings.or(stored_proxy_settings.as_ref())
|
||||
}
|
||||
_ => None, // Firefox browsers use PAC files, not launch args
|
||||
};
|
||||
|
||||
// For now, don't use proxy in launch args - we'll set it up after launch
|
||||
let proxy_for_launch_args: Option<&ProxySettings> = None;
|
||||
|
||||
// Get profile data path and launch arguments
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
@@ -399,24 +377,56 @@ impl BrowserRunner {
|
||||
// which is already handled in the profile creation process
|
||||
}
|
||||
|
||||
// Start proxy if configured and needed (for Chromium-based browsers)
|
||||
if let Some(proxy_id) = &profile.proxy_id {
|
||||
if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) {
|
||||
println!("Starting proxy for profile: {}", profile.name);
|
||||
// Always start a local proxy for traffic monitoring and potential upstream routing
|
||||
let upstream_proxy = profile
|
||||
.proxy_id
|
||||
.as_ref()
|
||||
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
|
||||
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
&stored_proxy,
|
||||
actual_pid,
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Proxy started successfully for profile: {}", profile.name),
|
||||
Err(e) => println!("Warning: Failed to start proxy: {e}"),
|
||||
println!(
|
||||
"Starting local proxy for profile: {} (upstream: {})",
|
||||
profile.name,
|
||||
upstream_proxy
|
||||
.as_ref()
|
||||
.map(|p| format!("{}:{}", p.host, p.port))
|
||||
.unwrap_or_else(|| "DIRECT".to_string())
|
||||
);
|
||||
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
actual_pid,
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(local_proxy) => {
|
||||
println!(
|
||||
"Local proxy started successfully for profile: {} on port: {}",
|
||||
profile.name, local_proxy.port
|
||||
);
|
||||
|
||||
// For Firefox-based browsers, update the PAC file with the local proxy
|
||||
if matches!(
|
||||
browser_type,
|
||||
BrowserType::Firefox
|
||||
| BrowserType::FirefoxDeveloper
|
||||
| BrowserType::Zen
|
||||
| BrowserType::TorBrowser
|
||||
| BrowserType::MullvadBrowser
|
||||
) {
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir
|
||||
.join(updated_profile.id.to_string())
|
||||
.join("profile");
|
||||
|
||||
if let Err(e) = self.apply_proxy_settings_to_profile(&profile_path, &local_proxy, None) {
|
||||
println!("Warning: Failed to update Firefox proxy settings: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => println!("Warning: Failed to start local proxy: {e}"),
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
@@ -1351,7 +1361,12 @@ pub async fn launch_browser_profile(
|
||||
|
||||
// Start the proxy first
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(app_handle.clone(), &proxy, temp_pid, Some(&profile.name))
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
Some(&proxy),
|
||||
temp_pid,
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(internal_proxy) => {
|
||||
|
||||
@@ -434,7 +434,12 @@ impl ProfileManager {
|
||||
// Browser is running and proxy is enabled, start new proxy
|
||||
if let Some(pid) = profile.process_id {
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(app_handle.clone(), &proxy_settings, pid, Some(profile_name))
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
Some(&proxy_settings),
|
||||
pid,
|
||||
Some(profile_name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(internal_proxy_settings) => {
|
||||
|
||||
@@ -249,10 +249,11 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
// Start a proxy for given proxy settings and associate it with a browser process ID
|
||||
// If proxy_settings is None, starts a direct proxy for traffic monitoring
|
||||
pub async fn start_proxy(
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
proxy_settings: &ProxySettings,
|
||||
proxy_settings: Option<&ProxySettings>,
|
||||
browser_pid: u32,
|
||||
profile_name: Option<&str>,
|
||||
) -> Result<ProxySettings, String> {
|
||||
@@ -273,15 +274,19 @@ impl ProxyManager {
|
||||
// Check if we have a preferred port for this profile
|
||||
let preferred_port = if let Some(name) = profile_name {
|
||||
let profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.get(name).and_then(|settings| {
|
||||
profile_proxies.get(name).and_then(|_settings| {
|
||||
// Find existing proxy with same settings to reuse port
|
||||
let active_proxies = self.active_proxies.lock().unwrap();
|
||||
active_proxies
|
||||
.values()
|
||||
.find(|p| {
|
||||
p.upstream_host == settings.host
|
||||
&& p.upstream_port == settings.port
|
||||
&& p.upstream_type == settings.proxy_type
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
p.upstream_host == proxy_settings.host
|
||||
&& p.upstream_port == proxy_settings.port
|
||||
&& p.upstream_type == proxy_settings.proxy_type
|
||||
} else {
|
||||
p.upstream_type == "DIRECT"
|
||||
}
|
||||
})
|
||||
.map(|p| p.local_port)
|
||||
})
|
||||
@@ -295,20 +300,25 @@ impl ProxyManager {
|
||||
.sidecar("nodecar")
|
||||
.map_err(|e| format!("Failed to create sidecar: {e}"))?
|
||||
.arg("proxy")
|
||||
.arg("start")
|
||||
.arg("--host")
|
||||
.arg(&proxy_settings.host)
|
||||
.arg("--proxy-port")
|
||||
.arg(proxy_settings.port.to_string())
|
||||
.arg("--type")
|
||||
.arg(&proxy_settings.proxy_type);
|
||||
.arg("start");
|
||||
|
||||
// Add credentials if provided
|
||||
if let Some(username) = &proxy_settings.username {
|
||||
nodecar = nodecar.arg("--username").arg(username);
|
||||
}
|
||||
if let Some(password) = &proxy_settings.password {
|
||||
nodecar = nodecar.arg("--password").arg(password);
|
||||
// Add upstream proxy settings if provided, otherwise create direct proxy
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
nodecar = nodecar
|
||||
.arg("--host")
|
||||
.arg(&proxy_settings.host)
|
||||
.arg("--proxy-port")
|
||||
.arg(proxy_settings.port.to_string())
|
||||
.arg("--type")
|
||||
.arg(&proxy_settings.proxy_type);
|
||||
|
||||
// Add credentials if provided
|
||||
if let Some(username) = &proxy_settings.username {
|
||||
nodecar = nodecar.arg("--username").arg(username);
|
||||
}
|
||||
if let Some(password) = &proxy_settings.password {
|
||||
nodecar = nodecar.arg("--password").arg(password);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a preferred port, use it
|
||||
@@ -349,9 +359,13 @@ impl ProxyManager {
|
||||
let proxy_info = ProxyInfo {
|
||||
id: id.to_string(),
|
||||
local_url,
|
||||
upstream_host: proxy_settings.host.clone(),
|
||||
upstream_port: proxy_settings.port,
|
||||
upstream_type: proxy_settings.proxy_type.clone(),
|
||||
upstream_host: proxy_settings
|
||||
.map(|p| p.host.clone())
|
||||
.unwrap_or_else(|| "DIRECT".to_string()),
|
||||
upstream_port: proxy_settings.map(|p| p.port).unwrap_or(0),
|
||||
upstream_type: proxy_settings
|
||||
.map(|p| p.proxy_type.clone())
|
||||
.unwrap_or_else(|| "DIRECT".to_string()),
|
||||
local_port,
|
||||
};
|
||||
|
||||
@@ -363,8 +377,10 @@ impl ProxyManager {
|
||||
|
||||
// Store the profile proxy info for persistence
|
||||
if let Some(name) = profile_name {
|
||||
let mut profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.insert(name.to_string(), proxy_settings.clone());
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
let mut profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.insert(name.to_string(), proxy_settings.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Return proxy settings for the browser
|
||||
|
||||
@@ -700,6 +700,77 @@ async fn test_nodecar_proxy_types() -> Result<(), Box<dyn std::error::Error + Se
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test direct proxy (no upstream) functionality
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_direct_proxy() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let nodecar_path = setup_test().await?;
|
||||
let mut tracker = TestResourceTracker::new(nodecar_path.clone());
|
||||
|
||||
// Test starting a direct proxy (no upstream)
|
||||
let args = ["proxy", "start"];
|
||||
|
||||
println!("Starting direct proxy with nodecar...");
|
||||
let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 30).await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
tracker.cleanup_all().await;
|
||||
return Err(format!("Direct proxy start failed - stdout: {stdout}, stderr: {stderr}").into());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
// Verify proxy configuration structure
|
||||
assert!(config["id"].is_string(), "Proxy ID should be a string");
|
||||
assert!(
|
||||
config["localPort"].is_number(),
|
||||
"Local port should be a number"
|
||||
);
|
||||
assert!(
|
||||
config["localUrl"].is_string(),
|
||||
"Local URL should be a string"
|
||||
);
|
||||
assert_eq!(
|
||||
config["upstreamUrl"].as_str().unwrap(),
|
||||
"DIRECT",
|
||||
"Upstream URL should be DIRECT"
|
||||
);
|
||||
|
||||
let proxy_id = config["id"].as_str().unwrap().to_string();
|
||||
let local_port = config["localPort"].as_u64().unwrap() as u16;
|
||||
tracker.track_proxy(proxy_id.clone());
|
||||
|
||||
println!("Direct proxy started with ID: {proxy_id} on port: {local_port}");
|
||||
|
||||
// Wait for the proxy to start listening
|
||||
let is_listening = TestUtils::wait_for_port_state(local_port, true, 10).await;
|
||||
assert!(
|
||||
is_listening,
|
||||
"Direct proxy should be listening on the assigned port"
|
||||
);
|
||||
|
||||
// Test stopping the proxy
|
||||
let stop_args = ["proxy", "stop", "--id", &proxy_id];
|
||||
let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await?;
|
||||
|
||||
assert!(
|
||||
stop_output.status.success(),
|
||||
"Direct proxy stop should succeed"
|
||||
);
|
||||
|
||||
let port_available = TestUtils::wait_for_port_state(local_port, false, 5).await;
|
||||
assert!(
|
||||
port_available,
|
||||
"Port should be available after stopping direct proxy"
|
||||
);
|
||||
|
||||
println!("Direct proxy test completed successfully");
|
||||
tracker.cleanup_all().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test SOCKS5 proxy chaining - create two proxies where the second uses the first as upstream
|
||||
#[tokio::test]
|
||||
async fn test_nodecar_socks5_proxy_chaining() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
|
||||
Reference in New Issue
Block a user