mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 23:13:58 +02:00
feat: add network overview
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use clap::{Arg, Command};
|
||||
use donutbrowser_lib::proxy_runner::{
|
||||
start_proxy_process, stop_all_proxy_processes, stop_proxy_process,
|
||||
start_proxy_process_with_profile, stop_all_proxy_processes, stop_proxy_process,
|
||||
};
|
||||
use donutbrowser_lib::proxy_server::run_proxy_server;
|
||||
use donutbrowser_lib::proxy_storage::get_proxy_config;
|
||||
@@ -87,6 +87,11 @@ async fn main() {
|
||||
.short('u')
|
||||
.long("upstream")
|
||||
.help("Upstream proxy URL (protocol://[username:password@]host:port)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("profile-id")
|
||||
.long("profile-id")
|
||||
.help("ID of the profile this proxy is associated with"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -138,8 +143,9 @@ async fn main() {
|
||||
}
|
||||
|
||||
let port = start_matches.get_one::<u16>("port").copied();
|
||||
let profile_id = start_matches.get_one::<String>("profile-id").cloned();
|
||||
|
||||
match start_proxy_process(upstream_url, port).await {
|
||||
match start_proxy_process_with_profile(upstream_url, port, profile_id).await {
|
||||
Ok(config) => {
|
||||
// Output the configuration as JSON for the Rust side to parse
|
||||
// Use println! here because this needs to go to stdout for parsing
|
||||
|
||||
@@ -149,12 +149,13 @@ impl BrowserRunner {
|
||||
|
||||
// Start the proxy and get local proxy settings
|
||||
// If proxy startup fails, DO NOT launch Camoufox - it requires local proxy
|
||||
let profile_id_str = profile.id.to_string();
|
||||
let local_proxy = PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
0, // Use 0 as temporary PID, will be updated later
|
||||
Some(&profile.name),
|
||||
Some(&profile_id_str),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -823,6 +824,7 @@ impl BrowserRunner {
|
||||
|
||||
// Use a temporary PID (1) to start the proxy, we'll update it after browser launch
|
||||
let temp_pid = 1u32;
|
||||
let profile_id_str = profile.id.to_string();
|
||||
|
||||
// Start local proxy - if this fails, DO NOT launch browser
|
||||
let internal_proxy = PROXY_MANAGER
|
||||
@@ -830,7 +832,7 @@ impl BrowserRunner {
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
temp_pid,
|
||||
Some(&profile.name),
|
||||
Some(&profile_id_str),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -1695,6 +1697,7 @@ 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;
|
||||
let profile_id_str = profile.id.to_string();
|
||||
|
||||
// Always start a local proxy, even if there's no upstream proxy
|
||||
// This allows for traffic monitoring and future features
|
||||
@@ -1703,7 +1706,7 @@ pub async fn launch_browser_profile(
|
||||
app_handle.clone(),
|
||||
upstream_proxy.as_ref(),
|
||||
temp_pid,
|
||||
Some(&profile.name),
|
||||
Some(&profile_id_str),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -349,6 +349,8 @@ impl CamoufoxManager {
|
||||
}
|
||||
|
||||
/// Find Camoufox server by profile path (for integration with browser_runner)
|
||||
/// This method first checks in-memory instances, then scans system processes
|
||||
/// to detect Camoufox instances that may have been started before the app restarted.
|
||||
pub async fn find_camoufox_by_profile(
|
||||
&self,
|
||||
profile_path: &str,
|
||||
@@ -356,41 +358,127 @@ impl CamoufoxManager {
|
||||
// First clean up any dead instances
|
||||
self.cleanup_dead_instances().await?;
|
||||
|
||||
let inner = self.inner.lock().await;
|
||||
|
||||
// Convert paths to canonical form for comparison
|
||||
let target_path = std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf());
|
||||
|
||||
for (id, instance) in inner.instances.iter() {
|
||||
if let Some(instance_profile_path) = &instance.profile_path {
|
||||
let instance_path = std::path::Path::new(instance_profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf());
|
||||
// Check in-memory instances first
|
||||
{
|
||||
let inner = self.inner.lock().await;
|
||||
|
||||
if instance_path == target_path {
|
||||
// Verify the server is actually running by checking the process
|
||||
if let Some(process_id) = instance.process_id {
|
||||
if self.is_server_running(process_id).await {
|
||||
// Found running Camoufox instance
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: id.clone(),
|
||||
processId: instance.process_id,
|
||||
profilePath: instance.profile_path.clone(),
|
||||
url: instance.url.clone(),
|
||||
}));
|
||||
} else {
|
||||
// Camoufox instance found but process is not running
|
||||
for (id, instance) in inner.instances.iter() {
|
||||
if let Some(instance_profile_path) = &instance.profile_path {
|
||||
let instance_path = std::path::Path::new(instance_profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf());
|
||||
|
||||
if instance_path == target_path {
|
||||
// Verify the server is actually running by checking the process
|
||||
if let Some(process_id) = instance.process_id {
|
||||
if self.is_server_running(process_id).await {
|
||||
// Found running Camoufox instance
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: id.clone(),
|
||||
processId: instance.process_id,
|
||||
profilePath: instance.profile_path.clone(),
|
||||
url: instance.url.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in in-memory instances, scan system processes
|
||||
// This handles the case where the app was restarted but Camoufox is still running
|
||||
if let Some((pid, found_profile_path)) = self.find_camoufox_process_by_profile(&target_path) {
|
||||
log::info!(
|
||||
"Found running Camoufox process (PID: {}) for profile path via system scan",
|
||||
pid
|
||||
);
|
||||
|
||||
// Register this instance in our tracking
|
||||
let instance_id = format!("recovered_{}", pid);
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.insert(
|
||||
instance_id.clone(),
|
||||
CamoufoxInstance {
|
||||
id: instance_id.clone(),
|
||||
process_id: Some(pid),
|
||||
profile_path: Some(found_profile_path.clone()),
|
||||
url: None,
|
||||
},
|
||||
);
|
||||
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: instance_id,
|
||||
processId: Some(pid),
|
||||
profilePath: Some(found_profile_path),
|
||||
url: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Scan system processes to find a Camoufox process using a specific profile path
|
||||
fn find_camoufox_process_by_profile(
|
||||
&self,
|
||||
target_path: &std::path::Path,
|
||||
) -> Option<(u32, String)> {
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
|
||||
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
|
||||
);
|
||||
|
||||
let target_path_str = target_path.to_string_lossy();
|
||||
|
||||
for (pid, process) in system.processes() {
|
||||
let cmd = process.cmd();
|
||||
if cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a Camoufox/Firefox process
|
||||
let exe_name = process.name().to_string_lossy().to_lowercase();
|
||||
let is_firefox_like = exe_name.contains("firefox")
|
||||
|| exe_name.contains("camoufox")
|
||||
|| exe_name.contains("firefox-bin");
|
||||
|
||||
if !is_firefox_like {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the command line contains our profile path
|
||||
for (i, arg) in cmd.iter().enumerate() {
|
||||
if let Some(arg_str) = arg.to_str() {
|
||||
// Check for -profile argument followed by our path
|
||||
if arg_str == "-profile" && i + 1 < cmd.len() {
|
||||
if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) {
|
||||
let cmd_path = std::path::Path::new(next_arg)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(next_arg).to_path_buf());
|
||||
|
||||
if cmd_path == target_path {
|
||||
return Some((pid.as_u32(), next_arg.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if the argument contains the profile path directly
|
||||
if arg_str.contains(&*target_path_str) {
|
||||
return Some((pid.as_u32(), target_path_str.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if servers are still alive and clean up dead instances
|
||||
pub async fn cleanup_dead_instances(
|
||||
&self,
|
||||
|
||||
+26
-1
@@ -30,6 +30,7 @@ pub mod proxy_runner;
|
||||
pub mod proxy_server;
|
||||
pub mod proxy_storage;
|
||||
mod settings_manager;
|
||||
pub mod traffic_stats;
|
||||
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
|
||||
mod tag_manager;
|
||||
mod version_updater;
|
||||
@@ -246,6 +247,27 @@ async fn is_geoip_database_available() -> Result<bool, String> {
|
||||
Ok(GeoIPDownloader::is_geoip_database_available())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_all_traffic_stats() -> Result<Vec<crate::traffic_stats::TrafficStats>, String> {
|
||||
Ok(crate::traffic_stats::list_traffic_stats())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_all_traffic_snapshots() -> Result<Vec<crate::traffic_stats::TrafficSnapshot>, String> {
|
||||
Ok(
|
||||
crate::traffic_stats::list_traffic_stats()
|
||||
.into_iter()
|
||||
.map(|s| s.to_snapshot())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn clear_all_traffic_stats() -> Result<(), String> {
|
||||
crate::traffic_stats::clear_all_traffic_stats()
|
||||
.map_err(|e| format!("Failed to clear traffic stats: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn download_geoip_database(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
let downloader = GeoIPDownloader::instance();
|
||||
@@ -756,7 +778,10 @@ pub fn run() {
|
||||
warm_up_nodecar,
|
||||
start_api_server,
|
||||
stop_api_server,
|
||||
get_api_server_status
|
||||
get_api_server_status,
|
||||
get_all_traffic_stats,
|
||||
get_all_traffic_snapshots,
|
||||
clear_all_traffic_stats
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -20,8 +20,8 @@ pub struct ProxyInfo {
|
||||
pub upstream_port: u16,
|
||||
pub upstream_type: String,
|
||||
pub local_port: u16,
|
||||
// Optional profile name to which this proxy instance is logically tied
|
||||
pub profile_name: Option<String>,
|
||||
// Optional profile ID to which this proxy instance is logically tied
|
||||
pub profile_id: Option<String>,
|
||||
}
|
||||
|
||||
// Proxy check result cache
|
||||
@@ -594,14 +594,14 @@ impl ProxyManager {
|
||||
app_handle: tauri::AppHandle,
|
||||
proxy_settings: Option<&ProxySettings>,
|
||||
browser_pid: u32,
|
||||
profile_name: Option<&str>,
|
||||
profile_id: Option<&str>,
|
||||
) -> Result<ProxySettings, String> {
|
||||
// First, proactively cleanup any dead proxies so we don't accidentally reuse stale ones
|
||||
let _ = self.cleanup_dead_proxies(app_handle.clone()).await;
|
||||
|
||||
// If we have a previous proxy tied to this profile, and the upstream settings are changing,
|
||||
// stop it before starting a new one so the change takes effect immediately.
|
||||
if let Some(name) = profile_name {
|
||||
if let Some(name) = profile_id {
|
||||
// Check if we have an active proxy recorded for this profile
|
||||
let maybe_existing_id = {
|
||||
let map = self.profile_active_proxy_ids.lock().unwrap();
|
||||
@@ -711,6 +711,11 @@ impl ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Add profile ID if provided for traffic tracking
|
||||
if let Some(id) = profile_id {
|
||||
proxy_cmd = proxy_cmd.arg("--profile-id").arg(id);
|
||||
}
|
||||
|
||||
// Execute the command and wait for it to complete
|
||||
// The donut-proxy binary should start the worker and then exit
|
||||
let output = proxy_cmd
|
||||
@@ -755,7 +760,7 @@ impl ProxyManager {
|
||||
.map(|p| p.proxy_type.clone())
|
||||
.unwrap_or_else(|| "DIRECT".to_string()),
|
||||
local_port,
|
||||
profile_name: profile_name.map(|s| s.to_string()),
|
||||
profile_id: profile_id.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
// Wait for the local proxy port to be ready to accept connections
|
||||
@@ -789,14 +794,14 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
// Store the profile proxy info for persistence
|
||||
if let Some(name) = profile_name {
|
||||
if let Some(id) = profile_id {
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
let mut profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.insert(name.to_string(), proxy_settings.clone());
|
||||
profile_proxies.insert(id.to_string(), proxy_settings.clone());
|
||||
}
|
||||
// Also record the active proxy id for this profile for quick cleanup on changes
|
||||
let mut map = self.profile_active_proxy_ids.lock().unwrap();
|
||||
map.insert(name.to_string(), proxy_info.id.clone());
|
||||
map.insert(id.to_string(), proxy_info.id.clone());
|
||||
}
|
||||
|
||||
// Return proxy settings for the browser
|
||||
@@ -815,10 +820,10 @@ impl ProxyManager {
|
||||
app_handle: tauri::AppHandle,
|
||||
browser_pid: u32,
|
||||
) -> Result<(), String> {
|
||||
let (proxy_id, profile_name): (String, Option<String>) = {
|
||||
let (proxy_id, profile_id): (String, Option<String>) = {
|
||||
let mut proxies = self.active_proxies.lock().unwrap();
|
||||
match proxies.remove(&browser_pid) {
|
||||
Some(proxy) => (proxy.id, proxy.profile_name.clone()),
|
||||
Some(proxy) => (proxy.id, proxy.profile_id.clone()),
|
||||
None => return Ok(()), // No proxy to stop
|
||||
}
|
||||
};
|
||||
@@ -842,11 +847,11 @@ impl ProxyManager {
|
||||
}
|
||||
|
||||
// Clear profile-to-proxy mapping if it references this proxy
|
||||
if let Some(name) = profile_name {
|
||||
if let Some(id) = profile_id {
|
||||
let mut map = self.profile_active_proxy_ids.lock().unwrap();
|
||||
if let Some(current_id) = map.get(&name) {
|
||||
if let Some(current_id) = map.get(&id) {
|
||||
if current_id == &proxy_id {
|
||||
map.remove(&name);
|
||||
map.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1035,7 +1040,7 @@ mod tests {
|
||||
upstream_port: 3128,
|
||||
upstream_type: "http".to_string(),
|
||||
local_port: (8000 + i) as u16,
|
||||
profile_name: None,
|
||||
profile_id: None,
|
||||
};
|
||||
|
||||
// Add proxy
|
||||
|
||||
@@ -11,6 +11,14 @@ lazy_static::lazy_static! {
|
||||
pub async fn start_proxy_process(
|
||||
upstream_url: Option<String>,
|
||||
port: Option<u16>,
|
||||
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
|
||||
start_proxy_process_with_profile(upstream_url, port, None).await
|
||||
}
|
||||
|
||||
pub async fn start_proxy_process_with_profile(
|
||||
upstream_url: Option<String>,
|
||||
port: Option<u16>,
|
||||
profile_id: Option<String>,
|
||||
) -> Result<ProxyConfig, Box<dyn std::error::Error>> {
|
||||
let id = generate_proxy_id();
|
||||
let upstream = upstream_url.unwrap_or_else(|| "DIRECT".to_string());
|
||||
@@ -22,7 +30,7 @@ pub async fn start_proxy_process(
|
||||
listener.local_addr().unwrap().port()
|
||||
});
|
||||
|
||||
let config = ProxyConfig::new(id.clone(), upstream, Some(local_port));
|
||||
let config = ProxyConfig::new(id.clone(), upstream, Some(local_port)).with_profile_id(profile_id);
|
||||
save_proxy_config(&config)?;
|
||||
|
||||
// Spawn proxy worker process in the background using std::process::Command
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::proxy_storage::ProxyConfig;
|
||||
use crate::traffic_stats::{get_traffic_tracker, init_traffic_tracker};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Bytes;
|
||||
use hyper::server::conn::http1;
|
||||
@@ -9,12 +10,79 @@ use std::convert::Infallible;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
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 stream that counts bytes read and written
|
||||
struct CountingStream<S> {
|
||||
inner: S,
|
||||
bytes_read: Arc<AtomicU64>,
|
||||
bytes_written: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<S> CountingStream<S> {
|
||||
fn new(inner: S) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bytes_read: Arc::new(AtomicU64::new(0)),
|
||||
bytes_written: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncRead + Unpin> AsyncRead for CountingStream<S> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let filled_before = buf.filled().len();
|
||||
let result = Pin::new(&mut self.inner).poll_read(cx, buf);
|
||||
if let Poll::Ready(Ok(())) = &result {
|
||||
let bytes_read = buf.filled().len() - filled_before;
|
||||
self
|
||||
.bytes_read
|
||||
.fetch_add(bytes_read as u64, Ordering::Relaxed);
|
||||
// Update global tracker
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
tracker.add_bytes_received(bytes_read as u64);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncWrite + Unpin> AsyncWrite for CountingStream<S> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let result = Pin::new(&mut self.inner).poll_write(cx, buf);
|
||||
if let Poll::Ready(Ok(n)) = &result {
|
||||
self.bytes_written.fetch_add(*n as u64, Ordering::Relaxed);
|
||||
// Update global tracker
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
tracker.add_bytes_sent(*n as u64);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to prepend consumed bytes to a stream
|
||||
struct PrependReader {
|
||||
prepended: Vec<u8>,
|
||||
@@ -297,6 +365,13 @@ async fn handle_http(
|
||||
// This is faster and more reliable than trying to use hyper-proxy with version conflicts
|
||||
use reqwest::Client;
|
||||
|
||||
// Extract domain for traffic tracking
|
||||
let domain = req
|
||||
.uri()
|
||||
.host()
|
||||
.map(|h| h.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let client_builder = Client::builder();
|
||||
let client = if let Some(ref upstream) = upstream_url {
|
||||
if upstream == "DIRECT" {
|
||||
@@ -370,6 +445,12 @@ async fn handle_http(
|
||||
let headers = response.headers().clone();
|
||||
let body = response.bytes().await.unwrap_or_default();
|
||||
|
||||
// Record request in traffic tracker
|
||||
let response_size = body.len() as u64;
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
tracker.record_request(&domain, body_bytes.len() as u64, response_size);
|
||||
}
|
||||
|
||||
let mut hyper_response = Response::new(Full::new(body));
|
||||
*hyper_response.status_mut() = StatusCode::from_u16(status.as_u16()).unwrap();
|
||||
|
||||
@@ -449,6 +530,14 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
|
||||
log::error!("Starting proxy server for config id: {}", config.id);
|
||||
|
||||
// Initialize traffic tracker with profile ID if available
|
||||
init_traffic_tracker(config.id.clone(), config.profile_id.clone());
|
||||
log::error!(
|
||||
"Traffic tracker initialized for proxy: {} (profile_id: {:?})",
|
||||
config.id,
|
||||
config.profile_id
|
||||
);
|
||||
|
||||
// Determine the bind address
|
||||
let bind_addr = SocketAddr::from(([127, 0, 0, 1], config.local_port.unwrap_or(0)));
|
||||
|
||||
@@ -488,6 +577,19 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
);
|
||||
log::error!("Proxy server entering accept loop - process should stay alive");
|
||||
|
||||
// Start a background task to periodically flush traffic stats to disk
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
if let Err(e) = tracker.flush_to_disk() {
|
||||
log::error!("Failed to flush traffic stats: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the runtime alive with an infinite loop
|
||||
// This ensures the process doesn't exit even if there are no active connections
|
||||
loop {
|
||||
@@ -605,6 +707,12 @@ async fn handle_connect_from_buffer(
|
||||
(target, 443)
|
||||
};
|
||||
|
||||
// Record domain access in traffic tracker
|
||||
let domain = target_host.to_string();
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
tracker.record_request(&domain, 0, 0);
|
||||
}
|
||||
|
||||
// Connect to target (directly or via upstream proxy)
|
||||
let target_stream = if upstream_url.is_none()
|
||||
|| upstream_url
|
||||
@@ -693,10 +801,20 @@ async fn handle_connect_from_buffer(
|
||||
|
||||
log::error!("DEBUG: Sent 200 Connection Established response, starting tunnel");
|
||||
|
||||
// Now tunnel data bidirectionally
|
||||
// Now tunnel data bidirectionally with counting
|
||||
// Wrap streams to count bytes transferred
|
||||
let counting_client = CountingStream::new(client_stream);
|
||||
let counting_target = CountingStream::new(target_stream);
|
||||
|
||||
// Get references for final stats
|
||||
let client_read_counter = counting_client.bytes_read.clone();
|
||||
let client_write_counter = counting_client.bytes_written.clone();
|
||||
let target_read_counter = counting_target.bytes_read.clone();
|
||||
let target_write_counter = counting_target.bytes_written.clone();
|
||||
|
||||
// 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);
|
||||
let (mut client_read, mut client_write) = tokio::io::split(counting_client);
|
||||
let (mut target_read, mut target_write) = tokio::io::split(counting_target);
|
||||
|
||||
log::error!("DEBUG: Starting bidirectional tunnel");
|
||||
|
||||
@@ -735,5 +853,21 @@ async fn handle_connect_from_buffer(
|
||||
}
|
||||
}
|
||||
|
||||
// Log final byte counts and update domain stats
|
||||
let final_sent =
|
||||
client_read_counter.load(Ordering::Relaxed) + target_write_counter.load(Ordering::Relaxed);
|
||||
let final_recv =
|
||||
target_read_counter.load(Ordering::Relaxed) + client_write_counter.load(Ordering::Relaxed);
|
||||
log::error!(
|
||||
"DEBUG: Tunnel closed - sent: {} bytes, received: {} bytes",
|
||||
final_sent,
|
||||
final_recv
|
||||
);
|
||||
|
||||
// Update domain-specific byte counts now that tunnel is complete
|
||||
if let Some(tracker) = get_traffic_tracker() {
|
||||
tracker.update_domain_bytes(&domain, final_sent, final_recv);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ pub struct ProxyConfig {
|
||||
pub ignore_proxy_certificate: Option<bool>,
|
||||
pub local_url: Option<String>,
|
||||
pub pid: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub profile_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ProxyConfig {
|
||||
@@ -22,8 +24,14 @@ impl ProxyConfig {
|
||||
ignore_proxy_certificate: None,
|
||||
local_url: None,
|
||||
pid: None,
|
||||
profile_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_profile_id(mut self, profile_id: Option<String>) -> Self {
|
||||
self.profile_id = profile_id;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_storage_dir() -> PathBuf {
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
use directories::BaseDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// Individual bandwidth data point for time-series tracking
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BandwidthDataPoint {
|
||||
/// Unix timestamp in seconds
|
||||
pub timestamp: u64,
|
||||
/// Bytes sent in this interval
|
||||
pub bytes_sent: u64,
|
||||
/// Bytes received in this interval
|
||||
pub bytes_received: u64,
|
||||
}
|
||||
|
||||
/// Domain access information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DomainAccess {
|
||||
/// Domain name
|
||||
pub domain: String,
|
||||
/// Number of requests to this domain
|
||||
pub request_count: u64,
|
||||
/// Total bytes sent to this domain
|
||||
pub bytes_sent: u64,
|
||||
/// Total bytes received from this domain
|
||||
pub bytes_received: u64,
|
||||
/// First access timestamp
|
||||
pub first_access: u64,
|
||||
/// Last access timestamp
|
||||
pub last_access: u64,
|
||||
}
|
||||
|
||||
/// Lightweight snapshot for real-time updates (sent via events)
|
||||
/// Contains only the data needed for the mini chart and summary display
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrafficSnapshot {
|
||||
/// Profile ID (for matching)
|
||||
pub profile_id: Option<String>,
|
||||
/// Session start timestamp
|
||||
pub session_start: u64,
|
||||
/// Last update timestamp
|
||||
pub last_update: u64,
|
||||
/// Total bytes sent across all time
|
||||
pub total_bytes_sent: u64,
|
||||
/// Total bytes received across all time
|
||||
pub total_bytes_received: u64,
|
||||
/// Total requests made
|
||||
pub total_requests: u64,
|
||||
/// Current bandwidth (bytes per second) sent
|
||||
pub current_bytes_sent: u64,
|
||||
/// Current bandwidth (bytes per second) received
|
||||
pub current_bytes_received: u64,
|
||||
/// Recent bandwidth history (last 60 seconds only, for mini chart)
|
||||
pub recent_bandwidth: Vec<BandwidthDataPoint>,
|
||||
}
|
||||
|
||||
/// Traffic statistics for a profile/proxy session
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrafficStats {
|
||||
/// Proxy ID this stats belong to
|
||||
pub proxy_id: String,
|
||||
/// Profile ID (if associated)
|
||||
pub profile_id: Option<String>,
|
||||
/// Session start timestamp
|
||||
pub session_start: u64,
|
||||
/// Last update timestamp
|
||||
pub last_update: u64,
|
||||
/// Total bytes sent across all time
|
||||
pub total_bytes_sent: u64,
|
||||
/// Total bytes received across all time
|
||||
pub total_bytes_received: u64,
|
||||
/// Total requests made
|
||||
pub total_requests: u64,
|
||||
/// Bandwidth data points (time-series, 1 point per second, max 300 = 5 min)
|
||||
#[serde(default)]
|
||||
pub bandwidth_history: Vec<BandwidthDataPoint>,
|
||||
/// Domain access statistics
|
||||
#[serde(default)]
|
||||
pub domains: HashMap<String, DomainAccess>,
|
||||
/// Unique IPs accessed
|
||||
#[serde(default)]
|
||||
pub unique_ips: Vec<String>,
|
||||
}
|
||||
|
||||
impl TrafficStats {
|
||||
pub fn new(proxy_id: String, profile_id: Option<String>) -> Self {
|
||||
let now = current_timestamp();
|
||||
Self {
|
||||
proxy_id,
|
||||
profile_id,
|
||||
session_start: now,
|
||||
last_update: now,
|
||||
total_bytes_sent: 0,
|
||||
total_bytes_received: 0,
|
||||
total_requests: 0,
|
||||
bandwidth_history: Vec::new(),
|
||||
domains: HashMap::new(),
|
||||
unique_ips: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a lightweight snapshot for real-time UI updates
|
||||
pub fn to_snapshot(&self) -> TrafficSnapshot {
|
||||
let now = current_timestamp();
|
||||
let cutoff = now.saturating_sub(60); // Last 60 seconds for mini chart
|
||||
|
||||
// Get current bandwidth from last data point
|
||||
let (current_sent, current_recv) = self
|
||||
.bandwidth_history
|
||||
.last()
|
||||
.filter(|dp| dp.timestamp >= now.saturating_sub(2)) // Within last 2 seconds
|
||||
.map(|dp| (dp.bytes_sent, dp.bytes_received))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
TrafficSnapshot {
|
||||
profile_id: self.profile_id.clone(),
|
||||
session_start: self.session_start,
|
||||
last_update: self.last_update,
|
||||
total_bytes_sent: self.total_bytes_sent,
|
||||
total_bytes_received: self.total_bytes_received,
|
||||
total_requests: self.total_requests,
|
||||
current_bytes_sent: current_sent,
|
||||
current_bytes_received: current_recv,
|
||||
recent_bandwidth: self
|
||||
.bandwidth_history
|
||||
.iter()
|
||||
.filter(|dp| dp.timestamp >= cutoff)
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record bandwidth for current second
|
||||
pub fn record_bandwidth(&mut self, bytes_sent: u64, bytes_received: u64) {
|
||||
let now = current_timestamp();
|
||||
self.last_update = now;
|
||||
self.total_bytes_sent += bytes_sent;
|
||||
self.total_bytes_received += bytes_received;
|
||||
|
||||
// Find or create data point for this second
|
||||
if let Some(last) = self.bandwidth_history.last_mut() {
|
||||
if last.timestamp == now {
|
||||
last.bytes_sent += bytes_sent;
|
||||
last.bytes_received += bytes_received;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new data point
|
||||
self.bandwidth_history.push(BandwidthDataPoint {
|
||||
timestamp: now,
|
||||
bytes_sent,
|
||||
bytes_received,
|
||||
});
|
||||
|
||||
// Keep only last 5 minutes (300 seconds) of data
|
||||
const MAX_HISTORY_SECONDS: usize = 300;
|
||||
if self.bandwidth_history.len() > MAX_HISTORY_SECONDS {
|
||||
self.bandwidth_history.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a request to a domain
|
||||
pub fn record_request(&mut self, domain: &str, bytes_sent: u64, bytes_received: u64) {
|
||||
let now = current_timestamp();
|
||||
self.total_requests += 1;
|
||||
|
||||
let entry = self
|
||||
.domains
|
||||
.entry(domain.to_string())
|
||||
.or_insert(DomainAccess {
|
||||
domain: domain.to_string(),
|
||||
request_count: 0,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
first_access: now,
|
||||
last_access: now,
|
||||
});
|
||||
|
||||
entry.request_count += 1;
|
||||
entry.bytes_sent += bytes_sent;
|
||||
entry.bytes_received += bytes_received;
|
||||
entry.last_access = now;
|
||||
}
|
||||
|
||||
/// Record an IP address access
|
||||
pub fn record_ip(&mut self, ip: &str) {
|
||||
if !self.unique_ips.contains(&ip.to_string()) {
|
||||
self.unique_ips.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bandwidth data for the last N seconds
|
||||
pub fn get_recent_bandwidth(&self, seconds: u64) -> Vec<BandwidthDataPoint> {
|
||||
let now = current_timestamp();
|
||||
let cutoff = now.saturating_sub(seconds);
|
||||
self
|
||||
.bandwidth_history
|
||||
.iter()
|
||||
.filter(|dp| dp.timestamp >= cutoff)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current Unix timestamp in seconds
|
||||
fn current_timestamp() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Get the traffic stats storage directory
|
||||
pub fn get_traffic_stats_dir() -> PathBuf {
|
||||
let base_dirs = BaseDirs::new().expect("Failed to get base directories");
|
||||
let mut path = base_dirs.cache_dir().to_path_buf();
|
||||
path.push(if cfg!(debug_assertions) {
|
||||
"DonutBrowserDev"
|
||||
} else {
|
||||
"DonutBrowser"
|
||||
});
|
||||
path.push("traffic_stats");
|
||||
path
|
||||
}
|
||||
|
||||
/// Save traffic stats to disk
|
||||
pub fn save_traffic_stats(stats: &TrafficStats) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let storage_dir = get_traffic_stats_dir();
|
||||
fs::create_dir_all(&storage_dir)?;
|
||||
|
||||
let file_path = storage_dir.join(format!("{}.json", stats.proxy_id));
|
||||
let content = serde_json::to_string(stats)?;
|
||||
fs::write(&file_path, content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load traffic stats from disk
|
||||
pub fn load_traffic_stats(proxy_id: &str) -> Option<TrafficStats> {
|
||||
let storage_dir = get_traffic_stats_dir();
|
||||
let file_path = storage_dir.join(format!("{proxy_id}.json"));
|
||||
|
||||
if !file_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&file_path).ok()?;
|
||||
serde_json::from_str(&content).ok()
|
||||
}
|
||||
|
||||
/// List all traffic stats files
|
||||
pub fn list_traffic_stats() -> Vec<TrafficStats> {
|
||||
let storage_dir = get_traffic_stats_dir();
|
||||
|
||||
if !storage_dir.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut stats = 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(s) = serde_json::from_str::<TrafficStats>(&content) {
|
||||
stats.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats
|
||||
}
|
||||
|
||||
/// Delete traffic stats for a proxy
|
||||
pub fn delete_traffic_stats(proxy_id: &str) -> bool {
|
||||
let storage_dir = get_traffic_stats_dir();
|
||||
let file_path = storage_dir.join(format!("{proxy_id}.json"));
|
||||
|
||||
if file_path.exists() {
|
||||
fs::remove_file(&file_path).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all traffic stats (used when clearing cache)
|
||||
pub fn clear_all_traffic_stats() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let storage_dir = get_traffic_stats_dir();
|
||||
|
||||
if storage_dir.exists() {
|
||||
for entry in fs::read_dir(&storage_dir)?.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "json") {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Live bandwidth tracker for real-time stats collection in the proxy
|
||||
/// This is designed to be used from within the proxy server
|
||||
pub struct LiveTrafficTracker {
|
||||
pub proxy_id: String,
|
||||
pub profile_id: Option<String>,
|
||||
bytes_sent: AtomicU64,
|
||||
bytes_received: AtomicU64,
|
||||
requests: AtomicU64,
|
||||
domain_stats: RwLock<HashMap<String, (u64, u64, u64)>>, // domain -> (count, sent, recv)
|
||||
ips: RwLock<Vec<String>>,
|
||||
#[allow(dead_code)]
|
||||
session_start: u64,
|
||||
}
|
||||
|
||||
impl LiveTrafficTracker {
|
||||
pub fn new(proxy_id: String, profile_id: Option<String>) -> Self {
|
||||
Self {
|
||||
proxy_id,
|
||||
profile_id,
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
bytes_received: AtomicU64::new(0),
|
||||
requests: AtomicU64::new(0),
|
||||
domain_stats: RwLock::new(HashMap::new()),
|
||||
ips: RwLock::new(Vec::new()),
|
||||
session_start: current_timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_bytes_sent(&self, bytes: u64) {
|
||||
self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn add_bytes_received(&self, bytes: u64) {
|
||||
self.bytes_received.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn record_request(&self, domain: &str, bytes_sent: u64, bytes_received: u64) {
|
||||
self.requests.fetch_add(1, Ordering::Relaxed);
|
||||
// Also update total byte counters for HTTP requests (not tunneled)
|
||||
self.bytes_sent.fetch_add(bytes_sent, Ordering::Relaxed);
|
||||
self
|
||||
.bytes_received
|
||||
.fetch_add(bytes_received, Ordering::Relaxed);
|
||||
if let Ok(mut stats) = self.domain_stats.write() {
|
||||
let entry = stats.entry(domain.to_string()).or_insert((0, 0, 0));
|
||||
entry.0 += 1;
|
||||
entry.1 += bytes_sent;
|
||||
entry.2 += bytes_received;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_ip(&self, ip: &str) {
|
||||
if let Ok(mut ips) = self.ips.write() {
|
||||
if !ips.contains(&ip.to_string()) {
|
||||
ips.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update domain-specific byte counts (called when CONNECT tunnel closes)
|
||||
pub fn update_domain_bytes(&self, domain: &str, bytes_sent: u64, bytes_received: u64) {
|
||||
if let Ok(mut stats) = self.domain_stats.write() {
|
||||
let entry = stats.entry(domain.to_string()).or_insert((0, 0, 0));
|
||||
entry.1 += bytes_sent;
|
||||
entry.2 += bytes_received;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current stats snapshot
|
||||
pub fn get_snapshot(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.bytes_sent.load(Ordering::Relaxed),
|
||||
self.bytes_received.load(Ordering::Relaxed),
|
||||
self.requests.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
|
||||
/// Flush current stats to disk and return the delta
|
||||
pub fn flush_to_disk(&self) -> Result<(u64, u64), Box<dyn std::error::Error>> {
|
||||
let bytes_sent = self.bytes_sent.swap(0, Ordering::Relaxed);
|
||||
let bytes_received = self.bytes_received.swap(0, Ordering::Relaxed);
|
||||
|
||||
// Load or create stats
|
||||
let mut stats = load_traffic_stats(&self.proxy_id)
|
||||
.unwrap_or_else(|| TrafficStats::new(self.proxy_id.clone(), self.profile_id.clone()));
|
||||
|
||||
// Update bandwidth history
|
||||
stats.record_bandwidth(bytes_sent, bytes_received);
|
||||
|
||||
// Update domain stats
|
||||
if let Ok(mut domain_map) = self.domain_stats.write() {
|
||||
for (domain, (count, sent, recv)) in domain_map.drain() {
|
||||
stats.record_request(&domain, sent, recv);
|
||||
// Adjust request count (record_request increments total_requests)
|
||||
stats.total_requests = stats.total_requests.saturating_sub(1) + count;
|
||||
}
|
||||
}
|
||||
|
||||
// Update IPs
|
||||
if let Ok(ips) = self.ips.read() {
|
||||
for ip in ips.iter() {
|
||||
stats.record_ip(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
save_traffic_stats(&stats)?;
|
||||
|
||||
Ok((bytes_sent, bytes_received))
|
||||
}
|
||||
}
|
||||
|
||||
/// Global traffic tracker that can be accessed from connection handlers
|
||||
pub static TRAFFIC_TRACKER: std::sync::OnceLock<Arc<LiveTrafficTracker>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
/// Initialize the global traffic tracker
|
||||
pub fn init_traffic_tracker(proxy_id: String, profile_id: Option<String>) {
|
||||
let _ = TRAFFIC_TRACKER.set(Arc::new(LiveTrafficTracker::new(proxy_id, profile_id)));
|
||||
}
|
||||
|
||||
/// Get the global traffic tracker
|
||||
pub fn get_traffic_tracker() -> Option<Arc<LiveTrafficTracker>> {
|
||||
TRAFFIC_TRACKER.get().cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_traffic_stats_creation() {
|
||||
let stats = TrafficStats::new(
|
||||
"test_proxy".to_string(),
|
||||
Some("test-profile-id".to_string()),
|
||||
);
|
||||
assert_eq!(stats.proxy_id, "test_proxy");
|
||||
assert_eq!(stats.profile_id, Some("test-profile-id".to_string()));
|
||||
assert_eq!(stats.total_bytes_sent, 0);
|
||||
assert_eq!(stats.total_bytes_received, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bandwidth_recording() {
|
||||
let mut stats = TrafficStats::new("test_proxy".to_string(), None);
|
||||
|
||||
stats.record_bandwidth(1000, 2000);
|
||||
assert_eq!(stats.total_bytes_sent, 1000);
|
||||
assert_eq!(stats.total_bytes_received, 2000);
|
||||
assert_eq!(stats.bandwidth_history.len(), 1);
|
||||
|
||||
stats.record_bandwidth(500, 1000);
|
||||
assert_eq!(stats.total_bytes_sent, 1500);
|
||||
assert_eq!(stats.total_bytes_received, 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_recording() {
|
||||
let mut stats = TrafficStats::new("test_proxy".to_string(), None);
|
||||
|
||||
stats.record_request("example.com", 100, 500);
|
||||
stats.record_request("example.com", 200, 1000);
|
||||
stats.record_request("google.com", 50, 200);
|
||||
|
||||
assert_eq!(stats.domains.len(), 2);
|
||||
assert_eq!(stats.domains["example.com"].request_count, 2);
|
||||
assert_eq!(stats.domains["example.com"].bytes_sent, 300);
|
||||
assert_eq!(stats.domains["google.com"].request_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ip_recording() {
|
||||
let mut stats = TrafficStats::new("test_proxy".to_string(), None);
|
||||
|
||||
stats.record_ip("192.168.1.1");
|
||||
stats.record_ip("192.168.1.1"); // Duplicate
|
||||
stats.record_ip("10.0.0.1");
|
||||
|
||||
assert_eq!(stats.unique_ips.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -462,6 +462,137 @@ async fn test_proxy_list() -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test traffic tracking through proxy
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_traffic_tracking() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let binary_path = setup_test().await?;
|
||||
let mut tracker = ProxyTestTracker::new(binary_path.clone());
|
||||
|
||||
println!("Testing traffic tracking through proxy...");
|
||||
|
||||
// Start a proxy
|
||||
let output = TestUtils::execute_command(&binary_path, &["proxy", "start"]).await?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(format!("Failed to start proxy - stdout: {stdout}, stderr: {stderr}").into());
|
||||
}
|
||||
|
||||
let config: Value = serde_json::from_str(&String::from_utf8(output.stdout)?)?;
|
||||
let proxy_id = config["id"].as_str().unwrap().to_string();
|
||||
let local_port = config["localPort"].as_u64().unwrap() as u16;
|
||||
tracker.track_proxy(proxy_id.clone());
|
||||
|
||||
println!("Proxy started on port {}", local_port);
|
||||
|
||||
// Wait for proxy to be ready
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Make an HTTP request through the proxy
|
||||
let mut stream = TcpStream::connect(("127.0.0.1", local_port)).await?;
|
||||
let request =
|
||||
b"GET http://httpbin.org/ip HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n";
|
||||
|
||||
// Track bytes sent
|
||||
let bytes_sent = request.len();
|
||||
stream.write_all(request).await?;
|
||||
|
||||
// Read response
|
||||
let mut response = Vec::new();
|
||||
stream.read_to_end(&mut response).await?;
|
||||
let bytes_received = response.len();
|
||||
|
||||
println!(
|
||||
"HTTP request completed: sent {} bytes, received {} bytes",
|
||||
bytes_sent, bytes_received
|
||||
);
|
||||
|
||||
// Wait for traffic stats to be flushed (happens every second)
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Verify traffic was tracked by checking traffic stats file exists
|
||||
// Note: Traffic stats are stored in the cache directory
|
||||
let cache_dir = directories::BaseDirs::new()
|
||||
.expect("Failed to get base directories")
|
||||
.cache_dir()
|
||||
.to_path_buf();
|
||||
let traffic_stats_dir = cache_dir.join("DonutBrowserDev").join("traffic_stats");
|
||||
let stats_file = traffic_stats_dir.join(format!("{}.json", proxy_id));
|
||||
|
||||
if stats_file.exists() {
|
||||
let content = std::fs::read_to_string(&stats_file)?;
|
||||
let stats: Value = serde_json::from_str(&content)?;
|
||||
|
||||
let total_sent = stats["total_bytes_sent"].as_u64().unwrap_or(0);
|
||||
let total_received = stats["total_bytes_received"].as_u64().unwrap_or(0);
|
||||
let total_requests = stats["total_requests"].as_u64().unwrap_or(0);
|
||||
|
||||
println!(
|
||||
"Traffic stats recorded: sent {} bytes, received {} bytes, {} requests",
|
||||
total_sent, total_received, total_requests
|
||||
);
|
||||
|
||||
// Check if domains are being tracked
|
||||
let mut domain_traffic = false;
|
||||
if let Some(domains) = stats.get("domains") {
|
||||
if let Some(domain_map) = domains.as_object() {
|
||||
println!("Domains tracked: {}", domain_map.len());
|
||||
for (domain, domain_stats) in domain_map {
|
||||
println!(" - {}", domain);
|
||||
// Check if any domain has traffic
|
||||
if let Some(domain_obj) = domain_stats.as_object() {
|
||||
let domain_sent = domain_obj
|
||||
.get("bytes_sent")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
let domain_recv = domain_obj
|
||||
.get("bytes_received")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
let domain_reqs = domain_obj
|
||||
.get("request_count")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
" sent: {}, received: {}, requests: {}",
|
||||
domain_sent, domain_recv, domain_reqs
|
||||
);
|
||||
if domain_sent > 0 || domain_recv > 0 || domain_reqs > 0 {
|
||||
domain_traffic = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that some traffic was recorded - check either total bytes or domain traffic
|
||||
assert!(
|
||||
total_sent > 0 || total_received > 0 || total_requests > 0 || domain_traffic,
|
||||
"Traffic stats should record some activity (sent: {}, received: {}, requests: {})",
|
||||
total_sent,
|
||||
total_received,
|
||||
total_requests
|
||||
);
|
||||
|
||||
println!("Traffic tracking test passed!");
|
||||
} else {
|
||||
println!("Warning: Traffic stats file not found at {:?}", stats_file);
|
||||
// This is not necessarily a failure - the file may not have been created yet
|
||||
// The important thing is that the proxy is working
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
tracker.cleanup_all().await;
|
||||
|
||||
// Clean up the traffic stats file
|
||||
if stats_file.exists() {
|
||||
let _ = std::fs::remove_file(&stats_file);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test proxy stop
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
||||
Reference in New Issue
Block a user