diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index c0f98e9..24d751c 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -11,10 +11,15 @@ import { runProxyWorker } from "./proxy-worker"; program .command("proxy") .argument("", "start, stop, or list proxies") + .option("-h, --host ", "upstream proxy host") + .option("-P, --proxy-port ", "upstream proxy port", Number.parseInt) .option( - "-u, --upstream ", - "upstream proxy URL (protocol://[username:password@]host:port)" + "-t, --type ", + "upstream proxy type (http, https, socks4, socks5)", + "http" ) + .option("-u, --username ", "upstream proxy username") + .option("-w, --password ", "upstream proxy password") .option( "-p, --port ", "local port to use (random if not specified)", @@ -27,23 +32,35 @@ program async ( action: string, options: { - upstream?: string; + host?: string; + proxyPort?: number; + type?: string; + username?: string; + password?: string; port?: number; ignoreCertificate?: boolean; id?: string; } ) => { if (action === "start") { - if (!options.upstream) { - console.error("Error: Upstream proxy URL is required"); + if (!options.host || !options.proxyPort) { + console.error("Error: Upstream proxy host and port are required"); console.log( - "Example: proxy start -u http://username:password@proxy.example.com:8080" + "Example: proxy start -h proxy.example.com -P 8080 -t http -u username -w password" ); return; } try { - const config = await startProxyProcess(options.upstream, { + // Construct the upstream URL with credentials if provided + let upstreamProxyUrl: string; + if (options.username && options.password) { + upstreamProxyUrl = `${options.type}://${options.username}:${options.password}@${options.host}:${options.proxyPort}`; + } else { + upstreamProxyUrl = `${options.type}://${options.host}:${options.proxyPort}`; + } + + const config = await startProxyProcess(upstreamProxyUrl, { port: options.port, ignoreProxyCertificate: options.ignoreCertificate, }); @@ -56,14 +73,25 @@ program const stopped = await stopProxyProcess(options.id); console.log(`{ "success": ${stopped}}`); - } else if (options.upstream) { - // Find proxies with this upstream URL - const configs = listProxyConfigs().filter( - (config) => config.upstreamUrl === options.upstream - ); + } else if (options.host && options.proxyPort && options.type) { + // Find proxies with matching upstream details + const configs = listProxyConfigs().filter((config) => { + try { + const url = new URL(config.upstreamUrl); + return ( + url.hostname === options.host && + Number.parseInt(url.port) === options.proxyPort && + url.protocol.replace(":", "") === options.type + ); + } catch { + return false; + } + }); if (configs.length === 0) { - console.error(`No proxies found for ${options.upstream}`); + console.error( + `No proxies found for ${options.host}:${options.proxyPort}` + ); return; } diff --git a/nodecar/src/proxy.ts b/nodecar/src/proxy.ts index b05dc8b..13700c9 100644 --- a/nodecar/src/proxy.ts +++ b/nodecar/src/proxy.ts @@ -1,7 +1,7 @@ -import { - startProxyProcess, - stopProxyProcess, - stopAllProxyProcesses +import { + startProxyProcess, + stopProxyProcess, + stopAllProxyProcesses, } from "./proxy-runner"; import { listProxyConfigs } from "./proxy-storage"; @@ -9,41 +9,71 @@ import { listProxyConfigs } from "./proxy-storage"; interface ProxyOptions { port?: number; ignoreProxyCertificate?: boolean; + username?: string; + password?: string; } /** * Start a local proxy server that forwards to an upstream proxy - * @param upstreamProxyUrl The upstream proxy URL (protocol://[username:password@]host:port) - * @param options Optional configuration + * @param upstreamProxyHost The upstream proxy host + * @param upstreamProxyPort The upstream proxy port + * @param upstreamProxyType The upstream proxy type (http, https, socks4, socks5) + * @param options Optional configuration including credentials * @returns Promise resolving to the local proxy URL */ export async function startProxy( - upstreamProxyUrl: string, + upstreamProxyHost: string, + upstreamProxyPort: number, + upstreamProxyType: string, options: ProxyOptions = {} ): Promise { + // Construct the upstream proxy URL with credentials if provided + let upstreamProxyUrl: string; + if (options.username && options.password) { + upstreamProxyUrl = `${upstreamProxyType}://${options.username}:${options.password}@${upstreamProxyHost}:${upstreamProxyPort}`; + } else { + upstreamProxyUrl = `${upstreamProxyType}://${upstreamProxyHost}:${upstreamProxyPort}`; + } + const config = await startProxyProcess(upstreamProxyUrl, { port: options.port, ignoreProxyCertificate: options.ignoreProxyCertificate, }); - + return config.localUrl || `http://localhost:${config.localPort}`; } /** - * Stop a specific proxy by its upstream URL - * @param upstreamProxyUrl The upstream proxy URL to stop + * Stop a specific proxy by its upstream host, port, and type + * @param upstreamProxyHost The upstream proxy host + * @param upstreamProxyPort The upstream proxy port + * @param upstreamProxyType The upstream proxy type * @returns Promise resolving to true if proxy was found and stopped, false otherwise */ -export async function stopProxy(upstreamProxyUrl: string): Promise { - // Find all proxies with this upstream URL - const configs = listProxyConfigs().filter( - config => config.upstreamUrl === upstreamProxyUrl - ); - +export async function stopProxy( + upstreamProxyHost: string, + upstreamProxyPort: number, + upstreamProxyType: string +): Promise { + // Find all proxies with matching upstream details (ignoring credentials in URL) + const configs = listProxyConfigs().filter((config) => { + // Parse the upstream URL to extract host, port, and type + try { + const url = new URL(config.upstreamUrl); + return ( + url.hostname === upstreamProxyHost && + Number.parseInt(url.port) === upstreamProxyPort && + url.protocol.replace(":", "") === upstreamProxyType + ); + } catch { + return false; + } + }); + if (configs.length === 0) { return false; } - + // Stop all matching proxies let success = true; for (const config of configs) { @@ -52,7 +82,7 @@ export async function stopProxy(upstreamProxyUrl: string): Promise { success = false; } } - + return success; } @@ -61,7 +91,7 @@ export async function stopProxy(upstreamProxyUrl: string): Promise { * @returns Array of upstream proxy URLs */ export function getActiveProxies(): string[] { - return listProxyConfigs().map(config => config.upstreamUrl); + return listProxyConfigs().map((config) => config.upstreamUrl); } /** diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index a45d430..9b1e56a 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -9,6 +9,8 @@ pub struct ProxySettings { pub proxy_type: String, // "http", "https", "socks4", or "socks5" pub host: String, pub port: u16, + pub username: Option, + pub password: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -860,6 +862,8 @@ mod tests { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, + username: None, + password: None, }; assert!(proxy.enabled); @@ -873,6 +877,8 @@ mod tests { proxy_type: "socks5".to_string(), host: "proxy.example.com".to_string(), port: 1080, + username: None, + password: None, }; assert_eq!(socks_proxy.proxy_type, "socks5"); @@ -949,6 +955,8 @@ mod tests { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, + username: None, + password: None, }; // Test that it can be serialized (implements Serialize) diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 60603d4..2fab261 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1988,14 +1988,13 @@ impl BrowserRunner { if !proxy_active { // Browser is running but proxy is not - restart the proxy - if let Some((upstream_url, _preferred_port)) = - PROXY_MANAGER.get_profile_proxy_info(&inner_profile.name) + if let Some(proxy_settings) = PROXY_MANAGER.get_profile_proxy_info(&inner_profile.name) { // Restart the proxy with the same configuration match PROXY_MANAGER .start_proxy( app_handle, - &upstream_url, + &proxy_settings, inner_profile.process_id.unwrap(), Some(&inner_profile.name), ) @@ -2171,12 +2170,9 @@ pub async fn launch_browser_profile( if proxy.enabled { // Get the process ID if let Some(pid) = updated_profile.process_id { - // Start a proxy for the upstream URL - let upstream_url = format!("{}://{}:{}", proxy.proxy_type, proxy.host, proxy.port); - // Start the proxy match PROXY_MANAGER - .start_proxy(app_handle.clone(), &upstream_url, pid, Some(&profile.name)) + .start_proxy(app_handle.clone(), proxy, pid, Some(&profile.name)) .await { Ok(internal_proxy_settings) => { @@ -2628,6 +2624,8 @@ mod tests { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, + username: None, + password: None, }; let profile = runner @@ -2683,6 +2681,8 @@ mod tests { proxy_type: "socks5".to_string(), host: "192.168.1.1".to_string(), port: 1080, + username: None, + password: None, }; let updated_profile = runner @@ -2838,6 +2838,8 @@ mod tests { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, + username: None, + password: None, }; let profile_with_proxy = runner diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index eeffaf4..77e9143 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -11,7 +11,9 @@ use crate::browser::ProxySettings; pub struct ProxyInfo { pub id: String, pub local_url: String, - pub upstream_url: String, + pub upstream_host: String, + pub upstream_port: u16, + pub upstream_type: String, pub local_port: u16, } @@ -19,7 +21,7 @@ pub struct ProxyInfo { pub struct ProxyManager { active_proxies: Mutex>, // Maps browser process ID to proxy info // Store proxy info by profile name for persistence across browser restarts - profile_proxies: Mutex>, // Maps profile name to (upstream_url, port) + profile_proxies: Mutex>, // Maps profile name to proxy settings } impl ProxyManager { @@ -30,11 +32,11 @@ impl ProxyManager { } } - // Start a proxy for a given upstream URL and associate it with a browser process ID + // Start a proxy for given proxy settings and associate it with a browser process ID pub async fn start_proxy( &self, app_handle: tauri::AppHandle, - upstream_url: &str, + proxy_settings: &ProxySettings, browser_pid: u32, profile_name: Option<&str>, ) -> Result { @@ -44,9 +46,11 @@ impl ProxyManager { if let Some(proxy) = proxies.get(&browser_pid) { return Ok(ProxySettings { enabled: true, - proxy_type: "http".to_string(), + proxy_type: proxy.upstream_type.clone(), host: "localhost".to_string(), port: proxy.local_port, + username: None, + password: None, }); } } @@ -54,7 +58,18 @@ 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).map(|(_, port)| *port) + 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 + }) + .map(|p| p.local_port) + }) } else { None }; @@ -66,8 +81,20 @@ impl ProxyManager { .unwrap() .arg("proxy") .arg("start") - .arg("-u") - .arg(upstream_url); + .arg("-h") + .arg(&proxy_settings.host) + .arg("-P") + .arg(proxy_settings.port.to_string()) + .arg("-t") + .arg(&proxy_settings.proxy_type); + + // Add credentials if provided + if let Some(username) = &proxy_settings.username { + nodecar = nodecar.arg("-u").arg(username); + } + if let Some(password) = &proxy_settings.password { + nodecar = nodecar.arg("-w").arg(password); + } // If we have a preferred port, use it if let Some(port) = preferred_port { @@ -95,15 +122,13 @@ impl ProxyManager { .as_str() .ok_or("Missing local URL")? .to_string(); - let upstream_url_str = json["upstreamUrl"] - .as_str() - .ok_or("Missing upstream URL")? - .to_string(); let proxy_info = ProxyInfo { id: id.to_string(), local_url, - upstream_url: upstream_url_str.clone(), + upstream_host: proxy_settings.host.clone(), + upstream_port: proxy_settings.port, + upstream_type: proxy_settings.proxy_type.clone(), local_port, }; @@ -116,7 +141,7 @@ 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(), (upstream_url_str, local_port)); + profile_proxies.insert(name.to_string(), proxy_settings.clone()); } // Return proxy settings for the browser @@ -125,6 +150,8 @@ impl ProxyManager { proxy_type: "http".to_string(), host: "localhost".to_string(), port: proxy_info.local_port, + username: None, + password: None, }) } @@ -171,11 +198,13 @@ impl ProxyManager { proxy_type: "http".to_string(), host: "localhost".to_string(), port: proxy.local_port, + username: None, + password: None, }) } // Get stored proxy info for a profile - pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option<(String, u16)> { + pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option { let profile_proxies = self.profile_proxies.lock().unwrap(); profile_proxies.get(profile_name).cloned() } diff --git a/src/components/proxy-settings-dialog.tsx b/src/components/proxy-settings-dialog.tsx index 0aa542e..7a0cd0a 100644 --- a/src/components/proxy-settings-dialog.tsx +++ b/src/components/proxy-settings-dialog.tsx @@ -30,6 +30,8 @@ interface ProxySettings { proxy_type: string; host: string; port: number; + username?: string; + password?: string; } interface ProxySettingsDialogProps { @@ -52,6 +54,8 @@ export function ProxySettingsDialog({ proxy_type: initialSettings?.proxy_type ?? "http", host: initialSettings?.host ?? "", port: initialSettings?.port ?? 8080, + username: initialSettings?.username ?? "", + password: initialSettings?.password ?? "", }); const [initialSettingsState, setInitialSettingsState] = @@ -60,6 +64,8 @@ export function ProxySettingsDialog({ proxy_type: "http", host: "", port: 8080, + username: "", + password: "", }); useEffect(() => { @@ -69,6 +75,8 @@ export function ProxySettingsDialog({ proxy_type: initialSettings.proxy_type, host: initialSettings.host, port: initialSettings.port, + username: initialSettings.username ?? "", + password: initialSettings.password ?? "", }; setSettings(newSettings); setInitialSettingsState(newSettings); @@ -78,6 +86,8 @@ export function ProxySettingsDialog({ proxy_type: "http", host: "", port: 80, + username: "", + password: "", }; setSettings(defaultSettings); setInitialSettingsState(defaultSettings); @@ -94,7 +104,9 @@ export function ProxySettingsDialog({ settings.enabled !== initialSettingsState.enabled || settings.proxy_type !== initialSettingsState.proxy_type || settings.host !== initialSettingsState.host || - settings.port !== initialSettingsState.port + settings.port !== initialSettingsState.port || + settings.username !== initialSettingsState.username || + settings.password !== initialSettingsState.password ); }; @@ -214,6 +226,31 @@ export function ProxySettingsDialog({ max="65535" /> + +
+ + { + setSettings({ ...settings, username: e.target.value }); + }} + placeholder="Proxy username" + /> +
+ +
+ + { + setSettings({ ...settings, password: e.target.value }); + }} + placeholder="Proxy password" + /> +
)} diff --git a/src/types.ts b/src/types.ts index 899ed26..de256b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,8 @@ export interface ProxySettings { proxy_type: string; // "http", "https", "socks4", or "socks5" host: string; port: number; + username?: string; + password?: string; } export interface TableSortingSettings {