style: cargo fmt --all — make codebase rustfmt-clean
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled

First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced
pre-existing formatting drift across the agent and server crates. Apply rustfmt
across the workspace so the codebase meets its own CI gate. Pure formatting; no
logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:02:12 +00:00
parent f2e0456f8d
commit 1c5c1e78e7
48 changed files with 1174 additions and 797 deletions

View File

@@ -3,44 +3,44 @@
//! Handles connections from both agents and dashboard viewers,
//! relaying video frames and input events between them.
mod api;
mod auth;
mod config;
mod db;
mod metrics;
mod middleware;
mod relay;
mod session;
mod auth;
mod api;
mod db;
mod support_codes;
mod middleware;
mod utils;
mod metrics;
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}
use anyhow::Result;
use axum::http::{HeaderValue, Method};
use axum::{
Router,
routing::{get, post, put, delete},
extract::{Path, State, Json, Query, Request},
response::{Html, IntoResponse},
extract::{Json, Path, Query, Request, State},
http::StatusCode,
middleware::{self as axum_middleware, Next},
response::{Html, IntoResponse},
routing::{delete, get, post, put},
Router,
};
use serde::Deserialize;
use std::net::SocketAddr;
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer, AllowOrigin};
use axum::http::{Method, HeaderValue};
use tower_http::trace::TraceLayer;
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
use serde::Deserialize;
use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
use auth::{JwtConfig, TokenBlacklist, hash_password, generate_random_password, AuthenticatedUser};
use auth::{generate_random_password, hash_password, AuthenticatedUser, JwtConfig, TokenBlacklist};
use metrics::SharedMetrics;
use prometheus_client::registry::Registry;
use support_codes::{CodeValidation, CreateCodeRequest, SupportCode, SupportCodeManager};
/// Application state
#[derive(Clone)]
@@ -67,7 +67,9 @@ async fn auth_layer(
next: Next,
) -> impl IntoResponse {
request.extensions_mut().insert(state.jwt_config.clone());
request.extensions_mut().insert(Arc::new(state.token_blacklist.clone()));
request
.extensions_mut()
.insert(Arc::new(state.token_blacklist.clone()));
next.run(request).await
}
@@ -89,8 +91,9 @@ async fn main() -> Result<()> {
info!("Loaded configuration, listening on {}", listen_addr);
// JWT configuration - REQUIRED for security
let jwt_secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET environment variable must be set! Generate one with: openssl rand -base64 64");
let jwt_secret = std::env::var("JWT_SECRET").expect(
"JWT_SECRET environment variable must be set! Generate one with: openssl rand -base64 64",
);
if jwt_secret.len() < 32 {
panic!("JWT_SECRET must be at least 32 characters long for security!");
@@ -114,7 +117,10 @@ async fn main() -> Result<()> {
Some(db)
}
Err(e) => {
tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);
tracing::warn!(
"Failed to connect to database: {}. Running without persistence.",
e
);
None
}
}
@@ -194,9 +200,14 @@ async fn main() -> Result<()> {
if let Some(ref db) = database {
match db::machines::get_all_machines(db.pool()).await {
Ok(machines) => {
info!("Restoring {} persistent machines from database", machines.len());
info!(
"Restoring {} persistent machines from database",
machines.len()
);
for machine in machines {
sessions.restore_offline_machine(&machine.agent_id, &machine.hostname).await;
sessions
.restore_offline_machine(&machine.agent_id, &machine.hostname)
.await;
}
}
Err(e) => {
@@ -254,92 +265,117 @@ async fn main() -> Result<()> {
.route("/health", get(health))
// Prometheus metrics (no auth required - for monitoring)
.route("/metrics", get(prometheus_metrics))
// Auth endpoints (TODO: Add rate limiting - see SEC2_RATE_LIMITING_TODO.md)
.route("/api/auth/login", post(api::auth::login))
.route("/api/auth/change-password", post(api::auth::change_password))
.route(
"/api/auth/change-password",
post(api::auth::change_password),
)
.route("/api/auth/me", get(api::auth::get_me))
.route("/api/auth/logout", post(api::auth_logout::logout))
.route("/api/auth/revoke-token", post(api::auth_logout::revoke_own_token))
.route("/api/auth/admin/revoke-user", post(api::auth_logout::revoke_user_tokens))
.route("/api/auth/blacklist/stats", get(api::auth_logout::get_blacklist_stats))
.route("/api/auth/blacklist/cleanup", post(api::auth_logout::cleanup_blacklist))
.route(
"/api/auth/revoke-token",
post(api::auth_logout::revoke_own_token),
)
.route(
"/api/auth/admin/revoke-user",
post(api::auth_logout::revoke_user_tokens),
)
.route(
"/api/auth/blacklist/stats",
get(api::auth_logout::get_blacklist_stats),
)
.route(
"/api/auth/blacklist/cleanup",
post(api::auth_logout::cleanup_blacklist),
)
// User management (admin only)
.route("/api/users", get(api::users::list_users))
.route("/api/users", post(api::users::create_user))
.route("/api/users/:id", get(api::users::get_user))
.route("/api/users/:id", put(api::users::update_user))
.route("/api/users/:id", delete(api::users::delete_user))
.route("/api/users/:id/permissions", put(api::users::set_permissions))
.route(
"/api/users/:id/permissions",
put(api::users::set_permissions),
)
.route("/api/users/:id/clients", put(api::users::set_client_access))
// Portal API - Support codes (TODO: Add rate limiting)
.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 - Sessions
.route("/api/sessions", get(list_sessions))
.route("/api/sessions/:id", get(get_session))
.route("/api/sessions/:id", delete(disconnect_session))
// REST API - Machines
.route("/api/machines", get(list_machines))
.route("/api/machines/:agent_id", get(get_machine))
.route("/api/machines/:agent_id", delete(delete_machine))
.route("/api/machines/:agent_id/history", get(get_machine_history))
.route("/api/machines/:agent_id/update", post(trigger_machine_update))
.route(
"/api/machines/:agent_id/update",
post(trigger_machine_update),
)
// REST API - Releases and Version
.route("/api/version", get(api::releases::get_version)) // No auth - for agent polling
.route("/api/version", get(api::releases::get_version)) // No auth - for agent polling
.route("/api/releases", get(api::releases::list_releases))
.route("/api/releases", post(api::releases::create_release))
.route("/api/releases/:version", get(api::releases::get_release))
.route("/api/releases/:version", put(api::releases::update_release))
.route("/api/releases/:version", delete(api::releases::delete_release))
.route(
"/api/releases/:version",
delete(api::releases::delete_release),
)
// Changelog (no auth - public, like /api/version)
// Single route: version == "latest" selects the latest file; axum 0.7 / matchit 0.7
// panics if a static segment and a path param share this position, so do not split it.
.route("/api/changelog/:component/:version", get(api::changelog::get))
.route(
"/api/changelog/:component/:version",
get(api::changelog::get),
)
// Agent downloads (no auth - public download links)
.route("/api/download/viewer", get(api::downloads::download_viewer))
.route("/api/download/support", get(api::downloads::download_support))
.route(
"/api/download/support",
get(api::downloads::download_support),
)
.route("/api/download/agent", get(api::downloads::download_agent))
// HTML page routes (clean URLs)
.route("/login", get(serve_login))
.route("/dashboard", get(serve_dashboard))
.route("/users", get(serve_users))
// State and middleware
.with_state(state.clone())
.layer(axum_middleware::from_fn_with_state(state, auth_layer))
// Serve static files for portal (fallback)
.fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
// Middleware
.layer(axum_middleware::from_fn(middleware::add_security_headers)) // SEC-7 & SEC-12
.layer(axum_middleware::from_fn(middleware::add_security_headers)) // SEC-7 & SEC-12
.layer(TraceLayer::new_for_http())
// SEC-11: Restricted CORS configuration
.layer({
let cors = CorsLayer::new()
// Allow requests from the production domain and localhost (for development)
.allow_origin([
"https://connect.azcomputerguru.com".parse::<HeaderValue>().unwrap(),
"https://connect.azcomputerguru.com"
.parse::<HeaderValue>()
.unwrap(),
"http://localhost:3002".parse::<HeaderValue>().unwrap(),
"http://127.0.0.1:3002".parse::<HeaderValue>().unwrap(),
])
// Allow only necessary HTTP methods
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::OPTIONS,
])
// Allow common headers needed for API requests
.allow_headers([
axum::http::header::AUTHORIZATION,
@@ -360,8 +396,9 @@ async fn main() -> Result<()> {
// Use into_make_service_with_connect_info to enable IP address extraction
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>()
).await?;
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}
@@ -371,9 +408,7 @@ async fn health() -> &'static str {
}
/// Prometheus metrics endpoint
async fn prometheus_metrics(
State(state): State<AppState>,
) -> String {
async fn prometheus_metrics(State(state): State<AppState>) -> String {
use prometheus_client::encoding::text::encode;
let registry = state.registry.lock().unwrap();
@@ -385,7 +420,7 @@ async fn prometheus_metrics(
// Support code API handlers
async fn create_code(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Json(request): Json<CreateCodeRequest>,
) -> Json<SupportCode> {
@@ -395,7 +430,7 @@ async fn create_code(
}
async fn list_codes(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
) -> Json<Vec<SupportCode>> {
Json(state.support_codes.list_active_codes().await)
@@ -414,7 +449,7 @@ async fn validate_code(
}
async fn cancel_code(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(code): Path<String>,
) -> impl IntoResponse {
@@ -428,7 +463,7 @@ async fn cancel_code(
// Session API handlers (updated to use AppState)
async fn list_sessions(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
) -> Json<Vec<api::SessionInfo>> {
let sessions = state.sessions.list_sessions().await;
@@ -436,21 +471,24 @@ async fn list_sessions(
}
async fn get_session(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
let session_id = uuid::Uuid::parse_str(&id)
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;
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
let session = state
.sessions
.get_session(session_id)
.await
.ok_or((StatusCode::NOT_FOUND, "Session not found"))?;
Ok(Json(api::SessionInfo::from(session)))
}
async fn disconnect_session(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
@@ -459,7 +497,11 @@ async fn disconnect_session(
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
};
if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
if state
.sessions
.disconnect_session(session_id, "Disconnected by administrator")
.await
{
info!("Session {} disconnected by admin", session_id);
(StatusCode::OK, "Session disconnected")
} else {
@@ -470,27 +512,35 @@ async fn disconnect_session(
// Machine API handlers
async fn list_machines(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
) -> Result<Json<Vec<api::MachineInfo>>, (StatusCode, &'static str)> {
let db = state.db.as_ref()
let db = state
.db
.as_ref()
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
let machines = db::machines::get_all_machines(db.pool()).await
let machines = db::machines::get_all_machines(db.pool())
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
Ok(Json(machines.into_iter().map(api::MachineInfo::from).collect()))
Ok(Json(
machines.into_iter().map(api::MachineInfo::from).collect(),
))
}
async fn get_machine(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(agent_id): Path<String>,
) -> Result<Json<api::MachineInfo>, (StatusCode, &'static str)> {
let db = state.db.as_ref()
let db = state
.db
.as_ref()
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
@@ -498,24 +548,29 @@ async fn get_machine(
}
async fn get_machine_history(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(agent_id): Path<String>,
) -> Result<Json<api::MachineHistory>, (StatusCode, &'static str)> {
let db = state.db.as_ref()
let db = state
.db
.as_ref()
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
// Get machine
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
// Get sessions for this machine
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
// Get events for this machine
let events = db::events::get_events_for_machine(db.pool(), machine.id).await
let events = db::events::get_events_for_machine(db.pool(), machine.id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
let history = api::MachineHistory {
@@ -529,24 +584,29 @@ async fn get_machine_history(
}
async fn delete_machine(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(agent_id): Path<String>,
Query(params): Query<api::DeleteMachineParams>,
) -> Result<Json<api::DeleteMachineResponse>, (StatusCode, &'static str)> {
let db = state.db.as_ref()
let db = state
.db
.as_ref()
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
// Get machine first
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
// Export history if requested
let history = if params.export {
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
let events = db::events::get_events_for_machine(db.pool(), machine.id).await
let events = db::events::get_events_for_machine(db.pool(), machine.id)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
Some(api::MachineHistory {
@@ -565,11 +625,14 @@ async fn delete_machine(
// Find session for this agent
if let Some(session) = state.sessions.get_session_by_agent(&agent_id).await {
if session.is_online {
uninstall_sent = state.sessions.send_admin_command(
session.id,
proto::AdminCommandType::AdminUninstall,
"Deleted by administrator",
).await;
uninstall_sent = state
.sessions
.send_admin_command(
session.id,
proto::AdminCommandType::AdminUninstall,
"Deleted by administrator",
)
.await;
if uninstall_sent {
info!("Sent uninstall command to agent {}", agent_id);
}
@@ -581,10 +644,19 @@ async fn delete_machine(
state.sessions.remove_agent(&agent_id).await;
// Delete from database (cascades to sessions and events)
db::machines::delete_machine(db.pool(), &agent_id).await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to delete machine"))?;
db::machines::delete_machine(db.pool(), &agent_id)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to delete machine",
)
})?;
info!("Deleted machine {} (uninstall_sent: {})", agent_id, uninstall_sent);
info!(
"Deleted machine {} (uninstall_sent: {})",
agent_id, uninstall_sent
);
Ok(Json(api::DeleteMachineResponse {
success: true,
@@ -603,27 +675,34 @@ struct TriggerUpdateRequest {
/// Trigger update on a specific machine
async fn trigger_machine_update(
_user: AuthenticatedUser, // Require authentication
_user: AuthenticatedUser, // Require authentication
State(state): State<AppState>,
Path(agent_id): Path<String>,
Json(request): Json<TriggerUpdateRequest>,
) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
let db = state.db.as_ref()
let db = state
.db
.as_ref()
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
// Get the target release (either specified or latest stable)
let release = if let Some(version) = request.version {
db::releases::get_release_by_version(db.pool(), &version).await
db::releases::get_release_by_version(db.pool(), &version)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
.ok_or((StatusCode::NOT_FOUND, "Release version not found"))?
} else {
db::releases::get_latest_stable_release(db.pool()).await
db::releases::get_latest_stable_release(db.pool())
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
.ok_or((StatusCode::NOT_FOUND, "No stable release available"))?
};
// Find session for this agent
let session = state.sessions.get_session_by_agent(&agent_id).await
let session = state
.sessions
.get_session_by_agent(&agent_id)
.await
.ok_or((StatusCode::NOT_FOUND, "Agent not found or offline"))?;
if !session.is_online {
@@ -632,21 +711,31 @@ async fn trigger_machine_update(
// Send update command via WebSocket
// For now, we send admin command - later we'll include UpdateInfo in the message
let sent = state.sessions.send_admin_command(
session.id,
proto::AdminCommandType::AdminUpdate,
&format!("Update to version {}", release.version),
).await;
let sent = state
.sessions
.send_admin_command(
session.id,
proto::AdminCommandType::AdminUpdate,
&format!("Update to version {}", release.version),
)
.await;
if sent {
info!("Sent update command to agent {} (version {})", agent_id, release.version);
info!(
"Sent update command to agent {} (version {})",
agent_id, release.version
);
// Update machine update status in database
let _ = db::releases::update_machine_update_status(db.pool(), &agent_id, "downloading").await;
let _ =
db::releases::update_machine_update_status(db.pool(), &agent_id, "downloading").await;
Ok((StatusCode::OK, "Update command sent"))
} else {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to send update command"))
Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to send update command",
))
}
}