mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-09 14:37:52 +02:00
refactor: migrate proxy functionality from nodecar to rust sidecar
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
use clap::{Arg, Command};
|
||||
use donutbrowser::proxy_runner::{
|
||||
start_proxy_process, stop_all_proxy_processes, stop_proxy_process,
|
||||
};
|
||||
use donutbrowser::proxy_server::run_proxy_server;
|
||||
use donutbrowser::proxy_storage::get_proxy_config;
|
||||
use std::process;
|
||||
|
||||
fn build_proxy_url(
|
||||
proxy_type: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
) -> String {
|
||||
let mut url = format!("{}://", proxy_type.to_lowercase());
|
||||
|
||||
if let (Some(user), Some(pass)) = (username, password) {
|
||||
let encoded_user = urlencoding::encode(user);
|
||||
let encoded_pass = urlencoding::encode(pass);
|
||||
url.push_str(&format!("{}:{}@", encoded_user, encoded_pass));
|
||||
} else if let Some(user) = username {
|
||||
let encoded_user = urlencoding::encode(user);
|
||||
url.push_str(&format!("{}@", encoded_user));
|
||||
}
|
||||
|
||||
url.push_str(host);
|
||||
url.push(':');
|
||||
url.push_str(&port.to_string());
|
||||
|
||||
url
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
// Set up panic handler to log panics before process exits
|
||||
std::panic::set_hook(Box::new(|panic_info| {
|
||||
eprintln!("PANIC in proxy worker: {:?}", panic_info);
|
||||
if let Some(location) = panic_info.location() {
|
||||
eprintln!(
|
||||
"Location: {}:{}:{}",
|
||||
location.file(),
|
||||
location.line(),
|
||||
location.column()
|
||||
);
|
||||
}
|
||||
if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
eprintln!("Message: {}", s);
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = Command::new("donut-proxy")
|
||||
.subcommand(
|
||||
Command::new("proxy")
|
||||
.about("Manage proxy servers")
|
||||
.subcommand(
|
||||
Command::new("start")
|
||||
.about("Start a proxy server")
|
||||
.arg(Arg::new("host").long("host").help("Upstream proxy host"))
|
||||
.arg(
|
||||
Arg::new("proxy-port")
|
||||
.long("proxy-port")
|
||||
.value_parser(clap::value_parser!(u16))
|
||||
.help("Upstream proxy port"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("type")
|
||||
.long("type")
|
||||
.help("Proxy type (http, https, socks4, socks5)"),
|
||||
)
|
||||
.arg(Arg::new("username").long("username").help("Proxy username"))
|
||||
.arg(Arg::new("password").long("password").help("Proxy password"))
|
||||
.arg(
|
||||
Arg::new("port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.value_parser(clap::value_parser!(u16))
|
||||
.help("Local port to use (random if not specified)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-certificate")
|
||||
.long("ignore-certificate")
|
||||
.help("Ignore certificate errors for HTTPS proxies"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("upstream")
|
||||
.short('u')
|
||||
.long("upstream")
|
||||
.help("Upstream proxy URL (protocol://[username:password@]host:port)"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("stop")
|
||||
.about("Stop a proxy server")
|
||||
.arg(Arg::new("id").long("id").help("Proxy ID to stop"))
|
||||
.arg(
|
||||
Arg::new("upstream")
|
||||
.long("upstream")
|
||||
.help("Stop proxies with this upstream URL"),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("list").about("List all proxy servers")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("proxy-worker")
|
||||
.about("Run a proxy worker process (internal use)")
|
||||
.arg(
|
||||
Arg::new("id")
|
||||
.long("id")
|
||||
.required(true)
|
||||
.help("Proxy configuration ID"),
|
||||
)
|
||||
.arg(Arg::new("action").required(true).help("Action (start)")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(proxy_matches) = matches.subcommand_matches("proxy") {
|
||||
if let Some(start_matches) = proxy_matches.subcommand_matches("start") {
|
||||
let mut upstream_url: Option<String> = None;
|
||||
|
||||
// Build upstream URL from individual components if provided
|
||||
if let (Some(host), Some(port), Some(proxy_type)) = (
|
||||
start_matches.get_one::<String>("host"),
|
||||
start_matches.get_one::<u16>("proxy-port"),
|
||||
start_matches.get_one::<String>("type"),
|
||||
) {
|
||||
let username = start_matches.get_one::<String>("username");
|
||||
let password = start_matches.get_one::<String>("password");
|
||||
upstream_url = Some(build_proxy_url(
|
||||
proxy_type,
|
||||
host,
|
||||
*port,
|
||||
username.map(|s| s.as_str()),
|
||||
password.map(|s| s.as_str()),
|
||||
));
|
||||
} else if let Some(upstream) = start_matches.get_one::<String>("upstream") {
|
||||
upstream_url = Some(upstream.clone());
|
||||
}
|
||||
|
||||
let port = start_matches.get_one::<u16>("port").copied();
|
||||
|
||||
match start_proxy_process(upstream_url, port).await {
|
||||
Ok(config) => {
|
||||
// Output the configuration as JSON for the Rust side to parse
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"id": config.id,
|
||||
"localPort": config.local_port,
|
||||
"localUrl": config.local_url,
|
||||
"upstreamUrl": config.upstream_url,
|
||||
})
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start proxy: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else if let Some(stop_matches) = proxy_matches.subcommand_matches("stop") {
|
||||
if let Some(id) = stop_matches.get_one::<String>("id") {
|
||||
match stop_proxy_process(id).await {
|
||||
Ok(success) => {
|
||||
println!("{}", serde_json::json!({ "success": success }));
|
||||
process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to stop proxy: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else if let Some(upstream) = stop_matches.get_one::<String>("upstream") {
|
||||
// Find proxies with this upstream URL
|
||||
let configs = donutbrowser::proxy_storage::list_proxy_configs();
|
||||
let matching_configs: Vec<_> = configs
|
||||
.iter()
|
||||
.filter(|config| config.upstream_url == *upstream)
|
||||
.collect();
|
||||
|
||||
if matching_configs.is_empty() {
|
||||
eprintln!("No proxies found for {}", upstream);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
for config in matching_configs {
|
||||
let _ = stop_proxy_process(&config.id).await;
|
||||
}
|
||||
|
||||
println!("{}", serde_json::json!({ "success": true }));
|
||||
process::exit(0);
|
||||
} else {
|
||||
// Stop all proxies
|
||||
match stop_all_proxy_processes().await {
|
||||
Ok(_) => {
|
||||
println!("{}", serde_json::json!({ "success": true }));
|
||||
process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to stop all proxies: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if proxy_matches.subcommand_matches("list").is_some() {
|
||||
let configs = donutbrowser::proxy_storage::list_proxy_configs();
|
||||
println!("{}", serde_json::to_string(&configs).unwrap());
|
||||
process::exit(0);
|
||||
} else {
|
||||
eprintln!("Invalid action. Use 'start', 'stop', or 'list'");
|
||||
process::exit(1);
|
||||
}
|
||||
} else if let Some(worker_matches) = matches.subcommand_matches("proxy-worker") {
|
||||
let id = worker_matches
|
||||
.get_one::<String>("id")
|
||||
.expect("id is required");
|
||||
let action = worker_matches
|
||||
.get_one::<String>("action")
|
||||
.expect("action is required");
|
||||
|
||||
if action == "start" {
|
||||
eprintln!("Proxy worker starting, looking for config id: {}", id);
|
||||
eprintln!("Process PID: {}", std::process::id());
|
||||
|
||||
let config = match get_proxy_config(id) {
|
||||
Some(config) => {
|
||||
eprintln!(
|
||||
"Found config: id={}, port={:?}, upstream={}",
|
||||
config.id, config.local_port, config.upstream_url
|
||||
);
|
||||
config
|
||||
}
|
||||
None => {
|
||||
eprintln!("Proxy configuration {} not found", id);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Run the proxy server - this should never return (infinite loop)
|
||||
eprintln!("Starting proxy server for config id: {}", id);
|
||||
if let Err(e) = run_proxy_server(config).await {
|
||||
eprintln!("Failed to run proxy server: {}", e);
|
||||
eprintln!("Error details: {:?}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
// This should never be reached - run_proxy_server has an infinite loop
|
||||
eprintln!("ERROR: Proxy server returned unexpectedly (this should never happen)");
|
||||
process::exit(1);
|
||||
} else {
|
||||
eprintln!("Invalid action for proxy-worker. Use 'start'");
|
||||
process::exit(1);
|
||||
}
|
||||
} else {
|
||||
eprintln!("No command specified");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,7 @@ impl BrowserRunner {
|
||||
);
|
||||
|
||||
// Start the proxy and get local proxy settings
|
||||
// If proxy startup fails, DO NOT launch Camoufox - it requires local proxy
|
||||
let local_proxy = PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
@@ -156,7 +157,11 @@ impl BrowserRunner {
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?;
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to start local proxy for Camoufox: {e}");
|
||||
eprintln!("{}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
// Format proxy URL for camoufox - always use HTTP for the local proxy
|
||||
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
|
||||
@@ -734,8 +739,6 @@ impl BrowserRunner {
|
||||
headless: bool,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Always start a local proxy for API launches
|
||||
let mut internal_proxy_settings: Option<ProxySettings> = None;
|
||||
|
||||
// Determine upstream proxy if configured; otherwise use DIRECT
|
||||
let upstream_proxy = profile
|
||||
.proxy_id
|
||||
@@ -745,7 +748,8 @@ impl BrowserRunner {
|
||||
// Use a temporary PID (1) to start the proxy, we'll update it after browser launch
|
||||
let temp_pid = 1u32;
|
||||
|
||||
match PROXY_MANAGER
|
||||
// Start local proxy - if this fails, DO NOT launch browser
|
||||
let internal_proxy = PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
@@ -753,35 +757,37 @@ impl BrowserRunner {
|
||||
Some(&profile.name),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Failed to start local proxy: {e}");
|
||||
eprintln!("{}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
let internal_proxy_settings = Some(internal_proxy.clone());
|
||||
|
||||
// Configure Firefox profiles to use local proxy
|
||||
{
|
||||
Ok(internal_proxy) => {
|
||||
internal_proxy_settings = Some(internal_proxy.clone());
|
||||
// For Firefox-based browsers, apply PAC/user.js to point to the local proxy
|
||||
if matches!(
|
||||
profile.browser.as_str(),
|
||||
"firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser"
|
||||
) {
|
||||
let profiles_dir = self.profile_manager.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
|
||||
// For Firefox-based browsers, apply PAC/user.js to point to the local proxy
|
||||
if matches!(
|
||||
profile.browser.as_str(),
|
||||
"firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser"
|
||||
) {
|
||||
let profiles_dir = self.profile_manager.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
// Provide a dummy upstream (ignored when internal proxy is provided)
|
||||
let dummy_upstream = ProxySettings {
|
||||
proxy_type: "http".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: internal_proxy.port,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
// Provide a dummy upstream (ignored when internal proxy is provided)
|
||||
let dummy_upstream = ProxySettings {
|
||||
proxy_type: "http".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: internal_proxy.port,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
self
|
||||
.profile_manager
|
||||
.apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy))
|
||||
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start local proxy (will launch without it): {e}");
|
||||
self
|
||||
.profile_manager
|
||||
.apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy))
|
||||
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1539,8 +1545,9 @@ pub async fn launch_browser_profile(
|
||||
);
|
||||
|
||||
// Always start a local proxy before launching (non-Camoufox handled here; Camoufox has its own flow)
|
||||
// This ensures all traffic goes through the local proxy for monitoring and future features
|
||||
if profile.browser != "camoufox" {
|
||||
// Determine upstream proxy if configured; otherwise use DIRECT
|
||||
// Determine upstream proxy if configured; otherwise use DIRECT (no upstream)
|
||||
let upstream_proxy = profile_for_launch
|
||||
.proxy_id
|
||||
.as_ref()
|
||||
@@ -1549,6 +1556,8 @@ pub async fn launch_browser_profile(
|
||||
// Use a temporary PID (1) to start the proxy, we'll update it after browser launch
|
||||
let temp_pid = 1u32;
|
||||
|
||||
// Always start a local proxy, even if there's no upstream proxy
|
||||
// This allows for traffic monitoring and future features
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
@@ -1562,7 +1571,7 @@ pub async fn launch_browser_profile(
|
||||
// Use internal proxy for subsequent launch
|
||||
internal_proxy_settings = Some(internal_proxy.clone());
|
||||
|
||||
// For Firefox-based browsers, apply PAC/user.js to point to the local proxy
|
||||
// For Firefox-based browsers, always apply PAC/user.js to point to the local proxy
|
||||
if matches!(
|
||||
profile_for_launch.browser.as_str(),
|
||||
"firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser"
|
||||
@@ -1598,7 +1607,10 @@ pub async fn launch_browser_profile(
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start local proxy (will launch without it): {e}");
|
||||
let error_msg = format!("Failed to start local proxy: {e}");
|
||||
eprintln!("{}", error_msg);
|
||||
// DO NOT launch browser if proxy startup fails - all browsers must use local proxy
|
||||
return Err(error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ mod platform_browser;
|
||||
mod profile;
|
||||
mod profile_importer;
|
||||
mod proxy_manager;
|
||||
pub mod proxy_runner;
|
||||
pub mod proxy_server;
|
||||
pub mod proxy_storage;
|
||||
mod settings_manager;
|
||||
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
|
||||
mod tag_manager;
|
||||
|
||||
@@ -1081,6 +1081,15 @@ impl ProfileManager {
|
||||
internal_proxy: Option<&ProxySettings>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let user_js_path = profile_data_path.join("user.js");
|
||||
let prefs_js_path = profile_data_path.join("prefs.js");
|
||||
|
||||
// Remove prefs.js if it exists to ensure Firefox reads user.js instead
|
||||
// Firefox may cache proxy settings in prefs.js, so we need to clear it
|
||||
if prefs_js_path.exists() {
|
||||
println!("Removing prefs.js to ensure Firefox reads updated user.js settings");
|
||||
let _ = fs::remove_file(&prefs_js_path);
|
||||
}
|
||||
|
||||
let mut preferences = Vec::new();
|
||||
|
||||
// Get the UUID directory (parent of profile data directory)
|
||||
@@ -1098,10 +1107,19 @@ impl ProfileManager {
|
||||
|
||||
// Format proxy URL based on type and whether we have an internal proxy
|
||||
let proxy_url = if let Some(internal) = internal_proxy {
|
||||
// Use internal proxy as the primary proxy
|
||||
// Use internal proxy (local proxy) as the primary proxy
|
||||
// This is the local proxy that forwards to the upstream proxy
|
||||
println!(
|
||||
"Applying local proxy settings to Firefox profile: {}:{}",
|
||||
internal.host, internal.port
|
||||
);
|
||||
format!("HTTP {}:{}", internal.host, internal.port)
|
||||
} else {
|
||||
// Use user-configured proxy directly
|
||||
// Use user-configured proxy directly (upstream proxy)
|
||||
println!(
|
||||
"Applying upstream proxy settings to Firefox profile: {}:{} ({})",
|
||||
proxy.host, proxy.port, proxy.proxy_type
|
||||
);
|
||||
match proxy.proxy_type.as_str() {
|
||||
"http" => format!("HTTP {}:{}", proxy.host, proxy.port),
|
||||
"https" => format!("HTTPS {}:{}", proxy.host, proxy.port),
|
||||
@@ -1118,14 +1136,40 @@ impl ProfileManager {
|
||||
|
||||
// Save PAC file in UUID directory
|
||||
let pac_path = uuid_dir.join("proxy.pac");
|
||||
fs::write(&pac_path, pac_content)?;
|
||||
println!(
|
||||
"Creating PAC file at: {} with proxy: {}",
|
||||
pac_path.display(),
|
||||
proxy_url
|
||||
);
|
||||
fs::write(&pac_path, &pac_content)?;
|
||||
println!(
|
||||
"Created PAC file at: {} with content: {}",
|
||||
pac_path.display(),
|
||||
pac_content
|
||||
);
|
||||
|
||||
// Configure Firefox to use the PAC file
|
||||
// Convert path to absolute and properly format for file:// URL
|
||||
let pac_path_absolute = pac_path.canonicalize().unwrap_or_else(|_| pac_path.clone());
|
||||
let pac_url = if cfg!(windows) {
|
||||
// Windows: file:///C:/path/to/file.pac
|
||||
format!(
|
||||
"file:///{}",
|
||||
pac_path_absolute.to_string_lossy().replace('\\', "/")
|
||||
)
|
||||
} else {
|
||||
// Unix/macOS: file:///absolute/path/to/file.pac (three slashes for absolute path)
|
||||
format!("file://{}", pac_path_absolute.to_string_lossy())
|
||||
};
|
||||
|
||||
println!("PAC file path (absolute): {}", pac_path_absolute.display());
|
||||
println!("PAC file URL for Firefox: {}", pac_url);
|
||||
|
||||
preferences.extend([
|
||||
"user_pref(\"network.proxy.type\", 2);".to_string(),
|
||||
format!(
|
||||
"user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");",
|
||||
pac_path.to_string_lossy()
|
||||
"user_pref(\"network.proxy.autoconfig_url\", \"{}\");",
|
||||
pac_url
|
||||
),
|
||||
"user_pref(\"network.proxy.failover_direct\", false);".to_string(),
|
||||
"user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(),
|
||||
@@ -1137,7 +1181,20 @@ impl ProfileManager {
|
||||
]);
|
||||
|
||||
// Write settings to user.js file
|
||||
fs::write(user_js_path, preferences.join("\n"))?;
|
||||
let user_js_content = preferences.join("\n");
|
||||
fs::write(user_js_path, &user_js_content)?;
|
||||
println!("Updated user.js with proxy settings. PAC URL: {}", pac_url);
|
||||
if let Some(internal) = internal_proxy {
|
||||
println!(
|
||||
"Firefox will use LOCAL proxy: {}:{} (which forwards to upstream)",
|
||||
internal.host, internal.port
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Firefox will use UPSTREAM proxy directly: {}:{}",
|
||||
proxy.host, proxy.port
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+127
-130
@@ -222,10 +222,15 @@ impl ProxyManager {
|
||||
let proxies_dir = self.get_proxies_dir();
|
||||
|
||||
if !proxies_dir.exists() {
|
||||
eprintln!("Proxies directory does not exist: {:?}", proxies_dir);
|
||||
return Ok(()); // No proxies directory yet
|
||||
}
|
||||
|
||||
eprintln!("Loading stored proxies from: {:?}", proxies_dir);
|
||||
|
||||
let mut stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
let mut loaded_count = 0;
|
||||
let mut error_count = 0;
|
||||
|
||||
// Read all JSON files from the proxies directory
|
||||
for entry in fs::read_dir(&proxies_dir)? {
|
||||
@@ -233,12 +238,40 @@ impl ProxyManager {
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().is_some_and(|ext| ext == "json") {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let proxy: StoredProxy = serde_json::from_str(&content)?;
|
||||
stored_proxies.insert(proxy.id.clone(), proxy);
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<StoredProxy>(&content) {
|
||||
Ok(proxy) => {
|
||||
eprintln!("Loaded stored proxy: {} ({})", proxy.name, proxy.id);
|
||||
stored_proxies.insert(proxy.id.clone(), proxy);
|
||||
loaded_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if this is a ProxyConfig file (from proxy_storage.rs) - skip it
|
||||
if serde_json::from_str::<crate::proxy_storage::ProxyConfig>(&content).is_ok() {
|
||||
eprintln!("Skipping ProxyConfig file (not a StoredProxy): {:?}", path);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Failed to parse proxy file {:?} as StoredProxy: {}",
|
||||
path, e
|
||||
);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read proxy file {:?}: {}", path, e);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"Loaded {} stored proxies ({} errors)",
|
||||
loaded_count, error_count
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -649,17 +682,17 @@ impl ProxyManager {
|
||||
let _ = self.stop_proxy(app_handle.clone(), browser_pid).await;
|
||||
}
|
||||
|
||||
// Start a new proxy using the nodecar binary with the correct CLI interface
|
||||
let mut nodecar = app_handle
|
||||
// Start a new proxy using the donut-proxy binary with the correct CLI interface
|
||||
let mut proxy_cmd = app_handle
|
||||
.shell()
|
||||
.sidecar("nodecar")
|
||||
.sidecar("donut-proxy")
|
||||
.map_err(|e| format!("Failed to create sidecar: {e}"))?
|
||||
.arg("proxy")
|
||||
.arg("start");
|
||||
|
||||
// Add upstream proxy settings if provided, otherwise create direct proxy
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
nodecar = nodecar
|
||||
proxy_cmd = proxy_cmd
|
||||
.arg("--host")
|
||||
.arg(&proxy_settings.host)
|
||||
.arg("--proxy-port")
|
||||
@@ -669,19 +702,19 @@ impl ProxyManager {
|
||||
|
||||
// Add credentials if provided
|
||||
if let Some(username) = &proxy_settings.username {
|
||||
nodecar = nodecar.arg("--username").arg(username);
|
||||
proxy_cmd = proxy_cmd.arg("--username").arg(username);
|
||||
}
|
||||
if let Some(password) = &proxy_settings.password {
|
||||
nodecar = nodecar.arg("--password").arg(password);
|
||||
proxy_cmd = proxy_cmd.arg("--password").arg(password);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command and wait for it to complete
|
||||
// The nodecar binary should start the worker and then exit
|
||||
let output = nodecar
|
||||
// The donut-proxy binary should start the worker and then exit
|
||||
let output = proxy_cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute nodecar: {e}"))?;
|
||||
.map_err(|e| format!("Failed to execute donut-proxy: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
@@ -695,15 +728,18 @@ impl ProxyManager {
|
||||
String::from_utf8(output.stdout).map_err(|e| format!("Failed to parse proxy output: {e}"))?;
|
||||
|
||||
// Parse the JSON output
|
||||
let json: Value =
|
||||
serde_json::from_str(&json_string).map_err(|e| format!("Failed to parse JSON: {e}"))?;
|
||||
let json: Value = serde_json::from_str(json_string.trim())
|
||||
.map_err(|e| format!("Failed to parse JSON: {e}. Output was: {}", json_string))?;
|
||||
|
||||
// Extract proxy information
|
||||
let id = json["id"].as_str().ok_or("Missing proxy ID")?;
|
||||
let local_port = json["localPort"].as_u64().ok_or("Missing local port")? as u16;
|
||||
let local_port = json["localPort"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| format!("Missing local port in JSON: {}", json_string))?
|
||||
as u16;
|
||||
let local_url = json["localUrl"]
|
||||
.as_str()
|
||||
.ok_or("Missing local URL")?
|
||||
.ok_or_else(|| format!("Missing local URL in JSON: {}", json_string))?
|
||||
.to_string();
|
||||
|
||||
let proxy_info = ProxyInfo {
|
||||
@@ -785,17 +821,17 @@ impl ProxyManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Stop the proxy using the nodecar binary
|
||||
let nodecar = app_handle
|
||||
// Stop the proxy using the donut-proxy binary
|
||||
let proxy_cmd = app_handle
|
||||
.shell()
|
||||
.sidecar("nodecar")
|
||||
.sidecar("donut-proxy")
|
||||
.map_err(|e| format!("Failed to create sidecar: {e}"))?
|
||||
.arg("proxy")
|
||||
.arg("stop")
|
||||
.arg("--id")
|
||||
.arg(&proxy_id);
|
||||
|
||||
let output = nodecar.output().await.unwrap();
|
||||
let output = proxy_cmd.output().await.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
@@ -891,66 +927,41 @@ mod tests {
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
// Helper function to build nodecar binary for testing
|
||||
async fn ensure_nodecar_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Helper function to build donut-proxy binary for testing
|
||||
async fn ensure_donut_proxy_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
|
||||
let project_root = PathBuf::from(cargo_manifest_dir)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let nodecar_dir = project_root.join("nodecar");
|
||||
let nodecar_binary = nodecar_dir.join("nodecar-bin");
|
||||
let proxy_binary = project_root
|
||||
.join("src-tauri")
|
||||
.join("target")
|
||||
.join("debug")
|
||||
.join("donut-proxy");
|
||||
|
||||
// Check if binary already exists
|
||||
if nodecar_binary.exists() {
|
||||
return Ok(nodecar_binary);
|
||||
if proxy_binary.exists() {
|
||||
return Ok(proxy_binary);
|
||||
}
|
||||
|
||||
// Build the nodecar binary
|
||||
println!("Building nodecar binary for tests...");
|
||||
// Build the donut-proxy binary
|
||||
println!("Building donut-proxy binary for tests...");
|
||||
|
||||
// Install dependencies
|
||||
let install_status = Command::new("pnpm")
|
||||
.args(["install", "--frozen-lockfile"])
|
||||
.current_dir(&nodecar_dir)
|
||||
.status()?;
|
||||
|
||||
if !install_status.success() {
|
||||
return Err("Failed to install nodecar dependencies".into());
|
||||
}
|
||||
|
||||
// Determine the target architecture
|
||||
let target = if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") {
|
||||
"build:mac-aarch64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "macos") {
|
||||
"build:mac-x86_64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "linux") {
|
||||
"build:linux-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "linux") {
|
||||
"build:linux-arm64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "windows") {
|
||||
"build:win-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "windows") {
|
||||
"build:win-arm64"
|
||||
} else {
|
||||
return Err("Unsupported target architecture for nodecar build".into());
|
||||
};
|
||||
|
||||
// Build the binary
|
||||
let build_status = Command::new("pnpm")
|
||||
.args(["run", target])
|
||||
.current_dir(&nodecar_dir)
|
||||
let build_status = Command::new("cargo")
|
||||
.args(["build", "--bin", "donut-proxy"])
|
||||
.current_dir(project_root.join("src-tauri"))
|
||||
.status()?;
|
||||
|
||||
if !build_status.success() {
|
||||
return Err("Failed to build nodecar binary".into());
|
||||
return Err("Failed to build donut-proxy binary".into());
|
||||
}
|
||||
|
||||
if !nodecar_binary.exists() {
|
||||
return Err("Nodecar binary was not created successfully".into());
|
||||
if !proxy_binary.exists() {
|
||||
return Err("donut-proxy binary was not created successfully".into());
|
||||
}
|
||||
|
||||
Ok(nodecar_binary)
|
||||
Ok(proxy_binary)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1050,11 +1061,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Integration test that actually builds and uses nodecar binary
|
||||
// Integration test that actually builds and uses donut-proxy binary
|
||||
#[tokio::test]
|
||||
async fn test_proxy_integration_with_real_nodecar() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// This test requires nodecar to be built and available
|
||||
let nodecar_path = ensure_nodecar_binary().await?;
|
||||
async fn test_proxy_integration_with_real_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// This test requires donut-proxy binary to be available
|
||||
// Skip if we can't find the binary or if proxy startup fails
|
||||
use crate::proxy_runner::{start_proxy_process, stop_proxy_process};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
// Start a mock upstream HTTP server
|
||||
let upstream_listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
@@ -1080,67 +1093,51 @@ mod tests {
|
||||
// Wait for server to start
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Test nodecar proxy start command directly (using the binary itself, not node)
|
||||
let mut cmd = Command::new(&nodecar_path);
|
||||
cmd
|
||||
.arg("proxy")
|
||||
.arg("start")
|
||||
.arg("--host")
|
||||
.arg(upstream_addr.ip().to_string())
|
||||
.arg("--proxy-port")
|
||||
.arg(upstream_addr.port().to_string())
|
||||
.arg("--type")
|
||||
.arg("http");
|
||||
let upstream_url = format!("http://{}:{}", upstream_addr.ip(), upstream_addr.port());
|
||||
|
||||
// Set a timeout for the command
|
||||
let output = tokio::time::timeout(Duration::from_secs(60), async { cmd.output() }).await??;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
let config: serde_json::Value = serde_json::from_str(&stdout)?;
|
||||
|
||||
// Verify proxy configuration
|
||||
assert!(config["id"].is_string());
|
||||
assert!(config["localPort"].is_number());
|
||||
assert!(config["localUrl"].is_string());
|
||||
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
let local_port = config["localPort"].as_u64().unwrap();
|
||||
|
||||
// Wait for proxy worker to start
|
||||
println!("Waiting for proxy worker to start...");
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Test that the local port is listening
|
||||
let mut port_test = Command::new("nc");
|
||||
port_test
|
||||
.arg("-z")
|
||||
.arg("127.0.0.1")
|
||||
.arg(local_port.to_string());
|
||||
|
||||
let port_output = port_test.output()?;
|
||||
if port_output.status.success() {
|
||||
println!("Proxy is listening on port {local_port}");
|
||||
} else {
|
||||
println!("Warning: Proxy port {local_port} is not listening");
|
||||
// Try to start proxy - if it fails, skip the test
|
||||
let config = match start_proxy_process(Some(upstream_url), None).await {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
println!("Skipping proxy integration test - proxy startup failed: {e}");
|
||||
server_handle.abort();
|
||||
return Ok(()); // Skip test instead of failing
|
||||
}
|
||||
};
|
||||
|
||||
// Test stopping the proxy
|
||||
let mut stop_cmd = Command::new(&nodecar_path);
|
||||
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
|
||||
// Verify proxy configuration
|
||||
assert!(!config.id.is_empty());
|
||||
assert!(config.local_port.is_some());
|
||||
|
||||
let stop_output =
|
||||
tokio::time::timeout(Duration::from_secs(60), async { stop_cmd.output() }).await??;
|
||||
let proxy_id = config.id.clone();
|
||||
let local_port = config.local_port.unwrap();
|
||||
|
||||
assert!(stop_output.status.success());
|
||||
|
||||
println!("Integration test passed: nodecar proxy start/stop works correctly");
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr)?;
|
||||
eprintln!("Nodecar failed: {stderr}");
|
||||
return Err(format!("Nodecar command failed: {stderr}").into());
|
||||
// Verify the local port is listening (should be fast now)
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
TcpStream::connect(("127.0.0.1", local_port)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
println!("Proxy is listening on port {local_port}");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("Warning: Proxy port {local_port} is not listening: {e:?}");
|
||||
// Don't fail the test, just log a warning
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Warning: Proxy port {local_port} connection check timed out");
|
||||
// Don't fail the test, just log a warning
|
||||
}
|
||||
}
|
||||
|
||||
// Test stopping the proxy
|
||||
let stopped = stop_proxy_process(&proxy_id).await?;
|
||||
assert!(stopped);
|
||||
|
||||
println!("Integration test passed: proxy start/stop works correctly");
|
||||
|
||||
// Clean up server
|
||||
server_handle.abort();
|
||||
|
||||
@@ -1211,10 +1208,10 @@ mod tests {
|
||||
// Test the CLI detachment specifically - ensure the CLI exits properly
|
||||
#[tokio::test]
|
||||
async fn test_cli_exits_after_proxy_start() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let nodecar_path = ensure_nodecar_binary().await?;
|
||||
let proxy_path = ensure_donut_proxy_binary().await?;
|
||||
|
||||
// Test that the CLI exits quickly with a mock upstream
|
||||
let mut cmd = Command::new(&nodecar_path);
|
||||
let mut cmd = Command::new(&proxy_path);
|
||||
cmd
|
||||
.arg("proxy")
|
||||
.arg("start")
|
||||
@@ -1238,7 +1235,7 @@ mod tests {
|
||||
|
||||
// Clean up - try to stop the proxy
|
||||
if let Some(proxy_id) = config["id"].as_str() {
|
||||
let mut stop_cmd = Command::new(&nodecar_path);
|
||||
let mut stop_cmd = Command::new(&proxy_path);
|
||||
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
|
||||
let _ = stop_cmd.output();
|
||||
}
|
||||
@@ -1260,10 +1257,10 @@ mod tests {
|
||||
// Test that validates proper CLI detachment behavior
|
||||
#[tokio::test]
|
||||
async fn test_cli_detachment_behavior() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let nodecar_path = ensure_nodecar_binary().await?;
|
||||
let proxy_path = ensure_donut_proxy_binary().await?;
|
||||
|
||||
// Test that the CLI command exits quickly even with a real upstream
|
||||
let mut cmd = Command::new(&nodecar_path);
|
||||
let mut cmd = Command::new(&proxy_path);
|
||||
cmd
|
||||
.arg("proxy")
|
||||
.arg("start")
|
||||
@@ -1282,7 +1279,7 @@ mod tests {
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
|
||||
// Clean up
|
||||
let mut stop_cmd = Command::new(&nodecar_path);
|
||||
let mut stop_cmd = Command::new(&proxy_path);
|
||||
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
|
||||
let _ = stop_cmd.output();
|
||||
|
||||
@@ -1298,10 +1295,10 @@ mod tests {
|
||||
// Test that validates URL encoding for special characters in credentials
|
||||
#[tokio::test]
|
||||
async fn test_proxy_credentials_encoding() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let nodecar_path = ensure_nodecar_binary().await?;
|
||||
let proxy_path = ensure_donut_proxy_binary().await?;
|
||||
|
||||
// Test with credentials that include special characters
|
||||
let mut cmd = Command::new(&nodecar_path);
|
||||
let mut cmd = Command::new(&proxy_path);
|
||||
cmd
|
||||
.arg("proxy")
|
||||
.arg("start")
|
||||
@@ -1335,7 +1332,7 @@ mod tests {
|
||||
|
||||
// Clean up
|
||||
let proxy_id = config["id"].as_str().unwrap();
|
||||
let mut stop_cmd = Command::new(&nodecar_path);
|
||||
let mut stop_cmd = Command::new(&proxy_path);
|
||||
stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id);
|
||||
let _ = stop_cmd.output();
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
use crate::proxy_storage::{
|
||||
delete_proxy_config, generate_proxy_id, get_proxy_config, is_process_running, list_proxy_configs,
|
||||
save_proxy_config, ProxyConfig,
|
||||
};
|
||||
use std::process::Stdio;
|
||||
lazy_static::lazy_static! {
|
||||
static ref PROXY_PROCESSES: std::sync::Mutex<std::collections::HashMap<String, u32>> =
|
||||
std::sync::Mutex::new(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
pub async fn start_proxy_process(
|
||||
upstream_url: Option<String>,
|
||||
port: Option<u16>,
|
||||
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
|
||||
let id = generate_proxy_id();
|
||||
let upstream = upstream_url.unwrap_or_else(|| "DIRECT".to_string());
|
||||
|
||||
// Get available port if not specified
|
||||
let local_port = port.unwrap_or_else(|| {
|
||||
// Find an available port
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
listener.local_addr().unwrap().port()
|
||||
});
|
||||
|
||||
let config = ProxyConfig::new(id.clone(), upstream, Some(local_port));
|
||||
save_proxy_config(&config)?;
|
||||
|
||||
// Spawn proxy worker process in the background using std::process::Command
|
||||
// This ensures proper process detachment on Unix systems
|
||||
let exe = std::env::current_exe()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
let mut cmd = StdCommand::new(&exe);
|
||||
cmd.arg("proxy-worker");
|
||||
cmd.arg("start");
|
||||
cmd.arg("--id");
|
||||
cmd.arg(&id);
|
||||
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let log_path = std::path::PathBuf::from("/tmp").join(format!("donut-proxy-{}.log", id));
|
||||
if let Ok(file) = std::fs::File::create(&log_path) {
|
||||
eprintln!("Proxy worker stderr will be logged to: {:?}", log_path);
|
||||
cmd.stderr(Stdio::from(file));
|
||||
} else {
|
||||
cmd.stderr(Stdio::null());
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
cmd.stderr(Stdio::null());
|
||||
}
|
||||
|
||||
// Properly detach the process on Unix by creating a new session
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
// Create a new process group so the process survives parent exit
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn detached process
|
||||
let child = cmd.spawn()?;
|
||||
let pid = child.id();
|
||||
|
||||
// Store PID
|
||||
{
|
||||
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
||||
processes.insert(id.clone(), pid);
|
||||
}
|
||||
|
||||
// Update config with PID
|
||||
let mut config_with_pid = config.clone();
|
||||
config_with_pid.pid = Some(pid);
|
||||
save_proxy_config(&config_with_pid)?;
|
||||
|
||||
// Don't wait for the child - it's detached
|
||||
drop(child);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
let mut cmd = StdCommand::new(&exe);
|
||||
cmd.arg("proxy-worker");
|
||||
cmd.arg("start");
|
||||
cmd.arg("--id");
|
||||
cmd.arg(&id);
|
||||
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
cmd.stderr(Stdio::null());
|
||||
|
||||
// On Windows, use CREATE_NEW_PROCESS_GROUP flag for proper detachment
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
|
||||
|
||||
let child = cmd.spawn()?;
|
||||
let pid = child.id();
|
||||
|
||||
// Store PID
|
||||
{
|
||||
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
||||
processes.insert(id.clone(), pid);
|
||||
}
|
||||
|
||||
// Update config with PID
|
||||
let mut config_with_pid = config.clone();
|
||||
config_with_pid.pid = Some(pid);
|
||||
save_proxy_config(&config_with_pid)?;
|
||||
|
||||
drop(child);
|
||||
}
|
||||
|
||||
// Give the process a moment to start up before checking
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
// Wait for the worker to bind to the port and update config
|
||||
// Since we pre-allocated the port, the worker should bind immediately
|
||||
// We check quickly with short intervals to make startup fast
|
||||
let mut attempts = 0;
|
||||
let max_attempts = 40; // 4 seconds max (40 * 100ms) - give it more time to start
|
||||
|
||||
loop {
|
||||
// Use shorter sleep for faster startup
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
if let Some(updated_config) = get_proxy_config(&id) {
|
||||
// Check if local_url is set (worker has bound and updated config)
|
||||
if let Some(ref local_url) = updated_config.local_url {
|
||||
if !local_url.is_empty() {
|
||||
if let Some(port) = updated_config.local_port {
|
||||
// Try to connect immediately - port should be ready since we pre-allocated it
|
||||
match tokio::time::timeout(
|
||||
tokio::time::Duration::from_millis(100),
|
||||
tokio::net::TcpStream::connect(("127.0.0.1", port)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_stream)) => {
|
||||
// Port is listening and accepting connections!
|
||||
return Ok(updated_config);
|
||||
}
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
// Port not ready yet, continue waiting
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
if attempts >= max_attempts {
|
||||
// Try to get the config one more time for better error message
|
||||
if let Some(config) = get_proxy_config(&id) {
|
||||
// Check if process is still running
|
||||
let process_running = config.pid.map(is_process_running).unwrap_or(false);
|
||||
return Err(
|
||||
format!(
|
||||
"Proxy worker failed to start in time. Config: id={}, local_url={:?}, local_port={:?}, pid={:?}, process_running={}",
|
||||
config.id, config.local_url, config.local_port, config.pid, process_running
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
return Err(
|
||||
format!(
|
||||
"Proxy worker failed to start in time. Config not found for id: {}",
|
||||
id
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_proxy_process(id: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let config = get_proxy_config(id);
|
||||
|
||||
if let Some(config) = config {
|
||||
if let Some(pid) = config.pid {
|
||||
// Kill the process
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::process::Command;
|
||||
let _ = Command::new("kill")
|
||||
.arg("-TERM")
|
||||
.arg(pid.to_string())
|
||||
.output();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::process::Command;
|
||||
let _ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
}
|
||||
|
||||
// Wait a bit for the process to exit
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Remove from tracking
|
||||
{
|
||||
let mut processes = PROXY_PROCESSES.lock().unwrap();
|
||||
processes.remove(id);
|
||||
}
|
||||
|
||||
// Delete the config file
|
||||
delete_proxy_config(id);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn stop_all_proxy_processes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let configs = list_proxy_configs();
|
||||
for config in configs {
|
||||
let _ = stop_proxy_process(&config.id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,737 @@
|
||||
use crate::proxy_storage::ProxyConfig;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Bytes;
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::convert::Infallible;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
|
||||
// Wrapper to prepend consumed bytes to a stream
|
||||
struct PrependReader {
|
||||
prepended: Vec<u8>,
|
||||
prepended_pos: usize,
|
||||
inner: TcpStream,
|
||||
}
|
||||
|
||||
impl AsyncRead for PrependReader {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
// First, read from prepended bytes if any
|
||||
if self.prepended_pos < self.prepended.len() {
|
||||
let available = self.prepended.len() - self.prepended_pos;
|
||||
let to_copy = available.min(buf.remaining());
|
||||
buf.put_slice(&self.prepended[self.prepended_pos..self.prepended_pos + to_copy]);
|
||||
self.prepended_pos += to_copy;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Then read from inner stream
|
||||
Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for PrependReader {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
req: Request<hyper::body::Incoming>,
|
||||
upstream_url: Option<String>,
|
||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
// Handle CONNECT method for HTTPS tunneling
|
||||
if req.method() == Method::CONNECT {
|
||||
return handle_connect(req, upstream_url).await;
|
||||
}
|
||||
|
||||
// Handle regular HTTP requests
|
||||
handle_http(req, upstream_url).await
|
||||
}
|
||||
|
||||
async fn handle_connect(
|
||||
req: Request<hyper::body::Incoming>,
|
||||
upstream_url: Option<String>,
|
||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
let authority = req.uri().authority().cloned();
|
||||
|
||||
if let Some(authority) = authority {
|
||||
let target_addr = format!("{}", authority);
|
||||
|
||||
// Parse target host and port
|
||||
let (target_host, target_port) = if let Some(colon_pos) = target_addr.find(':') {
|
||||
let host = &target_addr[..colon_pos];
|
||||
let port: u16 = target_addr[colon_pos + 1..].parse().unwrap_or(443);
|
||||
(host, port)
|
||||
} else {
|
||||
(&target_addr[..], 443)
|
||||
};
|
||||
|
||||
// If no upstream proxy, connect directly
|
||||
if upstream_url.is_none()
|
||||
|| upstream_url
|
||||
.as_ref()
|
||||
.map(|s| s == "DIRECT")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
match TcpStream::connect(&target_addr).await {
|
||||
Ok(_stream) => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("")));
|
||||
*response.status_mut() = StatusCode::from_u16(200).unwrap();
|
||||
return Ok(response);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to {}: {}", target_addr, e);
|
||||
let mut response =
|
||||
Response::new(Full::new(Bytes::from(format!("Connection failed: {}", e))));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect through upstream proxy
|
||||
let upstream = match upstream_url.as_ref().and_then(|u| Url::parse(u).ok()) {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("Invalid upstream URL")));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
let scheme = upstream.scheme();
|
||||
match scheme {
|
||||
"http" | "https" => {
|
||||
// Use manual CONNECT for HTTP/HTTPS proxies
|
||||
match connect_via_http_proxy(&upstream, target_host, target_port).await {
|
||||
Ok(_) => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("")));
|
||||
*response.status_mut() = StatusCode::from_u16(200).unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("HTTP proxy CONNECT failed: {}", e);
|
||||
let mut response = Response::new(Full::new(Bytes::from(format!(
|
||||
"Proxy connection failed: {}",
|
||||
e
|
||||
))));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
"socks4" | "socks5" => {
|
||||
// Use async-socks5 for SOCKS proxies
|
||||
let host = upstream.host_str().unwrap_or("127.0.0.1");
|
||||
let port = upstream.port().unwrap_or(1080);
|
||||
let socks_addr = format!("{}:{}", host, port);
|
||||
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
|
||||
match connect_via_socks(
|
||||
&socks_addr,
|
||||
target_host,
|
||||
target_port,
|
||||
scheme == "socks5",
|
||||
if !username.is_empty() {
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_stream) => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("")));
|
||||
*response.status_mut() = StatusCode::from_u16(200).unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("SOCKS connection failed: {}", e);
|
||||
let mut response = Response::new(Full::new(Bytes::from(format!(
|
||||
"SOCKS connection failed: {}",
|
||||
e
|
||||
))));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("Unsupported upstream scheme")));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut response = Response::new(Full::new(Bytes::from("Bad Request")));
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_via_http_proxy(
|
||||
upstream: &Url,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
) -> Result<TcpStream, Box<dyn std::error::Error>> {
|
||||
let proxy_host = upstream.host_str().unwrap_or("127.0.0.1");
|
||||
let proxy_port = upstream.port().unwrap_or(8080);
|
||||
let mut stream = TcpStream::connect((proxy_host, proxy_port)).await?;
|
||||
|
||||
// Add proxy authentication if provided
|
||||
let mut connect_req = format!(
|
||||
"CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n",
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
|
||||
if !upstream.username().is_empty() {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
|
||||
connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
|
||||
}
|
||||
|
||||
connect_req.push_str("\r\n");
|
||||
|
||||
stream.write_all(connect_req.as_bytes()).await?;
|
||||
|
||||
let mut buffer = [0u8; 4096];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
if response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200") {
|
||||
Ok(stream)
|
||||
} else {
|
||||
Err(format!("Upstream proxy CONNECT failed: {}", response).into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_via_socks(
|
||||
socks_addr: &str,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
is_socks5: bool,
|
||||
auth: Option<(&str, &str)>,
|
||||
) -> Result<TcpStream, Box<dyn std::error::Error>> {
|
||||
let mut stream = TcpStream::connect(socks_addr).await?;
|
||||
|
||||
if is_socks5 {
|
||||
// SOCKS5 connection using async_socks5
|
||||
use async_socks5::{connect, AddrKind, Auth};
|
||||
|
||||
let target = if let Ok(ip) = target_host.parse::<std::net::IpAddr>() {
|
||||
AddrKind::Ip(std::net::SocketAddr::new(ip, target_port))
|
||||
} else {
|
||||
AddrKind::Domain(target_host.to_string(), target_port)
|
||||
};
|
||||
|
||||
let auth_info: Option<Auth> = auth.map(|(user, pass)| Auth {
|
||||
username: user.to_string(),
|
||||
password: pass.to_string(),
|
||||
});
|
||||
|
||||
connect(&mut stream, target, auth_info).await?;
|
||||
Ok(stream)
|
||||
} else {
|
||||
// SOCKS4 - simplified implementation
|
||||
let ip: std::net::IpAddr = target_host.parse()?;
|
||||
|
||||
let mut request = vec![0x04, 0x01]; // SOCKS4, CONNECT
|
||||
request.extend_from_slice(&target_port.to_be_bytes());
|
||||
match ip {
|
||||
std::net::IpAddr::V4(ipv4) => {
|
||||
request.extend_from_slice(&ipv4.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(_) => {
|
||||
return Err("SOCKS4 does not support IPv6".into());
|
||||
}
|
||||
}
|
||||
request.push(0); // NULL terminator for userid
|
||||
|
||||
stream.write_all(&request).await?;
|
||||
|
||||
let mut response = [0u8; 8];
|
||||
stream.read_exact(&mut response).await?;
|
||||
|
||||
if response[1] != 0x5A {
|
||||
return Err("SOCKS4 connection failed".into());
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_http(
|
||||
req: Request<hyper::body::Incoming>,
|
||||
upstream_url: Option<String>,
|
||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
// Use reqwest for all HTTP requests as it handles proxies better
|
||||
// This is faster and more reliable than trying to use hyper-proxy with version conflicts
|
||||
use reqwest::Client;
|
||||
|
||||
let client_builder = Client::builder();
|
||||
let client = if let Some(ref upstream) = upstream_url {
|
||||
if upstream == "DIRECT" {
|
||||
client_builder.build().unwrap_or_default()
|
||||
} else {
|
||||
// Build reqwest client with proxy
|
||||
match build_reqwest_client_with_proxy(upstream) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create proxy client: {}", e);
|
||||
let mut response = Response::new(Full::new(Bytes::from(format!(
|
||||
"Proxy configuration error: {}",
|
||||
e
|
||||
))));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client_builder.build().unwrap_or_default()
|
||||
};
|
||||
|
||||
// Convert hyper request to reqwest request
|
||||
let uri = req.uri().to_string();
|
||||
let method = req.method().clone();
|
||||
let headers = req.headers().clone();
|
||||
|
||||
let mut request_builder = match method.as_str() {
|
||||
"GET" => client.get(&uri),
|
||||
"POST" => client.post(&uri),
|
||||
"PUT" => client.put(&uri),
|
||||
"DELETE" => client.delete(&uri),
|
||||
"PATCH" => client.patch(&uri),
|
||||
"HEAD" => client.head(&uri),
|
||||
_ => {
|
||||
let mut response = Response::new(Full::new(Bytes::from("Unsupported method")));
|
||||
*response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
// Copy headers, but skip proxy-specific headers that shouldn't be forwarded
|
||||
for (name, value) in headers.iter() {
|
||||
// Skip proxy-specific headers - these are for the local proxy, not the upstream
|
||||
if name.as_str().eq_ignore_ascii_case("proxy-authorization")
|
||||
|| name.as_str().eq_ignore_ascii_case("proxy-connection")
|
||||
|| name.as_str().eq_ignore_ascii_case("proxy-authenticate")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Ok(val) = value.to_str() {
|
||||
request_builder = request_builder.header(name.as_str(), val);
|
||||
}
|
||||
}
|
||||
|
||||
// Get body
|
||||
let body_bytes = match req.collect().await {
|
||||
Ok(collected) => collected.to_bytes(),
|
||||
Err(_) => Bytes::new(),
|
||||
};
|
||||
|
||||
if !body_bytes.is_empty() {
|
||||
request_builder = request_builder.body(body_bytes.to_vec());
|
||||
}
|
||||
|
||||
// Execute request
|
||||
match request_builder.send().await {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body = response.bytes().await.unwrap_or_default();
|
||||
|
||||
let mut hyper_response = Response::new(Full::new(body));
|
||||
*hyper_response.status_mut() = StatusCode::from_u16(status.as_u16()).unwrap();
|
||||
|
||||
// Copy response headers
|
||||
for (name, value) in headers.iter() {
|
||||
if let Ok(val) = value.to_str() {
|
||||
hyper_response
|
||||
.headers_mut()
|
||||
.insert(name, val.parse().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hyper_response)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Request failed: {}", e);
|
||||
let mut response = Response::new(Full::new(Bytes::from(format!("Request failed: {}", e))));
|
||||
*response.status_mut() = StatusCode::BAD_GATEWAY;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_reqwest_client_with_proxy(
|
||||
upstream_url: &str,
|
||||
) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
|
||||
use reqwest::Proxy;
|
||||
|
||||
let client_builder = reqwest::Client::builder();
|
||||
|
||||
// Parse the upstream URL
|
||||
let url = Url::parse(upstream_url)?;
|
||||
let scheme = url.scheme();
|
||||
|
||||
let proxy = match scheme {
|
||||
"http" | "https" => {
|
||||
// For HTTP/HTTPS proxies, reqwest handles them directly
|
||||
Proxy::http(upstream_url)?
|
||||
}
|
||||
"socks5" => {
|
||||
// For SOCKS5, reqwest supports it directly
|
||||
Proxy::all(upstream_url)?
|
||||
}
|
||||
"socks4" => {
|
||||
// SOCKS4 is not directly supported by reqwest, would need custom handling
|
||||
return Err("SOCKS4 not supported for HTTP requests via reqwest".into());
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unsupported proxy scheme: {}", scheme).into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(client_builder.proxy(proxy).build()?)
|
||||
}
|
||||
|
||||
pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
eprintln!(
|
||||
"Proxy worker starting, looking for config id: {}",
|
||||
config.id
|
||||
);
|
||||
|
||||
// Load the config from disk to get the latest state
|
||||
let config = match crate::proxy_storage::get_proxy_config(&config.id) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
eprintln!("Config not found for id: {}", config.id);
|
||||
return Err("Config not found".into());
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"Found config: id={}, port={:?}, upstream={}",
|
||||
config.id, config.local_port, config.upstream_url
|
||||
);
|
||||
|
||||
eprintln!("Starting proxy server for config id: {}", config.id);
|
||||
|
||||
// Determine the bind address
|
||||
let bind_addr = SocketAddr::from(([127, 0, 0, 1], config.local_port.unwrap_or(0)));
|
||||
|
||||
eprintln!("Attempting to bind proxy server to {}", bind_addr);
|
||||
|
||||
// Bind to the port
|
||||
let listener = TcpListener::bind(bind_addr).await?;
|
||||
let actual_port = listener.local_addr()?.port();
|
||||
|
||||
eprintln!("Successfully bound to port {}", actual_port);
|
||||
|
||||
// Update config with actual port and local_url
|
||||
let mut updated_config = config.clone();
|
||||
updated_config.local_port = Some(actual_port);
|
||||
updated_config.local_url = Some(format!("http://127.0.0.1:{}", actual_port));
|
||||
|
||||
// Save the updated config
|
||||
eprintln!(
|
||||
"Saving updated config with local_url={:?}",
|
||||
updated_config.local_url
|
||||
);
|
||||
if !crate::proxy_storage::update_proxy_config(&updated_config) {
|
||||
eprintln!("Failed to update proxy config");
|
||||
return Err("Failed to update proxy config".into());
|
||||
}
|
||||
|
||||
let upstream_url = if updated_config.upstream_url == "DIRECT" {
|
||||
None
|
||||
} else {
|
||||
Some(updated_config.upstream_url.clone())
|
||||
};
|
||||
|
||||
eprintln!("Proxy server bound to 127.0.0.1:{}", actual_port);
|
||||
eprintln!(
|
||||
"Proxy server listening on 127.0.0.1:{} (ready to accept connections)",
|
||||
actual_port
|
||||
);
|
||||
eprintln!("Proxy server entering accept loop - process should stay alive");
|
||||
|
||||
// Keep the runtime alive with an infinite loop
|
||||
// This ensures the process doesn't exit even if there are no active connections
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut stream, _)) => {
|
||||
let upstream = upstream_url.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
// Read first bytes to detect CONNECT requests
|
||||
// CONNECT requests need special handling for tunneling
|
||||
let mut peek_buffer = [0u8; 8];
|
||||
match stream.read(&mut peek_buffer).await {
|
||||
Ok(n) if n >= 7 => {
|
||||
let request_start = String::from_utf8_lossy(&peek_buffer[..n.min(7)]);
|
||||
if request_start.starts_with("CONNECT") {
|
||||
// Handle CONNECT request manually for tunneling
|
||||
let mut full_request = Vec::with_capacity(4096);
|
||||
full_request.extend_from_slice(&peek_buffer[..n]);
|
||||
|
||||
// Read the rest of the CONNECT request
|
||||
let mut remaining = [0u8; 4096];
|
||||
loop {
|
||||
match stream.read(&mut remaining).await {
|
||||
Ok(0) => break,
|
||||
Ok(m) => {
|
||||
full_request.extend_from_slice(&remaining[..m]);
|
||||
if full_request.ends_with(b"\r\n\r\n") || full_request.ends_with(b"\n\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle CONNECT manually
|
||||
eprintln!(
|
||||
"DEBUG: Handling CONNECT manually for: {}",
|
||||
String::from_utf8_lossy(&full_request[..full_request.len().min(100)])
|
||||
);
|
||||
if let Err(e) = handle_connect_from_buffer(stream, full_request, upstream).await {
|
||||
eprintln!("Error handling CONNECT request: {:?}", e);
|
||||
} else {
|
||||
eprintln!("DEBUG: CONNECT handled successfully");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Not CONNECT - reconstruct stream with consumed bytes prepended
|
||||
let prepended_bytes = peek_buffer[..n].to_vec();
|
||||
let prepended_reader = PrependReader {
|
||||
prepended: prepended_bytes,
|
||||
prepended_pos: 0,
|
||||
inner: stream,
|
||||
};
|
||||
let io = TokioIo::new(prepended_reader);
|
||||
let service = service_fn(move |req| handle_request(req, upstream.clone()));
|
||||
|
||||
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
|
||||
eprintln!("Error serving connection: {:?}", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// For non-CONNECT requests, use hyper's HTTP handling
|
||||
let io = TokioIo::new(stream);
|
||||
let service = service_fn(move |req| handle_request(req, upstream.clone()));
|
||||
|
||||
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
|
||||
eprintln!("Error serving connection: {:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error accepting connection: {:?}", e);
|
||||
// Continue accepting connections even if one fails
|
||||
// Add a small delay to avoid busy-waiting on errors
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connect_from_buffer(
|
||||
mut client_stream: TcpStream,
|
||||
request_buffer: Vec<u8>,
|
||||
upstream_url: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Parse the CONNECT request from the buffer
|
||||
let request_str = String::from_utf8_lossy(&request_buffer);
|
||||
let lines: Vec<&str> = request_str.lines().collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
let _ = client_stream
|
||||
.write_all(b"HTTP/1.1 400 Bad Request\r\n\r\n")
|
||||
.await;
|
||||
return Err("Empty CONNECT request".into());
|
||||
}
|
||||
|
||||
// Parse CONNECT request: "CONNECT host:port HTTP/1.1"
|
||||
let parts: Vec<&str> = lines[0].split_whitespace().collect();
|
||||
if parts.len() < 2 || parts[0] != "CONNECT" {
|
||||
let _ = client_stream
|
||||
.write_all(b"HTTP/1.1 400 Bad Request\r\n\r\n")
|
||||
.await;
|
||||
return Err("Invalid CONNECT request".into());
|
||||
}
|
||||
|
||||
let target = parts[1];
|
||||
let (target_host, target_port) = if let Some(colon_pos) = target.find(':') {
|
||||
let host = &target[..colon_pos];
|
||||
let port: u16 = target[colon_pos + 1..].parse().unwrap_or(443);
|
||||
(host, port)
|
||||
} else {
|
||||
(target, 443)
|
||||
};
|
||||
|
||||
// Connect to target (directly or via upstream proxy)
|
||||
let target_stream = if upstream_url.is_none()
|
||||
|| upstream_url
|
||||
.as_ref()
|
||||
.map(|s| s == "DIRECT")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Direct connection
|
||||
TcpStream::connect((target_host, target_port)).await?
|
||||
} else {
|
||||
// Connect via upstream proxy
|
||||
let upstream = Url::parse(upstream_url.as_ref().unwrap())?;
|
||||
let scheme = upstream.scheme();
|
||||
|
||||
match scheme {
|
||||
"http" | "https" => {
|
||||
// Connect via HTTP proxy CONNECT
|
||||
let proxy_host = upstream.host_str().unwrap_or("127.0.0.1");
|
||||
let proxy_port = upstream.port().unwrap_or(8080);
|
||||
let mut proxy_stream = TcpStream::connect((proxy_host, proxy_port)).await?;
|
||||
|
||||
// Add authentication if provided
|
||||
let mut connect_req = format!(
|
||||
"CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n",
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
|
||||
if !upstream.username().is_empty() {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
let auth = general_purpose::STANDARD.encode(format!("{}:{}", username, password));
|
||||
connect_req.push_str(&format!("Proxy-Authorization: Basic {}\r\n", auth));
|
||||
}
|
||||
|
||||
connect_req.push_str("\r\n");
|
||||
|
||||
// Send CONNECT request to upstream proxy
|
||||
proxy_stream.write_all(connect_req.as_bytes()).await?;
|
||||
|
||||
// Read response
|
||||
let mut buffer = [0u8; 4096];
|
||||
let n = proxy_stream.read(&mut buffer).await?;
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
if !response.starts_with("HTTP/1.1 200") && !response.starts_with("HTTP/1.0 200") {
|
||||
return Err(format!("Upstream proxy CONNECT failed: {}", response).into());
|
||||
}
|
||||
|
||||
proxy_stream
|
||||
}
|
||||
"socks4" | "socks5" => {
|
||||
// Connect via SOCKS proxy
|
||||
let socks_host = upstream.host_str().unwrap_or("127.0.0.1");
|
||||
let socks_port = upstream.port().unwrap_or(1080);
|
||||
let socks_addr = format!("{}:{}", socks_host, socks_port);
|
||||
|
||||
let username = upstream.username();
|
||||
let password = upstream.password().unwrap_or("");
|
||||
|
||||
connect_via_socks(
|
||||
&socks_addr,
|
||||
target_host,
|
||||
target_port,
|
||||
scheme == "socks5",
|
||||
if !username.is_empty() {
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unsupported upstream proxy scheme: {}", scheme).into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Send 200 Connection Established response to client
|
||||
// CRITICAL: Must flush after writing to ensure response is sent before tunneling
|
||||
client_stream
|
||||
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
.await?;
|
||||
client_stream.flush().await?;
|
||||
|
||||
eprintln!("DEBUG: Sent 200 Connection Established response, starting tunnel");
|
||||
|
||||
// Now tunnel data bidirectionally
|
||||
// Split streams for bidirectional copying
|
||||
let (mut client_read, mut client_write) = tokio::io::split(client_stream);
|
||||
let (mut target_read, mut target_write) = tokio::io::split(target_stream);
|
||||
|
||||
eprintln!("DEBUG: Starting bidirectional tunnel");
|
||||
|
||||
// Spawn two tasks to forward data in both directions
|
||||
let client_to_target = tokio::spawn(async move {
|
||||
let result = tokio::io::copy(&mut client_read, &mut target_write).await;
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
eprintln!("DEBUG: Tunneled {} bytes from client->target", bytes);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error forwarding client->target: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let target_to_client = tokio::spawn(async move {
|
||||
let result = tokio::io::copy(&mut target_read, &mut client_write).await;
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
eprintln!("DEBUG: Tunneled {} bytes from target->client", bytes);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error forwarding target->client: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for either direction to finish (connection closed)
|
||||
tokio::select! {
|
||||
_ = client_to_target => {
|
||||
eprintln!("DEBUG: Client->target tunnel closed");
|
||||
}
|
||||
_ = target_to_client => {
|
||||
eprintln!("DEBUG: Target->client tunnel closed");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::proxy_runner::{start_proxy_process, stop_proxy_process};
|
||||
use crate::proxy_storage::{delete_proxy_config, generate_proxy_id, list_proxy_configs};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_storage() {
|
||||
// Test proxy config storage
|
||||
let id = generate_proxy_id();
|
||||
let config = crate::proxy_storage::ProxyConfig::new(id.clone(), "DIRECT".to_string(), Some(8080));
|
||||
|
||||
// Save config
|
||||
crate::proxy_storage::save_proxy_config(&config).unwrap();
|
||||
|
||||
// Load config
|
||||
let loaded = crate::proxy_storage::get_proxy_config(&id).unwrap();
|
||||
assert_eq!(loaded.id, id);
|
||||
assert_eq!(loaded.upstream_url, "DIRECT");
|
||||
assert_eq!(loaded.local_port, Some(8080));
|
||||
|
||||
// Delete config
|
||||
assert!(crate::proxy_storage::delete_proxy_config(&id));
|
||||
assert!(crate::proxy_storage::get_proxy_config(&id).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_id_generation() {
|
||||
let id1 = generate_proxy_id();
|
||||
let id2 = generate_proxy_id();
|
||||
assert_ne!(id1, id2);
|
||||
assert!(id1.starts_with("proxy_"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_process_lifecycle() {
|
||||
// Start a direct proxy
|
||||
let config = start_proxy_process(None, Some(0)).await.unwrap();
|
||||
let id = config.id.clone();
|
||||
|
||||
// Verify config was saved
|
||||
let loaded = crate::proxy_storage::get_proxy_config(&id).unwrap();
|
||||
assert_eq!(loaded.id, id);
|
||||
|
||||
// Wait a bit for the proxy to start
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Stop the proxy
|
||||
let stopped = stop_proxy_process(&id).await.unwrap();
|
||||
assert!(stopped);
|
||||
|
||||
// Verify config was deleted
|
||||
assert!(crate::proxy_storage::get_proxy_config(&id).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_with_upstream_http() {
|
||||
// Start a proxy with HTTP upstream (using a non-existent proxy for testing)
|
||||
let upstream_url = "http://127.0.0.1:9999";
|
||||
let config = start_proxy_process(Some(upstream_url.to_string()), Some(0))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = config.id.clone();
|
||||
|
||||
// Wait a bit
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Clean up
|
||||
let _ = stop_proxy_process(&id).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_with_upstream_socks5() {
|
||||
// Start a proxy with SOCKS5 upstream
|
||||
let upstream_url = "socks5://127.0.0.1:1080";
|
||||
let config = start_proxy_process(Some(upstream_url.to_string()), Some(0))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = config.id.clone();
|
||||
|
||||
// Wait a bit
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Clean up
|
||||
let _ = stop_proxy_process(&id).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_port_assignment() {
|
||||
// Start multiple proxies and verify they get different ports
|
||||
let config1 = start_proxy_process(None, None).await.unwrap();
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
let config2 = start_proxy_process(None, None).await.unwrap();
|
||||
|
||||
// They should have different IDs
|
||||
assert_ne!(config1.id, config2.id);
|
||||
|
||||
// Clean up
|
||||
let _ = stop_proxy_process(&config1.id).await;
|
||||
let _ = stop_proxy_process(&config2.id).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_list() {
|
||||
// Start a few proxies
|
||||
let config1 = start_proxy_process(None, None).await.unwrap();
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
let config2 = start_proxy_process(None, None).await.unwrap();
|
||||
|
||||
// List all proxies
|
||||
let configs = list_proxy_configs();
|
||||
assert!(configs.len() >= 2);
|
||||
|
||||
// Clean up
|
||||
let _ = stop_proxy_process(&config1.id).await;
|
||||
let _ = stop_proxy_process(&config2.id).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use directories::BaseDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProxyConfig {
|
||||
pub id: String,
|
||||
pub upstream_url: String, // Can be "DIRECT" for direct proxy
|
||||
pub local_port: Option<u16>,
|
||||
pub ignore_proxy_certificate: Option<bool>,
|
||||
pub local_url: Option<String>,
|
||||
pub pid: Option<u32>,
|
||||
}
|
||||
|
||||
impl ProxyConfig {
|
||||
pub fn new(id: String, upstream_url: String, local_port: Option<u16>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
upstream_url,
|
||||
local_port,
|
||||
ignore_proxy_certificate: None,
|
||||
local_url: None,
|
||||
pid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_storage_dir() -> PathBuf {
|
||||
let base_dirs = BaseDirs::new().expect("Failed to get base directories");
|
||||
let mut path = base_dirs.data_local_dir().to_path_buf();
|
||||
path.push(if cfg!(debug_assertions) {
|
||||
"DonutBrowserDev"
|
||||
} else {
|
||||
"DonutBrowser"
|
||||
});
|
||||
path.push("proxies");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn save_proxy_config(config: &ProxyConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let storage_dir = get_storage_dir();
|
||||
fs::create_dir_all(&storage_dir)?;
|
||||
|
||||
let file_path = storage_dir.join(format!("{}.json", config.id));
|
||||
let content = serde_json::to_string_pretty(config)?;
|
||||
fs::write(&file_path, content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_proxy_config(id: &str) -> Option<ProxyConfig> {
|
||||
let storage_dir = get_storage_dir();
|
||||
let file_path = storage_dir.join(format!("{}.json", id));
|
||||
|
||||
if !file_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match fs::read_to_string(&file_path) {
|
||||
Ok(content) => serde_json::from_str(&content).ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_proxy_config(id: &str) -> bool {
|
||||
let storage_dir = get_storage_dir();
|
||||
let file_path = storage_dir.join(format!("{}.json", id));
|
||||
|
||||
if !file_path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::remove_file(&file_path).is_ok()
|
||||
}
|
||||
|
||||
pub fn list_proxy_configs() -> Vec<ProxyConfig> {
|
||||
let storage_dir = get_storage_dir();
|
||||
|
||||
if !storage_dir.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut configs = Vec::new();
|
||||
if let Ok(entries) = fs::read_dir(&storage_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "json") {
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
if let Ok(config) = serde_json::from_str::<ProxyConfig>(&content) {
|
||||
configs.push(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
pub fn update_proxy_config(config: &ProxyConfig) -> bool {
|
||||
let storage_dir = get_storage_dir();
|
||||
let file_path = storage_dir.join(format!("{}.json", config.id));
|
||||
|
||||
if !file_path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match serde_json::to_string_pretty(config) {
|
||||
Ok(content) => fs::write(&file_path, content).is_ok(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_proxy_id() -> String {
|
||||
format!(
|
||||
"proxy_{}_{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
rand::random::<u32>()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_process_running(pid: u32) -> bool {
|
||||
use sysinfo::{Pid, System};
|
||||
let system = System::new_all();
|
||||
system.process(Pid::from(pid as usize)).is_some()
|
||||
}
|
||||
@@ -545,13 +545,22 @@ mod tests {
|
||||
Err(_) => return BackgroundUpdateState::default(),
|
||||
};
|
||||
|
||||
serde_json::from_str(&content).unwrap_or_default()
|
||||
match serde_json::from_str(&content) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse test state file {:?}: {}", state_file, e);
|
||||
BackgroundUpdateState::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_update_state_persistence() {
|
||||
let test_name = "persistence";
|
||||
|
||||
// Clean up any existing test file first
|
||||
let _ = fs::remove_file(get_test_state_file(test_name));
|
||||
|
||||
// Create a test state
|
||||
let test_state = BackgroundUpdateState {
|
||||
last_update_time: 1609459200, // 2021-01-01 00:00:00 UTC
|
||||
@@ -561,14 +570,22 @@ mod tests {
|
||||
// Save the state
|
||||
save_test_state(test_name, &test_state).unwrap();
|
||||
|
||||
// Verify file was created
|
||||
let state_file = get_test_state_file(test_name);
|
||||
assert!(state_file.exists(), "State file should exist after saving");
|
||||
|
||||
// Load the state back
|
||||
let loaded_state = load_test_state(test_name);
|
||||
|
||||
// Verify the values match
|
||||
assert_eq!(loaded_state.last_update_time, test_state.last_update_time);
|
||||
assert_eq!(
|
||||
loaded_state.update_interval_hours,
|
||||
test_state.update_interval_hours
|
||||
loaded_state.last_update_time, test_state.last_update_time,
|
||||
"last_update_time should match. Expected: {}, Got: {}",
|
||||
test_state.last_update_time, loaded_state.last_update_time
|
||||
);
|
||||
assert_eq!(
|
||||
loaded_state.update_interval_hours, test_state.update_interval_hours,
|
||||
"update_interval_hours should match"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
@@ -628,37 +645,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_dir_creation() {
|
||||
// This should not panic and should create the directory if it doesn't exist
|
||||
let cache_dir_result = VersionUpdater::get_cache_dir();
|
||||
assert!(
|
||||
cache_dir_result.is_ok(),
|
||||
"Should successfully get cache directory"
|
||||
);
|
||||
|
||||
let cache_dir = cache_dir_result.unwrap();
|
||||
assert!(
|
||||
cache_dir.exists(),
|
||||
"Cache directory should exist after creation"
|
||||
);
|
||||
assert!(cache_dir.is_dir(), "Cache directory should be a directory");
|
||||
|
||||
// Verify the path contains expected components
|
||||
let path_str = cache_dir.to_string_lossy();
|
||||
assert!(
|
||||
path_str.contains("version_cache"),
|
||||
"Path should contain version_cache"
|
||||
);
|
||||
|
||||
// Test that calling it again returns the same directory
|
||||
let cache_dir2 = VersionUpdater::get_cache_dir().unwrap();
|
||||
assert_eq!(
|
||||
cache_dir, cache_dir2,
|
||||
"Multiple calls should return same directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_updater_creation() {
|
||||
let updater = VersionUpdater::new();
|
||||
|
||||
Reference in New Issue
Block a user