diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5eed938 --- /dev/null +++ b/TODO.md @@ -0,0 +1,230 @@ +# GuruConnect Feature Tracking + +## Status Legend +- [ ] Not started +- [~] In progress +- [x] Complete + +--- + +## Phase 1: Core MVP + +### Infrastructure +- [x] WebSocket relay server (Axum) +- [x] Agent WebSocket client +- [x] Protobuf message protocol +- [x] Agent authentication (agent_id, api_key) +- [x] Session management (create, join, leave) +- [x] Systemd service deployment +- [x] NPM proxy (connect.azcomputerguru.com) + +### Support Codes +- [x] Generate 6-digit codes +- [x] Code validation API +- [x] Code status tracking (pending, connected, completed, cancelled) +- [~] Link support codes to agent sessions +- [ ] Code expiration (auto-expire after X minutes) +- [ ] Support code in agent download URL + +### Dashboard +- [x] Technician login page +- [x] Support tab with code generation +- [x] Access tab with connected agents +- [ ] Session detail panel with tabs +- [ ] Screenshot thumbnails +- [ ] Join/Connect button + +### Agent (Windows) +- [x] DXGI screen capture +- [x] GDI fallback capture +- [x] WebSocket connection +- [x] Config persistence (agent_id) +- [ ] Support code parameter +- [ ] Hostname/machine info reporting +- [ ] Screenshot-only mode (for thumbnails) + +--- + +## Phase 2: Remote Control + +### Screen Viewing +- [ ] Web-based viewer (canvas) +- [ ] Raw frame decoding +- [ ] Dirty rectangle optimization +- [ ] Frame rate adaptation + +### Input Control +- [x] Mouse event handling (agent) +- [x] Keyboard event handling (agent) +- [ ] Input relay through server +- [ ] Multi-monitor support + +### Encoding +- [ ] VP9 software encoding +- [ ] H.264 hardware encoding (NVENC/QSV) +- [ ] Adaptive quality based on bandwidth + +--- + +## Phase 3: Backstage Tools (like ScreenConnect) + +### Device Information +- [ ] OS version, hostname, domain +- [ ] Logged-in user +- [ ] Public/private IP addresses +- [ ] MAC address +- [ ] CPU, RAM, disk info +- [ ] Uptime + +### Toolbox APIs +- [ ] Process list (name, PID, memory) +- [ ] Installed software list +- [ ] Windows services list +- [ ] Event log viewer +- [ ] Registry browser + +### Remote Commands +- [ ] Run shell commands +- [ ] PowerShell execution +- [ ] Command output streaming +- [ ] Command history per session + +### Chat/Messaging +- [ ] Technician → Client messages +- [ ] Client → Technician messages +- [ ] Message history + +### File Transfer +- [ ] Upload files to remote +- [ ] Download files from remote +- [ ] Progress tracking +- [ ] Folder browsing + +--- + +## Phase 4: Session Management + +### Timeline/History +- [ ] Connection events +- [ ] Session duration tracking +- [ ] Guest connection history +- [ ] Activity log + +### Session Recording +- [ ] Record session video +- [ ] Playback interface +- [ ] Storage management + +### Notes +- [ ] Per-session notes +- [ ] Session tagging + +--- + +## Phase 5: Access Mode (Unattended) + +### Persistent Agent +- [ ] Windows service installation +- [ ] Auto-start on boot +- [ ] Silent/background mode +- [ ] Automatic reconnection + +### Machine Groups +- [ ] Company/client organization +- [ ] Site/location grouping +- [ ] Custom tags +- [ ] Filtering/search + +### Installer Builder +- [ ] Customized agent builds +- [ ] Pre-configured company/site +- [ ] Silent install options +- [ ] MSI packaging + +--- + +## Phase 6: Security & Authentication + +### Technician Auth +- [ ] User accounts +- [ ] Password hashing +- [ ] JWT tokens +- [ ] Session management + +### MFA +- [ ] TOTP (Google Authenticator) +- [ ] Email verification + +### Audit Logging +- [ ] Login attempts +- [ ] Session access +- [ ] Command execution +- [ ] File transfers + +### Permissions +- [ ] Role-based access +- [ ] Per-client permissions +- [ ] Feature restrictions + +--- + +## Phase 7: Integrations + +### PSA Integration +- [ ] HaloPSA +- [ ] Autotask +- [ ] ConnectWise + +### GuruRMM Integration +- [ ] Dashboard embedding +- [ ] Single sign-on +- [ ] Asset linking + +--- + +## Phase 8: Polish + +### Branding +- [ ] White-label support +- [ ] Custom logos +- [ ] Custom colors + +### Mobile Support +- [ ] Responsive viewer +- [ ] Touch input handling + +### Annotations +- [ ] Draw on screen +- [ ] Pointer highlighting +- [ ] Screenshot annotations + +--- + +## Current Sprint + +### In Progress +1. Link support codes to agent sessions +2. Show connected status in dashboard + +### Next Up +1. Support code in agent download/config +2. Device info reporting from agent +3. Screenshot thumbnails + +--- + +## Notes + +### ScreenConnect Feature Reference (from screenshots) +- Support session list with idle times and connection bars +- Detail panel with tabbed interface: + - Join/Screen (thumbnail, Join button) + - Info (device details) + - Timeline (connection history) + - Chat (messaging) + - Commands (shell execution) + - Notes + - Toolbox (processes, software, events, services) + - File transfer + - Logs + - Settings diff --git a/agent/Cargo.toml b/agent/Cargo.toml index eb05149..4a510cb 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -48,6 +48,9 @@ chrono = { version = "0.4", features = ["serde"] } # Hostname hostname = "0.4" +# URL encoding +urlencoding = "2" + [target.'cfg(windows)'.dependencies] # Windows APIs for screen capture and input windows = { version = "0.58", features = [ diff --git a/agent/src/config.rs b/agent/src/config.rs index 50a0671..375a562 100644 --- a/agent/src/config.rs +++ b/agent/src/config.rs @@ -21,6 +21,10 @@ pub struct Config { /// Optional hostname override pub hostname_override: Option, + /// Support code for one-time support sessions (set via command line) + #[serde(skip)] + pub support_code: Option, + /// Capture settings #[serde(default)] pub capture: CaptureConfig, @@ -119,6 +123,9 @@ impl Config { let _ = config.save(); } + // support_code is always None when loading from file (set via CLI) + config.support_code = None; + return Ok(config); } @@ -137,6 +144,7 @@ impl Config { api_key, agent_id, hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(), + support_code: None, // Set via CLI capture: CaptureConfig::default(), encoding: EncodingConfig::default(), }; diff --git a/agent/src/main.rs b/agent/src/main.rs index c7e321b..681df21 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -1,6 +1,12 @@ //! GuruConnect Agent - Remote Desktop Agent for Windows //! //! Provides screen capture, input injection, and remote control capabilities. +//! +//! Usage: +//! guruconnect-agent.exe [support_code] +//! +//! If a support code is provided, the agent will connect using that code +//! for a one-time support session. mod capture; mod config; @@ -28,8 +34,25 @@ async fn main() -> Result<()> { info!("GuruConnect Agent v{}", env!("CARGO_PKG_VERSION")); + // Parse command line arguments + let args: Vec = std::env::args().collect(); + let support_code = if args.len() > 1 { + let code = args[1].trim().to_string(); + // Validate it looks like a 6-digit code + if code.len() == 6 && code.chars().all(|c| c.is_ascii_digit()) { + info!("Support code provided: {}", code); + Some(code) + } else { + info!("Invalid support code format, ignoring: {}", code); + None + } + } else { + None + }; + // Load configuration - let config = config::Config::load()?; + let mut config = config::Config::load()?; + config.support_code = support_code; info!("Loaded configuration for server: {}", config.server_url); // Run the agent diff --git a/agent/src/session/mod.rs b/agent/src/session/mod.rs index 1c87eae..1c3daa1 100644 --- a/agent/src/session/mod.rs +++ b/agent/src/session/mod.rs @@ -46,10 +46,13 @@ impl SessionManager { pub async fn connect(&mut self) -> Result<()> { self.state = SessionState::Connecting; + let hostname = self.config.hostname(); let transport = WebSocketTransport::connect( &self.config.server_url, &self.config.agent_id, &self.config.api_key, + Some(&hostname), + self.config.support_code.as_deref(), ).await?; self.transport = Some(transport); diff --git a/agent/src/transport/websocket.rs b/agent/src/transport/websocket.rs index 2fa675c..c4d5bbe 100644 --- a/agent/src/transport/websocket.rs +++ b/agent/src/transport/websocket.rs @@ -29,15 +29,35 @@ pub struct WebSocketTransport { impl WebSocketTransport { /// Connect to the server - pub async fn connect(url: &str, agent_id: &str, api_key: &str) -> Result { - // Append agent_id and API key as query parameters + pub async fn connect( + url: &str, + agent_id: &str, + api_key: &str, + hostname: Option<&str>, + support_code: Option<&str>, + ) -> Result { + // Build query parameters + let mut params = format!("agent_id={}&api_key={}", agent_id, api_key); + + if let Some(hostname) = hostname { + params.push_str(&format!("&hostname={}", urlencoding::encode(hostname))); + } + + if let Some(code) = support_code { + params.push_str(&format!("&support_code={}", code)); + } + + // Append parameters to URL let url_with_params = if url.contains('?') { - format!("{}&agent_id={}&api_key={}", url, agent_id, api_key) + format!("{}&{}", url, params) } else { - format!("{}?agent_id={}&api_key={}", url, agent_id, api_key) + format!("{}?{}", url, params) }; tracing::info!("Connecting to {} as agent {}", url, agent_id); + if let Some(code) = support_code { + tracing::info!("Using support code: {}", code); + } let (ws_stream, response) = connect_async(&url_with_params) .await diff --git a/server/src/relay/mod.rs b/server/src/relay/mod.rs index f895dff..da5e94f 100644 --- a/server/src/relay/mod.rs +++ b/server/src/relay/mod.rs @@ -24,6 +24,10 @@ pub struct AgentParams { agent_id: String, #[serde(default)] agent_name: Option, + #[serde(default)] + support_code: Option, + #[serde(default)] + hostname: Option, } #[derive(Debug, Deserialize)] @@ -38,10 +42,12 @@ pub async fn agent_ws_handler( Query(params): Query, ) -> impl IntoResponse { let agent_id = params.agent_id; - let agent_name = params.agent_name.unwrap_or_else(|| agent_id.clone()); + let agent_name = params.hostname.or(params.agent_name).unwrap_or_else(|| agent_id.clone()); + let support_code = params.support_code; let sessions = state.sessions.clone(); + let support_codes = state.support_codes.clone(); - ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, agent_id, agent_name)) + ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, agent_id, agent_name, support_code)) } /// WebSocket handler for viewer connections @@ -60,8 +66,10 @@ pub async fn viewer_ws_handler( async fn handle_agent_connection( socket: WebSocket, sessions: SessionManager, + support_codes: crate::support_codes::SupportCodeManager, agent_id: String, agent_name: String, + support_code: Option, ) { info!("Agent connected: {} ({})", agent_name, agent_id); @@ -70,6 +78,13 @@ async fn handle_agent_connection( info!("Session created: {}", session_id); + // If a support code was provided, mark it as connected + if let Some(ref code) = support_code { + info!("Linking support code {} to session {}", code, session_id); + support_codes.mark_connected(code, Some(agent_name.clone()), Some(agent_id.clone())).await; + support_codes.link_session(code, session_id).await; + } + let (mut ws_sender, mut ws_receiver) = socket.split(); // Task to forward input events from viewers to agent @@ -82,6 +97,8 @@ async fn handle_agent_connection( }); let sessions_cleanup = sessions.clone(); + let support_codes_cleanup = support_codes.clone(); + let support_code_cleanup = support_code.clone(); // Main loop: receive frames from agent and broadcast to viewers while let Some(msg) = ws_receiver.next().await { @@ -119,6 +136,13 @@ async fn handle_agent_connection( // Cleanup input_forward.abort(); sessions_cleanup.remove_session(session_id).await; + + // Mark support code as completed if one was used + if let Some(ref code) = support_code_cleanup { + support_codes_cleanup.mark_completed(code).await; + info!("Support code {} marked as completed", code); + } + info!("Session {} ended", session_id); } diff --git a/server/src/support_codes.rs b/server/src/support_codes.rs index a8f93d2..ae119ea 100644 --- a/server/src/support_codes.rs +++ b/server/src/support_codes.rs @@ -147,6 +147,27 @@ impl SupportCodeManager { } } + /// Link a support code to an actual WebSocket session + pub async fn link_session(&self, code: &str, real_session_id: Uuid) { + let mut codes = self.codes.write().await; + if let Some(support_code) = codes.get_mut(code) { + // Update session_to_code mapping with real session ID + let old_session_id = support_code.session_id; + support_code.session_id = real_session_id; + + // Update the reverse mapping + let mut session_to_code = self.session_to_code.write().await; + session_to_code.remove(&old_session_id); + session_to_code.insert(real_session_id, code.to_string()); + } + } + + /// Get code by its code string + pub async fn get_code(&self, code: &str) -> Option { + let codes = self.codes.read().await; + codes.get(code).cloned() + } + /// Mark a code as completed pub async fn mark_completed(&self, code: &str) { let mut codes = self.codes.write().await; diff --git a/server/static/dashboard.html b/server/static/dashboard.html index 05e3a9c..3b45c93 100644 --- a/server/static/dashboard.html +++ b/server/static/dashboard.html @@ -269,17 +269,17 @@