style: update ui to accept proxy separately

This commit is contained in:
zhom
2025-06-09 23:19:34 +04:00
parent ac293f6204
commit c4b1745a0f
7 changed files with 191 additions and 55 deletions
+41 -13
View File
@@ -11,10 +11,15 @@ import { runProxyWorker } from "./proxy-worker";
program
.command("proxy")
.argument("<action>", "start, stop, or list proxies")
.option("-h, --host <host>", "upstream proxy host")
.option("-P, --proxy-port <port>", "upstream proxy port", Number.parseInt)
.option(
"-u, --upstream <url>",
"upstream proxy URL (protocol://[username:password@]host:port)"
"-t, --type <type>",
"upstream proxy type (http, https, socks4, socks5)",
"http"
)
.option("-u, --username <username>", "upstream proxy username")
.option("-w, --password <password>", "upstream proxy password")
.option(
"-p, --port <number>",
"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;
}
+49 -19
View File
@@ -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<string> {
// 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<boolean> {
// 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<boolean> {
// 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<boolean> {
success = false;
}
}
return success;
}
@@ -61,7 +91,7 @@ export async function stopProxy(upstreamProxyUrl: string): Promise<boolean> {
* @returns Array of upstream proxy URLs
*/
export function getActiveProxies(): string[] {
return listProxyConfigs().map(config => config.upstreamUrl);
return listProxyConfigs().map((config) => config.upstreamUrl);
}
/**
+8
View File
@@ -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<String>,
pub password: Option<String>,
}
#[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)
+9 -7
View File
@@ -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
+44 -15
View File
@@ -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<HashMap<u32, ProxyInfo>>, // Maps browser process ID to proxy info
// Store proxy info by profile name for persistence across browser restarts
profile_proxies: Mutex<HashMap<String, (String, u16)>>, // Maps profile name to (upstream_url, port)
profile_proxies: Mutex<HashMap<String, ProxySettings>>, // 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<ProxySettings, String> {
@@ -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<ProxySettings> {
let profile_proxies = self.profile_proxies.lock().unwrap();
profile_proxies.get(profile_name).cloned()
}
+38 -1
View File
@@ -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"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username (optional)</Label>
<Input
id="username"
value={settings.username}
onChange={(e) => {
setSettings({ ...settings, username: e.target.value });
}}
placeholder="Proxy username"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password (optional)</Label>
<Input
id="password"
type="password"
value={settings.password}
onChange={(e) => {
setSettings({ ...settings, password: e.target.value });
}}
placeholder="Proxy password"
/>
</div>
</>
)}
</div>
+2
View File
@@ -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 {