refactor: launch all browsers via proxy

This commit is contained in:
zhom
2025-08-03 01:08:59 +04:00
parent 62b9768006
commit 77a50c60d1
8 changed files with 217 additions and 114 deletions
+2 -10
View File
@@ -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, {
+4 -4
View File
@@ -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,
};
+1 -1
View File
@@ -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;
+4
View File
@@ -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,
+90 -75
View File
@@ -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) => {
+6 -1
View File
@@ -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) => {
+39 -23
View File
@@ -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
+71
View File
@@ -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>>