Initial GuruConnect implementation - Phase 1 MVP

- Agent: DXGI/GDI screen capture, mouse/keyboard input, WebSocket transport
- Server: Axum relay, session management, REST API
- Dashboard: React viewer components with TypeScript
- Protocol: Protobuf definitions for all message types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AZ Computer Guru
2025-12-21 17:18:05 -07:00
commit 33893ea73b
38 changed files with 7724 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
//! WebSocket transport for agent-server communication
mod websocket;
pub use websocket::WebSocketTransport;

View File

@@ -0,0 +1,183 @@
//! WebSocket client transport
//!
//! Handles WebSocket connection to the GuruConnect server with:
//! - TLS encryption
//! - Automatic reconnection
//! - Protobuf message serialization
use crate::proto::Message;
use anyhow::{Context, Result};
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use prost::Message as ProstMessage;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio_tungstenite::{
connect_async, tungstenite::protocol::Message as WsMessage, MaybeTlsStream, WebSocketStream,
};
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
/// WebSocket transport for server communication
pub struct WebSocketTransport {
stream: Arc<Mutex<WsStream>>,
incoming: VecDeque<Message>,
connected: bool,
}
impl WebSocketTransport {
/// Connect to the server
pub async fn connect(url: &str, api_key: &str) -> Result<Self> {
// Append API key as query parameter
let url_with_auth = if url.contains('?') {
format!("{}&api_key={}", url, api_key)
} else {
format!("{}?api_key={}", url, api_key)
};
tracing::info!("Connecting to {}", url);
let (ws_stream, response) = connect_async(&url_with_auth)
.await
.context("Failed to connect to WebSocket server")?;
tracing::info!("Connected, status: {}", response.status());
Ok(Self {
stream: Arc::new(Mutex::new(ws_stream)),
incoming: VecDeque::new(),
connected: true,
})
}
/// Send a protobuf message
pub async fn send(&mut self, msg: Message) -> Result<()> {
let mut stream = self.stream.lock().await;
// Serialize to protobuf binary
let mut buf = Vec::with_capacity(msg.encoded_len());
msg.encode(&mut buf)?;
// Send as binary WebSocket message
stream
.send(WsMessage::Binary(buf.into()))
.await
.context("Failed to send message")?;
Ok(())
}
/// Try to receive a message (non-blocking)
pub fn try_recv(&mut self) -> Result<Option<Message>> {
// Return buffered message if available
if let Some(msg) = self.incoming.pop_front() {
return Ok(Some(msg));
}
// Try to receive more messages
let stream = self.stream.clone();
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let mut stream = stream.lock().await;
// Use try_next for non-blocking receive
match tokio::time::timeout(
std::time::Duration::from_millis(1),
stream.next(),
)
.await
{
Ok(Some(Ok(ws_msg))) => Ok(Some(ws_msg)),
Ok(Some(Err(e))) => Err(anyhow::anyhow!("WebSocket error: {}", e)),
Ok(None) => {
// Connection closed
Ok(None)
}
Err(_) => {
// Timeout - no message available
Ok(None)
}
}
})
});
match result? {
Some(ws_msg) => {
if let Some(msg) = self.parse_message(ws_msg)? {
Ok(Some(msg))
} else {
Ok(None)
}
}
None => Ok(None),
}
}
/// Receive a message (blocking)
pub async fn recv(&mut self) -> Result<Option<Message>> {
// Return buffered message if available
if let Some(msg) = self.incoming.pop_front() {
return Ok(Some(msg));
}
let mut stream = self.stream.lock().await;
match stream.next().await {
Some(Ok(ws_msg)) => self.parse_message(ws_msg),
Some(Err(e)) => {
self.connected = false;
Err(anyhow::anyhow!("WebSocket error: {}", e))
}
None => {
self.connected = false;
Ok(None)
}
}
}
/// Parse a WebSocket message into a protobuf message
fn parse_message(&mut self, ws_msg: WsMessage) -> Result<Option<Message>> {
match ws_msg {
WsMessage::Binary(data) => {
let msg = Message::decode(Bytes::from(data))
.context("Failed to decode protobuf message")?;
Ok(Some(msg))
}
WsMessage::Ping(data) => {
// Pong is sent automatically by tungstenite
tracing::trace!("Received ping");
Ok(None)
}
WsMessage::Pong(_) => {
tracing::trace!("Received pong");
Ok(None)
}
WsMessage::Close(frame) => {
tracing::info!("Connection closed: {:?}", frame);
self.connected = false;
Ok(None)
}
WsMessage::Text(text) => {
// We expect binary protobuf, but log text messages
tracing::warn!("Received unexpected text message: {}", text);
Ok(None)
}
_ => Ok(None),
}
}
/// Check if connected
pub fn is_connected(&self) -> bool {
self.connected
}
/// Close the connection
pub async fn close(&mut self) -> Result<()> {
let mut stream = self.stream.lock().await;
stream.close(None).await?;
self.connected = false;
Ok(())
}
}