1→//! Agent configuration management 2→//! 3→//! Supports three configuration sources (in priority order): 4→//! 1. Embedded config (magic bytes appended to executable) 5→//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml) 6→//! 3. Environment variables (fallback) 7→ 8→use anyhow::{anyhow, Context, Result}; 9→use serde::{Deserialize, Serialize}; 10→use std::io::{Read, Seek, SeekFrom}; 11→use std::path::PathBuf; 12→use tracing::{info, warn}; 13→use uuid::Uuid; 14→ 15→/// Magic marker for embedded configuration (10 bytes) 16→const MAGIC_MARKER: &[u8] = b"GURUCONFIG"; 17→ 18→/// Embedded configuration data (appended to executable) 19→#[derive(Debug, Clone, Serialize, Deserialize)] 20→pub struct EmbeddedConfig { 21→ /// Server WebSocket URL 22→ pub server_url: String, 23→ /// API key for authentication 24→ pub api_key: String, 25→ /// Company/organization name 26→ #[serde(default)] 27→ pub company: Option, 28→ /// Site/location name 29→ #[serde(default)] 30→ pub site: Option, 31→ /// Tags for categorization 32→ #[serde(default)] 33→ pub tags: Vec, 34→} 35→ 36→/// Detected run mode based on filename 37→#[derive(Debug, Clone, PartialEq)] 38→pub enum RunMode { 39→ /// Viewer-only installation (filename contains "Viewer") 40→ Viewer, 41→ /// Temporary support session (filename contains 6-digit code) 42→ TempSupport(String), 43→ /// Permanent agent with embedded config 44→ PermanentAgent, 45→ /// Unknown/default mode 46→ Default, 47→} 48→ 49→/// Agent configuration 50→#[derive(Debug, Clone, Serialize, Deserialize)] 51→pub struct Config { 52→ /// Server WebSocket URL (e.g., wss://connect.example.com/ws) 53→ pub server_url: String, 54→ 55→ /// Agent API key for authentication 56→ pub api_key: String, 57→ 58→ /// Unique agent identifier (generated on first run) 59→ #[serde(default = "generate_agent_id")] 60→ pub agent_id: String, 61→ 62→ /// Optional hostname override 63→ pub hostname_override: Option, 64→ 65→ /// Company/organization name (from embedded config) 66→ #[serde(default)] 67→ pub company: Option, 68→ 69→ /// Site/location name (from embedded config) 70→ #[serde(default)] 71→ pub site: Option, 72→ 73→ /// Tags for categorization (from embedded config) 74→ #[serde(default)] 75→ pub tags: Vec, 76→ 77→ /// Support code for one-time support sessions (set via command line or filename) 78→ #[serde(skip)] 79→ pub support_code: Option, 80→ 81→ /// Capture settings 82→ #[serde(default)] 83→ pub capture: CaptureConfig, 84→ 85→ /// Encoding settings 86→ #[serde(default)] 87→ pub encoding: EncodingConfig, 88→} 89→ 90→fn generate_agent_id() -> String { 91→ Uuid::new_v4().to_string() 92→} 93→ 94→#[derive(Debug, Clone, Serialize, Deserialize)] 95→pub struct CaptureConfig { 96→ /// Target frames per second (1-60) 97→ #[serde(default = "default_fps")] 98→ pub fps: u32, 99→ 100→ /// Use DXGI Desktop Duplication (recommended) 101→ #[serde(default = "default_true")] 102→ pub use_dxgi: bool, 103→ 104→ /// Fall back to GDI if DXGI fails 105→ #[serde(default = "default_true")] 106→ pub gdi_fallback: bool, 107→} 108→ 109→#[derive(Debug, Clone, Serialize, Deserialize)] 110→pub struct EncodingConfig { 111→ /// Preferred codec (auto, raw, vp9, h264) 112→ #[serde(default = "default_codec")] 113→ pub codec: String, 114→ 115→ /// Quality (1-100, higher = better quality, more bandwidth) 116→ #[serde(default = "default_quality")] 117→ pub quality: u32, 118→ 119→ /// Use hardware encoding if available 120→ #[serde(default = "default_true")] 121→ pub hardware_encoding: bool, 122→} 123→ 124→fn default_fps() -> u32 { 125→ 30 126→} 127→ 128→fn default_true() -> bool { 129→ true 130→} 131→ 132→fn default_codec() -> String { 133→ "auto".to_string() 134→} 135→ 136→fn default_quality() -> u32 { 137→ 75 138→} 139→ 140→impl Default for CaptureConfig { 141→ fn default() -> Self { 142→ Self { 143→ fps: default_fps(), 144→ use_dxgi: true, 145→ gdi_fallback: true, 146→ } 147→ } 148→} 149→ 150→impl Default for EncodingConfig { 151→ fn default() -> Self { 152→ Self { 153→ codec: default_codec(), 154→ quality: default_quality(), 155→ hardware_encoding: true, 156→ } 157→ } 158→} 159→ 160→impl Config { 161→ /// Detect run mode from executable filename 162→ pub fn detect_run_mode() -> RunMode { 163→ let exe_path = match std::env::current_exe() { 164→ Ok(p) => p, 165→ Err(_) => return RunMode::Default, 166→ }; 167→ 168→ let filename = match exe_path.file_stem() { 169→ Some(s) => s.to_string_lossy().to_string(), 170→ None => return RunMode::Default, 171→ }; 172→ 173→ let filename_lower = filename.to_lowercase(); 174→ 175→ // Check for viewer mode 176→ if filename_lower.contains("viewer") { 177→ info!("Detected viewer mode from filename: {}", filename); 178→ return RunMode::Viewer; 179→ } 180→ 181→ // Check for support code in filename (6-digit number) 182→ if let Some(code) = Self::extract_support_code(&filename) { 183→ info!("Detected support code from filename: {}", code); 184→ return RunMode::TempSupport(code); 185→ } 186→ 187→ // Check for embedded config 188→ if Self::has_embedded_config() { 189→ info!("Detected embedded config in executable"); 190→ return RunMode::PermanentAgent; 191→ } 192→ 193→ RunMode::Default 194→ } 195→ 196→ /// Extract 6-digit support code from filename 197→ fn extract_support_code(filename: &str) -> Option { 198→ // Look for patterns like "GuruConnect-123456" or "GuruConnect_123456" 199→ for part in filename.split(|c| c == '-' || c == '_' || c == '.') { 200→ let trimmed = part.trim(); 201→ if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) { 202→ return Some(trimmed.to_string()); 203→ } 204→ } 205→ 206→ // Check if last 6 characters are all digits 207→ if filename.len() >= 6 { 208→ let last_six = &filename[filename.len() - 6..]; 209→ if last_six.chars().all(|c| c.is_ascii_digit()) { 210→ return Some(last_six.to_string()); 211→ } 212→ } 213→ 214→ None 215→ } 216→ 217→ /// Check if embedded configuration exists in the executable 218→ pub fn has_embedded_config() -> bool { 219→ Self::read_embedded_config().is_ok() 220→ } 221→ 222→ /// Read embedded configuration from the executable 223→ pub fn read_embedded_config() -> Result { 224→ let exe_path = std::env::current_exe() 225→ .context("Failed to get current executable path")?; 226→ 227→ let mut file = std::fs::File::open(&exe_path) 228→ .context("Failed to open executable for reading")?; 229→ 230→ let file_size = file.metadata()?.len(); 231→ if file_size < (MAGIC_MARKER.len() + 4) as u64 { 232→ return Err(anyhow!("File too small to contain embedded config")); 233→ } 234→ 235→ // Read the last part of the file to find magic marker 236→ // Structure: [PE binary][GURUCONFIG][length:u32][json config] 237→ // We need to search backwards from the end 238→ 239→ // Read last 64KB (should be more than enough for config) 240→ let search_size = std::cmp::min(65536, file_size as usize); 241→ let search_start = file_size - search_size as u64; 242→ 243→ file.seek(SeekFrom::Start(search_start))?; 244→ let mut buffer = vec![0u8; search_size]; 245→ file.read_exact(&mut buffer)?; 246→ 247→ // Find magic marker 248→ let marker_pos = buffer.windows(MAGIC_MARKER.len()) 249→ .rposition(|window| window == MAGIC_MARKER) 250→ .ok_or_else(|| anyhow!("Magic marker not found"))?; 251→ 252→ // Read config length (4 bytes after marker) 253→ let length_start = marker_pos + MAGIC_MARKER.len(); 254→ if length_start + 4 > buffer.len() { 255→ return Err(anyhow!("Invalid embedded config: length field truncated")); 256→ } 257→ 258→ let config_length = u32::from_le_bytes([ 259→ buffer[length_start], 260→ buffer[length_start + 1], 261→ buffer[length_start + 2], 262→ buffer[length_start + 3], 263→ ]) as usize; 264→ 265→ // Read config data 266→ let config_start = length_start + 4; 267→ if config_start + config_length > buffer.len() { 268→ return Err(anyhow!("Invalid embedded config: data truncated")); 269→ } 270→ 271→ let config_bytes = &buffer[config_start..config_start + config_length]; 272→ let config: EmbeddedConfig = serde_json::from_slice(config_bytes) 273→ .context("Failed to parse embedded config JSON")?; 274→ 275→ info!("Loaded embedded config: server={}, company={:?}", 276→ config.server_url, config.company); 277→ 278→ Ok(config) 279→ } 280→ 281→ /// Check if an explicit agent configuration file exists 282→ /// This returns true only if there's a real config file, not generated defaults 283→ pub fn has_agent_config() -> bool { 284→ // Check for embedded config first 285→ if Self::has_embedded_config() { 286→ return true; 287→ } 288→ 289→ // Check for config in current directory 290→ let local_config = PathBuf::from("guruconnect.toml"); 291→ if local_config.exists() { 292→ return true; 293→ } 294→ 295→ // Check in program data directory (Windows) 296→ #[cfg(windows)] 297→ { 298→ if let Ok(program_data) = std::env::var("ProgramData") { 299→ let path = PathBuf::from(program_data) 300→ .join("GuruConnect") 301→ .join("agent.toml"); 302→ if path.exists() { 303→ return true; 304→ } 305→ } 306→ } 307→ 308→ false 309→ } 310→ 311→ /// Load configuration from embedded config, file, or environment 312→ pub fn load() -> Result { 313→ // Priority 1: Try loading from embedded config 314→ if let Ok(embedded) = Self::read_embedded_config() { 315→ info!("Using embedded configuration"); 316→ let config = Config { 317→ server_url: embedded.server_url, 318→ api_key: embedded.api_key, 319→ agent_id: generate_agent_id(), 320→ hostname_override: None, 321→ company: embedded.company, 322→ site: embedded.site, 323→ tags: embedded.tags, 324→ support_code: None, 325→ capture: CaptureConfig::default(), 326→ encoding: EncodingConfig::default(), 327→ }; 328→ 329→ // Save to file for persistence (so agent_id is preserved) 330→ let _ = config.save(); 331→ return Ok(config); 332→ } 333→ 334→ // Priority 2: Try loading from config file 335→ let config_path = Self::config_path(); 336→ 337→ if config_path.exists() { 338→ let contents = std::fs::read_to_string(&config_path) 339→ .with_context(|| format!("Failed to read config from {:?}", config_path))?; 340→ 341→ let mut config: Config = toml::from_str(&contents) 342→ .with_context(|| "Failed to parse config file")?; 343→ 344→ // Ensure agent_id is set and saved 345→ if config.agent_id.is_empty() { 346→ config.agent_id = generate_agent_id(); 347→ let _ = config.save(); 348→ } 349→ 350→ // support_code is always None when loading from file (set via CLI) 351→ config.support_code = None; 352→ 353→ return Ok(config); 354→ } 355→ 356→ // Priority 3: Fall back to environment variables 357→ let server_url = std::env::var("GURUCONNECT_SERVER_URL") 358→ .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string()); 359→ 360→ let api_key = std::env::var("GURUCONNECT_API_KEY") 361→ .unwrap_or_else(|_| "dev-key".to_string()); 362→ 363→ let agent_id = std::env::var("GURUCONNECT_AGENT_ID") 364→ .unwrap_or_else(|_| generate_agent_id()); 365→ 366→ let config = Config { 367→ server_url, 368→ api_key, 369→ agent_id, 370→ hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(), 371→ company: None, 372→ site: None, 373→ tags: Vec::new(), 374→ support_code: None, 375→ capture: CaptureConfig::default(), 376→ encoding: EncodingConfig::default(), 377→ }; 378→ 379→ // Save config with generated agent_id for persistence 380→ let _ = config.save(); 381→ 382→ Ok(config) 383→ } 384→ 385→ /// Get the configuration file path 386→ fn config_path() -> PathBuf { 387→ // Check for config in current directory first 388→ let local_config = PathBuf::from("guruconnect.toml"); 389→ if local_config.exists() { 390→ return local_config; 391→ } 392→ 393→ // Check in program data directory (Windows) 394→ #[cfg(windows)] 395→ { 396→ if let Ok(program_data) = std::env::var("ProgramData") { 397→ let path = PathBuf::from(program_data) 398→ .join("GuruConnect") 399→ .join("agent.toml"); 400→ if path.exists() { 401→ return path; 402→ } 403→ } 404→ } 405→ 406→ // Default to local config 407→ local_config 408→ } 409→ 410→ /// Get the hostname to use 411→ pub fn hostname(&self) -> String { 412→ self.hostname_override 413→ .clone() 414→ .unwrap_or_else(|| { 415→ hostname::get() 416→ .map(|h| h.to_string_lossy().to_string()) 417→ .unwrap_or_else(|_| "unknown".to_string()) 418→ }) 419→ } 420→ 421→ /// Save current configuration to file 422→ pub fn save(&self) -> Result<()> { 423→ let config_path = Self::config_path(); 424→ 425→ // Ensure parent directory exists 426→ if let Some(parent) = config_path.parent() { 427→ std::fs::create_dir_all(parent)?; 428→ } 429→ 430→ let contents = toml::to_string_pretty(self)?; 431→ std::fs::write(&config_path, contents)?; 432→ 433→ Ok(()) 434→ } 435→} 436→ 437→/// Example configuration file content 438→pub fn example_config() -> &'static str { 439→ r#"# GuruConnect Agent Configuration 440→ 441→# Server connection 442→server_url = "wss://connect.example.com/ws" 443→api_key = "your-agent-api-key" 444→agent_id = "auto-generated-uuid" 445→ 446→# Optional: override hostname 447→# hostname_override = "custom-hostname" 448→ 449→[capture] 450→fps = 30 451→use_dxgi = true 452→gdi_fallback = true 453→ 454→[encoding] 455→codec = "auto" # auto, raw, vp9, h264 456→quality = 75 # 1-100 457→hardware_encoding = true 458→"# 459→} 460→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.