diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..9438e74 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,19 @@ +# GuruConnect Cargo Configuration (Windows Development) + +# Default to 64-bit Windows MSVC for local dev +[build] +target = "x86_64-pc-windows-msvc" + +# Build aliases for convenience +[alias] +# Build 64-bit release +b64 = "build --release --target x86_64-pc-windows-msvc" +# Build 32-bit release +b32 = "build --release --target i686-pc-windows-msvc" + +# Target-specific settings - static CRT for standalone binaries +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] + +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/Cargo.lock b/Cargo.lock index 0096198..86cd7d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,6 +691,7 @@ dependencies = [ "prost", "prost-build", "prost-types", + "rand", "ring", "serde", "serde_json", @@ -814,6 +815,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -1155,6 +1162,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2419,8 +2436,15 @@ dependencies = [ "bitflags", "bytes", "futures-core", + "futures-util", "http", "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", "tokio", "tokio-util", @@ -2528,6 +2552,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..c28c711 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,692 @@ +# GuruConnect Requirements + +## Design Principles + +1. **End-user simplicity** - One-click or code-based session joining +2. **Standalone capable** - Works independently, integrates with GuruRMM optionally +3. **Technician-centric** - Built for MSP workflows + +--- + +## End-User Portal (connect.azcomputerguru.com) + +### Unauthenticated View + +When a user visits the portal without being logged in: + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ [Company Logo] │ +│ │ +│ Enter your support code: │ +│ ┌─────────────────────────┐ │ +│ │ 8 4 7 2 9 1 │ │ +│ └─────────────────────────┘ │ +│ │ +│ [ Connect ] │ +│ │ +│ ───────────────────────────────────────────────── │ +│ │ +│ Instructions will appear here after clicking │ +│ Connect, based on your browser. │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +### Connection Flow + +1. **User enters code** → Click "Connect" +2. **Server validates code** → Returns session info or error +3. **Attempt app launch** via custom protocol: + - `guruconnect://session/{code}` + - If app is installed, it launches and connects +4. **If app doesn't launch** (timeout ~3 seconds): + - Auto-download small EXE (`GuruConnect-{code}.exe`) + - Show browser-specific instructions + +### Browser-Specific Instructions + +Detect browser via User-Agent and show appropriate guidance: + +**Chrome:** +> "Click the download in the bottom-left corner of your screen, then click 'Open'" +> [Screenshot of Chrome download bar] + +**Firefox:** +> "Click 'Save File', then open your Downloads folder and double-click the file" +> [Screenshot of Firefox download dialog] + +**Edge:** +> "Click 'Open file' in the download notification at the top of your screen" +> [Screenshot of Edge download prompt] + +**Safari:** +> "Click the download icon in the toolbar, then double-click the file" +> [Screenshot of Safari downloads] + +**Generic/Unknown:** +> "Your download should start automatically. Look for the file in your Downloads folder and double-click to run it." + +### Custom Protocol Handler + +**Protocol:** `guruconnect://` + +**Format:** `guruconnect://session/{code}` + +**Registration:** +- Permanent agent registers protocol handler on install +- One-time agent does NOT register (to avoid clutter) + +**Behavior:** +- If registered: OS launches installed agent with session code +- If not registered: Browser shows "nothing happened" → triggers download fallback + +### One-Time Session Agent (Temp/Support) + +**Key Requirements:** +- Runs in **user space** - NO admin elevation required +- Downloads as `GuruConnect-{code}.exe` (code baked in) +- ~3-5MB executable +- Self-contained (no installer, no dependencies) +- Connects directly to session on launch +- Self-deletes after session ends (or on next reboot) + +**Elevation Note:** +- Basic screen sharing works without admin +- Some features (input to elevated windows, UAC dialogs) need admin +- Show optional "Run as Administrator" button for full access + +--- + +## Technician Dashboard (Logged-In View) + +### Visual Style + +Follow GuruRMM dashboard design: +- HSL CSS variables for theming (dark/light mode support) +- Sidebar navigation with lucide-react icons +- Card-based content areas +- Responsive layout (mobile hamburger menu) +- Consistent component library (Button, Card, Input) + +### Navigation Structure + +``` +┌──────────────────────────────────────────────────────────────┐ +│ ┌──────────┐ │ +│ │GuruConnect│ │ +│ └──────────┘ │ +│ │ +│ 📋 Support ← Active temp sessions │ +│ 🖥️ Access ← Unattended/permanent sessions │ +│ 🔧 Build ← Installer builder │ +│ ⚙️ Settings ← Preferences, groupings, appearance │ +│ │ +│ ───────────── │ +│ 👤 Mike S. │ +│ Admin │ +│ [Sign out] │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Support Tab (Active Temporary Sessions) + +**Layout:** +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Support Sessions [ + Generate Code ] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ▼ My Sessions (3) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 847291 │ John's PC │ Connected │ 00:15:32 │ [Join] [End] │ │ +│ │ 293847 │ Waiting │ Pending │ - │ [Cancel] │ │ +│ │ 182736 │ Sarah-PC │ Connected │ 00:45:10 │ [Join] [End] │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ▼ Team Sessions (2) [Howard's sessions] │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 928374 │ DESKTOP-A │ Connected │ 00:05:22 │ [View] [Join] │ │ +│ │ 746382 │ Laptop-01 │ Connected │ 01:20:15 │ [View] │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ▶ Support Requests (1) [End-user initiated] │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**Features:** +- Sessions grouped by technician (own first, then team) +- Real-time status updates (WebSocket) +- Duration timer for active sessions +- Quick actions: Join, View (spectate), End, Cancel +- Support request queue from end-user tray icon requests + +### Access Tab (Unattended/Permanent Sessions) + +**Layout:** +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Access 🔍 [Search...] [ + Build ] │ +├──────────────┬──────────────────────────────────────────────────────┤ +│ │ │ +│ ▼ By Company │ All Machines by Company 1083 machines │ +│ (empty) 120│ ─────────────────────────────────────────────────── │ +│ 4 Paws 1│ ┌──────────────────────────────────────────────┐ │ +│ ACG 10│ │ ● PC-FRONT01 │ Glaztech │ Win 11 │ Online │ │ +│ Glaztech 224 │ ● SERVER-DC01 │ Glaztech │ Svr 22 │ Online │ │ +│ AirPros 2│ │ ○ LAPTOP-SALES │ Glaztech │ Win 10 │ 2h ago │ │ +│ ... │ │ ● WORKSTATION-3 │ ACG │ Win 11 │ Online │ │ +│ │ │ ... │ │ +│ ▶ By Site │ └──────────────────────────────────────────────┘ │ +│ ▶ By OS │ │ +│ ▶ By Tag │ ──────────────── Machine Detail ───────────────── │ +│ │ Name: PC-FRONT01 │ +│ ──────────── │ Company: Glaztech Industries │ +│ Smart Groups │ Site: Phoenix Office │ +│ ──────────── │ OS: Windows 11 Pro (23H2) │ +│ Attention 1│ User: jsmith │ +│ Online 847 IP: 192.168.1.45 / 72.194.62.4 │ +│ Offline 30d 241 Serial: 8XKJF93 │ +│ Offline 1yr 238 Last Seen: Now │ +│ Outdated 516│ │ +│ Recent 5│ [ Connect ] [ Wake ] [ Tools ▼ ] │ +│ │ │ +│ ▶ My Filters │ │ +│ + New Filter│ │ +└──────────────┴──────────────────────────────────────────────────────┘ +``` + +**Left Sidebar - Groupings:** +- By Company (with counts, expandable) +- By Site +- By OS +- By Tag +- By Device Type +- Smart Groups (auto-generated) +- Custom Filters (user-created) + +**Main Panel:** +- Machine list with status indicators (● online, ○ offline) +- Quick info columns (configurable) +- Click to select → shows detail panel + +**Right Panel - Machine Detail:** +- Full machine info (Session, Device, Network sections) +- Action buttons: Connect, Wake (if offline), Tools dropdown + +### Build Tab (Installer Builder) + +**Layout:** +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Build Installer │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Name: [ Use Machine Name ▼ ] │ +│ Company: [ __________________________ ] (autocomplete) │ +│ Site: [ __________________________ ] (autocomplete) │ +│ Department: [ __________________________ ] │ +│ Device Type: [ Workstation ▼ ] │ +│ Tag: [ __________________________ ] │ +│ │ +│ Platform: ○ Windows 64-bit (recommended) │ +│ ○ Windows 32-bit │ +│ ○ Linux (coming soon) │ +│ │ +│ ───────────────────────────────────────────────────────────────── │ +│ │ +│ [ Download EXE ] [ Download MSI ] [ Copy URL ] [ Send Link ] │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Settings Tab + +**Sections:** + +**Appearance:** +- Theme: Light / Dark / System +- Sidebar: Expanded / Collapsed by default +- Default landing tab: Support / Access + +**Groupings:** +- Default grouping for Access tab +- Show/hide specific smart groups +- Configure custom filter defaults + +**Notifications:** +- Browser notifications: On/Off +- Sound alerts: On/Off +- Email alerts for support requests: On/Off + +**Session Defaults:** +- Default session visibility: Private / Team / Company +- Auto-accept from specific companies + +**Account:** +- Change password +- Two-factor authentication +- API keys (for integrations) + +--- + +## Session Types + +### 1. Support Sessions (Attended/One-Time) + +**End-User Experience:** +- User visits portal (e.g., `support.azcomputerguru.com`) +- Portal generates a 5-6 digit numeric code (e.g., `847291`) +- User enters code OR clicks generated link +- Small executable downloads and runs (no install required) +- Session connects to assigned technician + +**Technician Experience:** +- Generate session codes from dashboard +- Codes can be pre-assigned to specific tech or first-come +- Session appears on assigned tech's dashboard + +**Code Management:** +- Codes remain active until used (no automatic expiration) +- Anti-collision: Active codes tracked in database, never reissued while active +- Once session completes, code is released back to pool +- Manual code cancellation available +- Optional: Tech can set manual expiration if desired +- 6 digits = 1M codes, plenty of headroom for concurrent active codes + +### 2. Unattended Sessions (Permanent/MSP) + +**Installer Builder:** + +Build custom installers with pre-defined metadata fields: + +| Field | Description | Example | +|-------|-------------|---------| +| Name | Machine identifier | "Use Machine Name" (auto) or custom | +| Company | Client/organization | "Glaztech Industries" | +| Site | Physical location | "Phoenix Office" | +| Department | Business unit | "Accounting" | +| Device Type | Machine category | "Workstation", "Server", "Laptop" | +| Tag | Custom label | "VIP", "Critical", "Testing" | + +**Installer Output Options:** +- Download EXE directly +- Download MSI (for GPO deployment) +- Copy installer URL (for deployment scripts) +- Send link via email + +**Server-Built Installers:** +- Server generates installers on-demand +- All metadata (Company, Site, etc.) baked into binary +- Unique installer per configuration +- No manual config file editing required +- Server URL and auth token embedded + +**MSI Support:** +- MSI wrapper for Group Policy deployment +- Silent install support: `msiexec /i guruconnect.msi /qn` +- Uninstall via Add/Remove Programs or GPO +- Transform files (.mst) for custom configurations (optional) + +**End-User Reconfiguration:** +- Re-run installer with flags to modify settings +- `--reconfigure` flag enters config mode instead of reinstall +- User can change: Name, Site, Tag, Department +- Changes sync to server on next check-in +- Useful for when machine moves to different site/department + +Example: +``` +guruconnect-agent.exe --reconfigure --site "New York Office" --tag "Laptop" +``` + +**Deployment:** +- Installed as Windows service +- Persists across reboots +- Auto-reconnects on network changes +- Can be bundled with GuruRMM agent OR standalone +- Metadata fields baked into agent at build time + +**Management:** +- Assigned to client/site hierarchy +- Always available for remote access (when machine is on) +- Background service, no user interaction required + +--- + +## Technician Dashboard + +### Session Visibility & Permissions + +| Role | Own Sessions | Team Sessions | All Sessions | +|------|--------------|---------------|--------------| +| Technician | Full access | View if permitted | No | +| Senior Tech | Full access | View + join | View | +| Admin | Full access | Full access | Full access | + +**Permission Model:** +- Sessions created by a tech default to their dashboard +- Configurable visibility: Private, Team, Company-wide +- "Snoop" capability for supervisors (view session list, optionally join) +- Session handoff between technicians + +### Auto-Generated Groups (Sidebar) + +The dashboard automatically generates navigable groups based on metadata and status: + +**By Metadata Field:** +- All Machines by Company (with counts per company) +- All Machines by Site +- All Machines by OS +- All Machines by Tag +- All Machines by Device Type + +**Smart Status Groups:** +| Group | Definition | +|-------|------------| +| Attention | Machines flagged for follow-up | +| Host Connected | Tech currently connected | +| Guest Connected | End-user currently at machine | +| Recently Accessed | Connected within last 24 hours | +| Offline 30 Days | No check-in for 30+ days | +| Offline 1 Year | Stale agents, cleanup candidates | +| Outdated Clients | Agent version behind current | +| Powered on last 10 min | Just came online | + +**Custom Session Groups:** +- Create saved filter combinations +- Name and organize custom groups +- Share groups with team (optional) + +### Machine Detail Panel + +When a machine is selected, show comprehensive info in side panel: + +**Session Info:** +- Name, Company, Site, Department +- Device Type, Tag +- Hosts Connected (tech count) +- Guests Connected (user present) +- Guest Last Connected +- Logged On User +- Idle Time +- Pending Activity +- Custom Attributes + +**Device Info:** +- Machine name +- Operating System + Version +- OS Install Date +- Processor +- Available Memory +- Manufacturer & Model +- Serial Number / Service Tag +- Machine Description + +**Network Info:** +- Public IP Address +- Private IP Address(es) +- MAC Address(es) + +**Other:** +- Agent Version +- Last Check-in +- First Seen +- Screenshot thumbnail (optional) + +### Unattended Session Search + +**Searchable Fields:** +- Hostname / Computer name +- Internal IP address +- External/Public IP address +- Currently logged-in user +- OS type (Windows 10, 11, Server 2019, etc.) +- OS version/build number +- Serial number +- Service tag (Dell, HP, Lenovo tags) +- Client/Site assignment +- Custom tags/labels +- Last check-in time +- Agent version + +**Filter Capabilities:** +- Last check-in: < 1 hour, < 24 hours, < 7 days, > 30 days (stale) +- OS type grouping +- Client/Site hierarchy +- Online/Offline status +- Custom saved filters (user-defined queries) + +**Saved Searches:** +- Create and name custom filter combinations +- Share saved searches with team +- Pin frequently used searches + +--- + +## Remote Control Features + +### Screen Control +- Real-time screen viewing +- Mouse control (click, drag, scroll) +- Keyboard input +- Multi-monitor support (switch displays, view all) + +### Clipboard Integration + +**Priority Feature - Full Bidirectional Clipboard:** + +| Direction | Content Types | +|-----------|---------------| +| Local → Remote | Text, Files, Images, Rich text | +| Remote → Local | Text, Files, Images, Rich text | + +**Special Capabilities:** +- **Keystroke injection from clipboard** - Paste local clipboard as keystrokes (for login screens, BIOS, pre-OS environments) +- Drag-and-drop file transfer +- Large file support (chunked transfer with progress) + +### File Transfer +- Browse remote filesystem +- Upload files to remote +- Download files from remote +- Drag-and-drop support +- Transfer queue with progress + +### Backstage Tools (No Screen Required) +- Remote command prompt / PowerShell +- Task manager view +- Services manager +- Registry editor (future) +- Event log viewer (future) +- System info panel + +### Chat / Messaging + +**Bidirectional Chat:** +- Tech can message end user during session +- End user can message tech +- Chat persists across session reconnects +- Chat history viewable in session log + +**End-User Initiated Contact:** +- System tray icon for permanent agents +- "Request Support" option in tray menu +- User can type message/description of issue +- Creates support request visible to assigned technicians + +**Technician Notifications:** +- Dashboard shows pending support requests +- Optional: Desktop/browser notifications for new requests +- Optional: Email/webhook alerts for after-hours requests +- Request queue with timestamps and user messages + +### Credential Management (Future) + +**Credential Injection:** +- Integration with ITGlue for credential lookup +- Integration with GuruRMM credential vault +- Tech selects credential from dropdown, never sees actual password +- Credential injected directly as keystrokes to remote session +- Audit log of which credential was used, by whom, when + +**Local Credential Capture (Future):** +- Optional feature to capture credentials entered during session +- Stored encrypted, accessible only to admins +- For scenarios where client provides password verbally + +--- + +## Security Requirements + +### Authentication +- Technician login with username/password +- MFA/2FA support (TOTP) +- SSO integration (future - Azure AD, Google) +- API key auth for programmatic access + +### Session Security +- All traffic over TLS/WSS +- End-to-end encryption for screen data +- Session consent prompt (attended sessions) +- Configurable session timeout + +### Audit & Compliance +- Full audit log: who, when, what machine, duration +- Optional session recording +- Action logging (file transfers, commands run) +- Exportable audit reports + +--- + +## Integration + +### GuruRMM Integration +- Launch remote session from RMM agent list +- Share agent data (hostname, IP, user, etc.) +- Single authentication +- Unified dashboard option + +### Standalone Mode +- Fully functional without GuruRMM +- Own user management +- Own agent deployment +- Can be licensed/sold separately + +--- + +## Agent Requirements + +### Support Session Agent (One-Time) +- Single executable, no installation +- Downloads and runs from portal +- Self-deletes after session ends +- Minimal footprint (<5MB) +- No admin rights required for basic screen share +- Admin rights optional for elevated access + +### Unattended Agent (Permanent) +- Windows service installation +- Auto-start on boot +- Runs as SYSTEM for full access +- Configurable check-in interval +- Resilient reconnection + +**Auto-Update:** +- Agent checks for updates on configurable interval +- Silent background update (no user interaction) +- Rollback capability if update fails +- Version reported to server for "Outdated Clients" filtering + +**Lightweight Performance:** +- Minimal CPU/RAM footprint when idle +- No performance impact during normal operation +- Screen capture only active during remote session +- Target: <10MB RAM idle, <1% CPU idle + +**Survival & Recovery:** +- Survives reboots (Windows service auto-start) +- Works in Safe Mode with Networking +- Registers as safe-mode-capable service +- Remote-initiated Safe Mode reboot (with networking) +- Auto-reconnects after safe mode boot + +**Safe Mode Reboot Feature:** +- Tech can trigger safe mode reboot from dashboard +- Options: Safe Mode, Safe Mode with Networking, Safe Mode with Command Prompt +- Agent persists through safe mode boot +- Useful for malware removal, driver issues, repairs + +**Emergency Reboot:** +- Force immediate reboot without waiting for processes +- Bypasses "program not responding" dialogs +- Equivalent to holding power button, but cleaner +- Use case: Frozen system, hung updates, unresponsive machine +- Confirmation required to prevent accidental use + +**Wake-on-LAN:** +- Store MAC address for each agent +- Send WoL magic packet to wake offline machines +- Works within same broadcast domain (LAN) +- For remote WoL: requires WoL relay/proxy on local network +- Dashboard shows "Wake" button for offline machines with known MAC +- Optional: Integration with GuruRMM agent as WoL relay + +### Reported Metrics (Unattended) +- Hostname +- Internal IP(s) +- External IP +- Current user +- OS type and version +- Serial number +- Service tag +- CPU, RAM, Disk (basic) +- Last boot time +- Agent version +- Custom properties (extensible) + +--- + +## Platform Support + +### Build Targets + +| Target | Architecture | Priority | Notes | +|--------|--------------|----------|-------| +| `x86_64-pc-windows-msvc` | 64-bit | Primary | Default build, Win7+ | +| `i686-pc-windows-msvc` | 32-bit | Secondary | Legacy outliers | + +### Phase 1 (MVP) +- Windows 10/11 agents (64-bit) +- Windows Server 2016+ agents (64-bit) +- Web dashboard (any browser) + +### Phase 2 +- 32-bit agent builds for legacy systems +- Windows 7/8.1 support + +### Future Phases +- macOS agent +- Linux agent +- Mobile viewer (iOS/Android) + +--- + +## Non-Functional Requirements + +### Performance +- Screen updates: 30+ FPS on LAN, 15+ FPS on WAN +- Input latency: <100ms on LAN, <200ms on WAN +- Support 50+ concurrent unattended agents per server (scalable) + +### Reliability +- Agent auto-reconnect on network change +- Server clustering for HA (future) +- Graceful degradation on poor networks + +### Deployment +- Single binary server (Docker or native) +- Single binary agent (MSI installer + standalone EXE) +- Cloud-hostable or on-premises diff --git a/server/Cargo.toml b/server/Cargo.toml index 15f3932..da4f629 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,7 +12,7 @@ tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", # Web framework axum = { version = "0.7", features = ["ws", "macros"] } tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] } +tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "fs"] } # WebSocket futures-util = "0.3" @@ -52,6 +52,7 @@ uuid = { version = "1", features = ["v4", "serde"] } # Time chrono = { version = "0.4", features = ["serde"] } +rand = "0.8" [build-dependencies] prost-build = "0.13" diff --git a/server/src/main.rs b/server/src/main.rs index 183660d..5ecedb5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,6 +9,7 @@ mod session; mod auth; mod api; mod db; +mod support_codes; pub mod proto { include!(concat!(env!("OUT_DIR"), "/guruconnect.rs")); @@ -17,13 +18,27 @@ pub mod proto { use anyhow::Result; use axum::{ Router, - routing::get, + routing::{get, post}, + extract::{Path, State, Json}, + response::{Html, IntoResponse}, + http::StatusCode, }; use std::net::SocketAddr; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; +use tower_http::services::ServeDir; use tracing::{info, Level}; use tracing_subscriber::FmtSubscriber; +use serde::Deserialize; + +use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation}; + +/// Application state +#[derive(Clone)] +pub struct AppState { + sessions: session::SessionManager, + support_codes: SupportCodeManager, +} #[tokio::main] async fn main() -> Result<()> { @@ -37,26 +52,46 @@ async fn main() -> Result<()> { // Load configuration let config = config::Config::load()?; - info!("Loaded configuration, listening on {}", config.listen_addr); + + // Use port 3002 for GuruConnect + let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string()); + info!("Loaded configuration, listening on {}", listen_addr); - // Initialize database connection (optional for MVP) - // let db = db::init(&config.database_url).await?; - - // Create session manager - let sessions = session::SessionManager::new(); + // Create application state + let state = AppState { + sessions: session::SessionManager::new(), + support_codes: SupportCodeManager::new(), + }; // Build router let app = Router::new() // Health check .route("/health", get(health)) + + // Portal API - Support codes + .route("/api/codes", post(create_code)) + .route("/api/codes", get(list_codes)) + .route("/api/codes/:code/validate", get(validate_code)) + .route("/api/codes/:code/cancel", post(cancel_code)) + // WebSocket endpoints .route("/ws/agent", get(relay::agent_ws_handler)) .route("/ws/viewer", get(relay::viewer_ws_handler)) - // REST API - .route("/api/sessions", get(api::list_sessions)) - .route("/api/sessions/:id", get(api::get_session)) + + // REST API - Sessions + .route("/api/sessions", get(list_sessions)) + .route("/api/sessions/:id", get(get_session)) + + // HTML page routes (clean URLs) + .route("/login", get(serve_login)) + .route("/dashboard", get(serve_dashboard)) + // State - .with_state(sessions) + .with_state(state) + + // Serve static files for portal (fallback) + .fallback_service(ServeDir::new("static").append_index_html_on_directories(true)) + // Middleware .layer(TraceLayer::new_for_http()) .layer( @@ -67,7 +102,7 @@ async fn main() -> Result<()> { ); // Start server - let addr: SocketAddr = config.listen_addr.parse()?; + let addr: SocketAddr = listen_addr.parse()?; let listener = tokio::net::TcpListener::bind(addr).await?; info!("Server listening on {}", addr); @@ -80,3 +115,80 @@ async fn main() -> Result<()> { async fn health() -> &'static str { "OK" } + +// Support code API handlers + +async fn create_code( + State(state): State, + Json(request): Json, +) -> Json { + let code = state.support_codes.create_code(request).await; + info!("Created support code: {}", code.code); + Json(code) +} + +async fn list_codes( + State(state): State, +) -> Json> { + Json(state.support_codes.list_active_codes().await) +} + +#[derive(Deserialize)] +struct ValidateParams { + code: String, +} + +async fn validate_code( + State(state): State, + Path(code): Path, +) -> Json { + Json(state.support_codes.validate_code(&code).await) +} + +async fn cancel_code( + State(state): State, + Path(code): Path, +) -> impl IntoResponse { + if state.support_codes.cancel_code(&code).await { + (StatusCode::OK, "Code cancelled") + } else { + (StatusCode::BAD_REQUEST, "Cannot cancel code") + } +} + +// Session API handlers (updated to use AppState) + +async fn list_sessions( + State(state): State, +) -> Json> { + let sessions = state.sessions.list_sessions().await; + Json(sessions.into_iter().map(api::SessionInfo::from).collect()) +} + +async fn get_session( + State(state): State, + Path(id): Path, +) -> Result, (StatusCode, &'static str)> { + let session_id = uuid::Uuid::parse_str(&id) + .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?; + + let session = state.sessions.get_session(session_id).await + .ok_or((StatusCode::NOT_FOUND, "Session not found"))?; + + Ok(Json(api::SessionInfo::from(session))) +} + +// Static page handlers +async fn serve_login() -> impl IntoResponse { + match tokio::fs::read_to_string("static/login.html").await { + Ok(content) => Html(content).into_response(), + Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(), + } +} + +async fn serve_dashboard() -> impl IntoResponse { + match tokio::fs::read_to_string("static/dashboard.html").await { + Ok(content) => Html(content).into_response(), + Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(), + } +} diff --git a/server/src/relay/mod.rs b/server/src/relay/mod.rs index 5ef72a8..f895dff 100644 --- a/server/src/relay/mod.rs +++ b/server/src/relay/mod.rs @@ -17,6 +17,7 @@ use tracing::{error, info, warn}; use crate::proto; use crate::session::SessionManager; +use crate::AppState; #[derive(Debug, Deserialize)] pub struct AgentParams { @@ -33,11 +34,12 @@ pub struct ViewerParams { /// WebSocket handler for agent connections pub async fn agent_ws_handler( ws: WebSocketUpgrade, - State(sessions): State, + State(state): State, Query(params): Query, ) -> impl IntoResponse { let agent_id = params.agent_id; let agent_name = params.agent_name.unwrap_or_else(|| agent_id.clone()); + let sessions = state.sessions.clone(); ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, agent_id, agent_name)) } @@ -45,10 +47,11 @@ pub async fn agent_ws_handler( /// WebSocket handler for viewer connections pub async fn viewer_ws_handler( ws: WebSocketUpgrade, - State(sessions): State, + State(state): State, Query(params): Query, ) -> impl IntoResponse { let session_id = params.session_id; + let sessions = state.sessions.clone(); ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, session_id)) } @@ -78,6 +81,8 @@ async fn handle_agent_connection( } }); + let sessions_cleanup = sessions.clone(); + // Main loop: receive frames from agent and broadcast to viewers while let Some(msg) = ws_receiver.next().await { match msg { @@ -113,7 +118,7 @@ async fn handle_agent_connection( // Cleanup input_forward.abort(); - sessions.remove_session(session_id).await; + sessions_cleanup.remove_session(session_id).await; info!("Session {} ended", session_id); } @@ -154,6 +159,8 @@ async fn handle_viewer_connection( } }); + let sessions_cleanup = sessions.clone(); + // Main loop: receive input from viewer and forward to agent while let Some(msg) = ws_receiver.next().await { match msg { @@ -189,6 +196,6 @@ async fn handle_viewer_connection( // Cleanup frame_forward.abort(); - sessions.leave_session(session_id).await; + sessions_cleanup.leave_session(session_id).await; info!("Viewer left session: {}", session_id); } diff --git a/server/src/support_codes.rs b/server/src/support_codes.rs new file mode 100644 index 0000000..a8f93d2 --- /dev/null +++ b/server/src/support_codes.rs @@ -0,0 +1,199 @@ +//! Support session codes management +//! +//! Handles generation and validation of 6-digit support codes +//! for one-time remote support sessions. + +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use chrono::{DateTime, Utc}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// A support session code +#[derive(Debug, Clone, Serialize)] +pub struct SupportCode { + pub code: String, + pub session_id: Uuid, + pub created_by: String, + pub created_at: DateTime, + pub status: CodeStatus, + pub client_name: Option, + pub client_machine: Option, + pub connected_at: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum CodeStatus { + Pending, // Waiting for client to connect + Connected, // Client connected, session active + Completed, // Session ended normally + Cancelled, // Code cancelled by tech +} + +/// Request to create a new support code +#[derive(Debug, Deserialize)] +pub struct CreateCodeRequest { + pub technician_id: Option, + pub technician_name: Option, +} + +/// Response when a code is validated +#[derive(Debug, Serialize)] +pub struct CodeValidation { + pub valid: bool, + pub session_id: Option, + pub server_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Manages support codes +#[derive(Clone)] +pub struct SupportCodeManager { + codes: Arc>>, + session_to_code: Arc>>, +} + +impl SupportCodeManager { + pub fn new() -> Self { + Self { + codes: Arc::new(RwLock::new(HashMap::new())), + session_to_code: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Generate a unique 6-digit code + async fn generate_unique_code(&self) -> String { + let codes = self.codes.read().await; + let mut rng = rand::thread_rng(); + + loop { + let code: u32 = rng.gen_range(100000..999999); + let code_str = code.to_string(); + + if !codes.contains_key(&code_str) { + return code_str; + } + } + } + + /// Create a new support code + pub async fn create_code(&self, request: CreateCodeRequest) -> SupportCode { + let code = self.generate_unique_code().await; + let session_id = Uuid::new_v4(); + + let support_code = SupportCode { + code: code.clone(), + session_id, + created_by: request.technician_name.unwrap_or_else(|| "Unknown".to_string()), + created_at: Utc::now(), + status: CodeStatus::Pending, + client_name: None, + client_machine: None, + connected_at: None, + }; + + let mut codes = self.codes.write().await; + codes.insert(code.clone(), support_code.clone()); + + let mut session_to_code = self.session_to_code.write().await; + session_to_code.insert(session_id, code); + + support_code + } + + /// Validate a code and return session info + pub async fn validate_code(&self, code: &str) -> CodeValidation { + let codes = self.codes.read().await; + + match codes.get(code) { + Some(support_code) => { + if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected { + CodeValidation { + valid: true, + session_id: Some(support_code.session_id.to_string()), + server_url: Some("wss://connect.azcomputerguru.com/ws/support".to_string()), + error: None, + } + } else { + CodeValidation { + valid: false, + session_id: None, + server_url: None, + error: Some("This code has expired or been used".to_string()), + } + } + } + None => CodeValidation { + valid: false, + session_id: None, + server_url: None, + error: Some("Invalid code".to_string()), + }, + } + } + + /// Mark a code as connected + pub async fn mark_connected(&self, code: &str, client_name: Option, client_machine: Option) { + let mut codes = self.codes.write().await; + if let Some(support_code) = codes.get_mut(code) { + support_code.status = CodeStatus::Connected; + support_code.client_name = client_name; + support_code.client_machine = client_machine; + support_code.connected_at = Some(Utc::now()); + } + } + + /// Mark a code as completed + pub async fn mark_completed(&self, code: &str) { + let mut codes = self.codes.write().await; + if let Some(support_code) = codes.get_mut(code) { + support_code.status = CodeStatus::Completed; + } + } + + /// Cancel a code + pub async fn cancel_code(&self, code: &str) -> bool { + let mut codes = self.codes.write().await; + if let Some(support_code) = codes.get_mut(code) { + if support_code.status == CodeStatus::Pending { + support_code.status = CodeStatus::Cancelled; + return true; + } + } + false + } + + /// List all codes (for dashboard) + pub async fn list_codes(&self) -> Vec { + let codes = self.codes.read().await; + codes.values().cloned().collect() + } + + /// List active codes only + pub async fn list_active_codes(&self) -> Vec { + let codes = self.codes.read().await; + codes.values() + .filter(|c| c.status == CodeStatus::Pending || c.status == CodeStatus::Connected) + .cloned() + .collect() + } + + /// Get code by session ID + pub async fn get_by_session(&self, session_id: Uuid) -> Option { + let session_to_code = self.session_to_code.read().await; + let code = session_to_code.get(&session_id)?; + + let codes = self.codes.read().await; + codes.get(code).cloned() + } +} + +impl Default for SupportCodeManager { + fn default() -> Self { + Self::new() + } +} diff --git a/server/static/dashboard.html b/server/static/dashboard.html new file mode 100644 index 0000000..05e3a9c --- /dev/null +++ b/server/static/dashboard.html @@ -0,0 +1,481 @@ + + + + + + GuruConnect - Dashboard + + + +
+
+ +
+
+ + +
+
+ + + +
+ +
+
+
+
+

