//! Agent configuration management //! //! Supports three configuration sources (in priority order): //! 1. Embedded config (magic bytes appended to executable) //! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml) //! 3. Environment variables (fallback) use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use std::io::{Read, Seek, SeekFrom}; use std::path::PathBuf; use tracing::{info, warn}; use uuid::Uuid; /// Magic marker for embedded configuration (10 bytes) const MAGIC_MARKER: &[u8] = b"GURUCONFIG"; /// Embedded configuration data (appended to executable) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbeddedConfig { /// Server WebSocket URL pub server_url: String, /// API key for authentication pub api_key: String, /// Company/organization name #[serde(default)] pub company: Option, /// Site/location name #[serde(default)] pub site: Option, /// Tags for categorization #[serde(default)] pub tags: Vec, } /// Detected run mode based on filename #[derive(Debug, Clone, PartialEq)] pub enum RunMode { /// Viewer-only installation (filename contains "Viewer") Viewer, /// Temporary support session (filename contains 6-digit code) TempSupport(String), /// Permanent agent with embedded config PermanentAgent, /// Unknown/default mode Default, } /// Agent configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// Server WebSocket URL (e.g., wss://connect.example.com/ws) pub server_url: String, /// Agent API key for authentication pub api_key: String, /// Unique agent identifier (generated on first run) #[serde(default = "generate_agent_id")] pub agent_id: String, /// Optional hostname override pub hostname_override: Option, /// Company/organization name (from embedded config) #[serde(default)] pub company: Option, /// Site/location name (from embedded config) #[serde(default)] pub site: Option, /// Tags for categorization (from embedded config) #[serde(default)] pub tags: Vec, /// Support code for one-time support sessions (set via command line or filename) #[serde(skip)] pub support_code: Option, /// Capture settings #[serde(default)] pub capture: CaptureConfig, /// Encoding settings #[serde(default)] pub encoding: EncodingConfig, } fn generate_agent_id() -> String { Uuid::new_v4().to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CaptureConfig { /// Target frames per second (1-60) #[serde(default = "default_fps")] pub fps: u32, /// Use DXGI Desktop Duplication (recommended) #[serde(default = "default_true")] pub use_dxgi: bool, /// Fall back to GDI if DXGI fails #[serde(default = "default_true")] pub gdi_fallback: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EncodingConfig { /// Preferred codec (auto, raw, vp9, h264) #[serde(default = "default_codec")] pub codec: String, /// Quality (1-100, higher = better quality, more bandwidth) #[serde(default = "default_quality")] pub quality: u32, /// Use hardware encoding if available #[serde(default = "default_true")] pub hardware_encoding: bool, } fn default_fps() -> u32 { 30 } fn default_true() -> bool { true } fn default_codec() -> String { "auto".to_string() } fn default_quality() -> u32 { 75 } impl Default for CaptureConfig { fn default() -> Self { Self { fps: default_fps(), use_dxgi: true, gdi_fallback: true, } } } impl Default for EncodingConfig { fn default() -> Self { Self { codec: default_codec(), quality: default_quality(), hardware_encoding: true, } } } impl Config { /// Detect run mode from executable filename pub fn detect_run_mode() -> RunMode { let exe_path = match std::env::current_exe() { Ok(p) => p, Err(_) => return RunMode::Default, }; let filename = match exe_path.file_stem() { Some(s) => s.to_string_lossy().to_string(), None => return RunMode::Default, }; let filename_lower = filename.to_lowercase(); // Check for viewer mode if filename_lower.contains("viewer") { info!("Detected viewer mode from filename: {}", filename); return RunMode::Viewer; } // Check for support code in filename (6-digit number) if let Some(code) = Self::extract_support_code(&filename) { info!("Detected support code from filename: {}", code); return RunMode::TempSupport(code); } // Check for embedded config if Self::has_embedded_config() { info!("Detected embedded config in executable"); return RunMode::PermanentAgent; } RunMode::Default } /// Extract 6-digit support code from filename fn extract_support_code(filename: &str) -> Option { // Look for patterns like "GuruConnect-123456" or "GuruConnect_123456" for part in filename.split(|c| c == '-' || c == '_' || c == '.') { let trimmed = part.trim(); if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) { return Some(trimmed.to_string()); } } // Check if last 6 characters are all digits if filename.len() >= 6 { let last_six = &filename[filename.len() - 6..]; if last_six.chars().all(|c| c.is_ascii_digit()) { return Some(last_six.to_string()); } } None } /// Check if embedded configuration exists in the executable pub fn has_embedded_config() -> bool { Self::read_embedded_config().is_ok() } /// Read embedded configuration from the executable pub fn read_embedded_config() -> Result { let exe_path = std::env::current_exe() .context("Failed to get current executable path")?; let mut file = std::fs::File::open(&exe_path) .context("Failed to open executable for reading")?; let file_size = file.metadata()?.len(); if file_size < (MAGIC_MARKER.len() + 4) as u64 { return Err(anyhow!("File too small to contain embedded config")); } // Read the last part of the file to find magic marker // Structure: [PE binary][GURUCONFIG][length:u32][json config] // We need to search backwards from the end // Read last 64KB (should be more than enough for config) let search_size = std::cmp::min(65536, file_size as usize); let search_start = file_size - search_size as u64; file.seek(SeekFrom::Start(search_start))?; let mut buffer = vec![0u8; search_size]; file.read_exact(&mut buffer)?; // Find magic marker let marker_pos = buffer.windows(MAGIC_MARKER.len()) .rposition(|window| window == MAGIC_MARKER) .ok_or_else(|| anyhow!("Magic marker not found"))?; // Read config length (4 bytes after marker) let length_start = marker_pos + MAGIC_MARKER.len(); if length_start + 4 > buffer.len() { return Err(anyhow!("Invalid embedded config: length field truncated")); } let config_length = u32::from_le_bytes([ buffer[length_start], buffer[length_start + 1], buffer[length_start + 2], buffer[length_start + 3], ]) as usize; // Read config data let config_start = length_start + 4; if config_start + config_length > buffer.len() { return Err(anyhow!("Invalid embedded config: data truncated")); } let config_bytes = &buffer[config_start..config_start + config_length]; let config: EmbeddedConfig = serde_json::from_slice(config_bytes) .context("Failed to parse embedded config JSON")?; info!("Loaded embedded config: server={}, company={:?}", config.server_url, config.company); Ok(config) } /// Check if an explicit agent configuration file exists /// This returns true only if there's a real config file, not generated defaults pub fn has_agent_config() -> bool { // Check for embedded config first if Self::has_embedded_config() { return true; } // Check for config in current directory let local_config = PathBuf::from("guruconnect.toml"); if local_config.exists() { return true; } // Check in program data directory (Windows) #[cfg(windows)] { if let Ok(program_data) = std::env::var("ProgramData") { let path = PathBuf::from(program_data) .join("GuruConnect") .join("agent.toml"); if path.exists() { return true; } } } false } /// Load configuration from embedded config, file, or environment pub fn load() -> Result { // Priority 1: Try loading from embedded config if let Ok(embedded) = Self::read_embedded_config() { info!("Using embedded configuration"); let config = Config { server_url: embedded.server_url, api_key: embedded.api_key, agent_id: generate_agent_id(), hostname_override: None, company: embedded.company, site: embedded.site, tags: embedded.tags, support_code: None, capture: CaptureConfig::default(), encoding: EncodingConfig::default(), }; // Save to file for persistence (so agent_id is preserved) let _ = config.save(); return Ok(config); } // Priority 2: Try loading from config file let config_path = Self::config_path(); if config_path.exists() { let contents = std::fs::read_to_string(&config_path) .with_context(|| format!("Failed to read config from {:?}", config_path))?; let mut config: Config = toml::from_str(&contents) .with_context(|| "Failed to parse config file")?; // Ensure agent_id is set and saved if config.agent_id.is_empty() { config.agent_id = generate_agent_id(); let _ = config.save(); } // support_code is always None when loading from file (set via CLI) config.support_code = None; return Ok(config); } // Priority 3: Fall back to environment variables let server_url = std::env::var("GURUCONNECT_SERVER_URL") .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string()); let api_key = std::env::var("GURUCONNECT_API_KEY") .unwrap_or_else(|_| "dev-key".to_string()); let agent_id = std::env::var("GURUCONNECT_AGENT_ID") .unwrap_or_else(|_| generate_agent_id()); let config = Config { server_url, api_key, agent_id, hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(), company: None, site: None, tags: Vec::new(), support_code: None, capture: CaptureConfig::default(), encoding: EncodingConfig::default(), }; // Save config with generated agent_id for persistence let _ = config.save(); Ok(config) } /// Get the configuration file path fn config_path() -> PathBuf { // Check for config in current directory first let local_config = PathBuf::from("guruconnect.toml"); if local_config.exists() { return local_config; } // Check in program data directory (Windows) #[cfg(windows)] { if let Ok(program_data) = std::env::var("ProgramData") { let path = PathBuf::from(program_data) .join("GuruConnect") .join("agent.toml"); if path.exists() { return path; } } } // Default to local config local_config } /// Get the hostname to use pub fn hostname(&self) -> String { self.hostname_override .clone() .unwrap_or_else(|| { hostname::get() .map(|h| h.to_string_lossy().to_string()) .unwrap_or_else(|_| "unknown".to_string()) }) } /// Save current configuration to file pub fn save(&self) -> Result<()> { let config_path = Self::config_path(); // Ensure parent directory exists if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent)?; } let contents = toml::to_string_pretty(self)?; std::fs::write(&config_path, contents)?; Ok(()) } } /// Example configuration file content pub fn example_config() -> &'static str { r#"# GuruConnect Agent Configuration # Server connection server_url = "wss://connect.example.com/ws" api_key = "your-agent-api-key" agent_id = "auto-generated-uuid" # Optional: override hostname # hostname_override = "custom-hostname" [capture] fps = 30 use_dxgi = true gdi_fallback = true [encoding] codec = "auto" # auto, raw, vp9, h264 quality = 75 # 1-100 hardware_encoding = true "# }