Active Support Sessions

+

Temporary sessions initiated by support codes

+
+ +
+
+ + + + + + + + + + + + + + + +
CodeStatusCreatedTechnicianActions
+
+

No active sessions

+

Generate a code to start a support session

+
+
+
+
+
+ + +
+
+ +
+
+

No machines

+

Install the agent on a machine to see it here

+
+
+
+
+

Select a machine

+

Click a machine to view details

+
+
+
+
+ + +
+
+
+
+

Installer Builder

+

Create customized agent installers for unattended access

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+

+ Agent builds will be available once the agent is compiled. +

+
+
+ + +
+
+
+
+

Settings

+

Configure your GuruConnect preferences

+
+
+
+
+ + +
+
+ + +
+
+

+ Additional settings coming soon. +

+
+
+
+ + + + diff --git a/server/static/index.html b/server/static/index.html new file mode 100644 index 0000000..0ec763a --- /dev/null +++ b/server/static/index.html @@ -0,0 +1,415 @@ + + + + + + GuruConnect - Remote Support + + + +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+

How to connect:

+
    +
  1. Enter the 6-digit code provided by your technician
  2. +
  3. Click "Connect" to start the session
  4. +
  5. If prompted, allow the download and run the file
  6. +
+
+ + +
+ + + + diff --git a/server/static/login.html b/server/static/login.html new file mode 100644 index 0000000..23c5819 --- /dev/null +++ b/server/static/login.html @@ -0,0 +1,230 @@ + + + + + + GuruConnect - Technician Login + + + +
+ + + + +
+

Auth not yet configured. Skip to Dashboard

+
+ + +
+ + + +