From f7d29a98d30c1173eb1534e01075d804a6343dd0 Mon Sep 17 00:00:00 2001 From: Helios Agent Date: Tue, 3 Mar 2026 14:29:22 +0100 Subject: [PATCH] feat: commit hash in banner, version command, file upload/download --- crates/client/build.rs | 15 +++-- crates/client/src/main.rs | 50 ++++++++++++++++- crates/common/src/protocol.rs | 25 +++++++++ crates/server/build.rs | 11 ++++ crates/server/src/api.rs | 99 ++++++++++++++++++++++++++++++++- crates/server/src/main.rs | 7 +++ crates/server/src/ws_handler.rs | 2 + scripts/install.ps1 | 2 +- 8 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 crates/server/build.rs diff --git a/crates/client/build.rs b/crates/client/build.rs index d8cbd5f..6754ee2 100644 --- a/crates/client/build.rs +++ b/crates/client/build.rs @@ -1,8 +1,11 @@ fn main() { - #[cfg(target_os = "windows")] - { - let mut res = winres::WindowsResource::new(); - res.set_icon("../../assets/logo.ico"); - res.compile().expect("Failed to compile Windows resources"); - } + let hash = std::process::Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .unwrap_or_default(); + let hash = hash.trim(); + println!("cargo:rustc-env=GIT_COMMIT={}", if hash.is_empty() { "unknown" } else { hash }); + println!("cargo:rerun-if-changed=.git/HEAD"); } diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 9d26d9d..31563bb 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Connector}; +use base64::Engine; use helios_common::{ClientMessage, ServerMessage}; mod shell; @@ -20,7 +21,7 @@ mod windows_mgmt; fn banner() { println!(); - println!(" {} HELIOS REMOTE", "☀".yellow().bold()); + println!(" {} HELIOS REMOTE v{} ({})", "☀".yellow().bold(), env!("CARGO_PKG_VERSION"), env!("GIT_COMMIT")); println!(" {}", "─".repeat(45).dimmed()); } @@ -431,6 +432,53 @@ async fn handle_message( } } + ServerMessage::VersionRequest { request_id } => { + ClientMessage::VersionResponse { + request_id, + version: env!("CARGO_PKG_VERSION").to_string(), + commit: env!("GIT_COMMIT").to_string(), + } + } + + ServerMessage::UploadRequest { request_id, path, content_base64 } => { + log_cmd!("upload → {}", path); + match (|| -> Result<(), String> { + let bytes = base64::engine::general_purpose::STANDARD + .decode(&content_base64) + .map_err(|e| format!("base64 decode: {e}"))?; + if let Some(parent) = std::path::Path::new(&path).parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + std::fs::write(&path, &bytes).map_err(|e| e.to_string())?; + Ok(()) + })() { + Ok(()) => { + log_ok!("Saved {}", path); + ClientMessage::Ack { request_id } + } + Err(e) => { + log_err!("upload failed: {e}"); + ClientMessage::Error { request_id, message: e } + } + } + } + + ServerMessage::DownloadRequest { request_id, path } => { + log_cmd!("download ← {}", path); + match std::fs::read(&path) { + Ok(bytes) => { + let size = bytes.len() as u64; + let content_base64 = base64::engine::general_purpose::STANDARD.encode(&bytes); + log_ok!("Sent {} bytes", size); + ClientMessage::DownloadResponse { request_id, content_base64, size } + } + Err(e) => { + log_err!("download failed: {e}"); + ClientMessage::Error { request_id, message: format!("Read failed: {e}") } + } + } + } + ServerMessage::Ack { request_id } => { ClientMessage::Ack { request_id } } diff --git a/crates/common/src/protocol.rs b/crates/common/src/protocol.rs index 3f9a8f9..ffb4a22 100644 --- a/crates/common/src/protocol.rs +++ b/crates/common/src/protocol.rs @@ -47,6 +47,19 @@ pub enum ServerMessage { FocusWindowRequest { request_id: Uuid, window_id: u64 }, /// Maximize a window and bring it to the foreground MaximizeAndFocusRequest { request_id: Uuid, window_id: u64 }, + /// Request client version info + VersionRequest { request_id: Uuid }, + /// Upload a file to the client + UploadRequest { + request_id: Uuid, + path: String, + content_base64: String, + }, + /// Download a file from the client + DownloadRequest { + request_id: Uuid, + path: String, + }, } /// Messages sent from the client to the relay server @@ -81,6 +94,18 @@ pub enum ClientMessage { request_id: Uuid, windows: Vec, }, + /// Response to a version request + VersionResponse { + request_id: Uuid, + version: String, + commit: String, + }, + /// Response to a download request + DownloadResponse { + request_id: Uuid, + content_base64: String, + size: u64, + }, } /// Mouse button variants diff --git a/crates/server/build.rs b/crates/server/build.rs new file mode 100644 index 0000000..6754ee2 --- /dev/null +++ b/crates/server/build.rs @@ -0,0 +1,11 @@ +fn main() { + let hash = std::process::Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .unwrap_or_default(); + let hash = hash.trim(); + println!("cargo:rustc-env=GIT_COMMIT={}", if hash.is_empty() { "unknown" } else { hash }); + println!("cargo:rerun-if-changed=.git/HEAD"); +} diff --git a/crates/server/src/api.rs b/crates/server/src/api.rs index c670abc..2df3832 100644 --- a/crates/server/src/api.rs +++ b/crates/server/src/api.rs @@ -1,6 +1,6 @@ use std::time::Duration; use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json, @@ -307,6 +307,103 @@ pub async fn maximize_and_focus( } } +/// GET /version — server version (public, no auth) +pub async fn server_version() -> impl IntoResponse { + Json(serde_json::json!({ + "version": env!("CARGO_PKG_VERSION"), + "commit": env!("GIT_COMMIT"), + })) +} + +/// GET /sessions/:id/version — client version +pub async fn client_version( + Path(session_id): Path, + State(state): State, +) -> impl IntoResponse { + match dispatch(&state, &session_id, "version", |rid| { + ServerMessage::VersionRequest { request_id: rid } + }) + .await + { + Ok(ClientMessage::VersionResponse { version, commit, .. }) => ( + StatusCode::OK, + Json(serde_json::json!({ "version": version, "commit": commit })), + ) + .into_response(), + Ok(ClientMessage::Error { message, .. }) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": message })), + ) + .into_response(), + Ok(_) => ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({ "error": "Unexpected response from client" })), + ) + .into_response(), + Err(e) => e.into_response(), + } +} + +/// POST /sessions/:id/upload +#[derive(Deserialize)] +pub struct UploadBody { + pub path: String, + pub content_base64: String, +} + +pub async fn upload_file( + Path(session_id): Path, + State(state): State, + Json(body): Json, +) -> impl IntoResponse { + match dispatch(&state, &session_id, "upload", |rid| ServerMessage::UploadRequest { + request_id: rid, + path: body.path.clone(), + content_base64: body.content_base64.clone(), + }) + .await + { + Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "ok": true }))).into_response(), + Err(e) => e.into_response(), + } +} + +/// GET /sessions/:id/download?path=... +#[derive(Deserialize)] +pub struct DownloadQuery { + pub path: String, +} + +pub async fn download_file( + Path(session_id): Path, + State(state): State, + Query(query): Query, +) -> impl IntoResponse { + match dispatch(&state, &session_id, "download", |rid| ServerMessage::DownloadRequest { + request_id: rid, + path: query.path.clone(), + }) + .await + { + Ok(ClientMessage::DownloadResponse { content_base64, size, .. }) => ( + StatusCode::OK, + Json(serde_json::json!({ "content_base64": content_base64, "size": size })), + ) + .into_response(), + Ok(ClientMessage::Error { message, .. }) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": message })), + ) + .into_response(), + Ok(_) => ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({ "error": "Unexpected response from client" })), + ) + .into_response(), + Err(e) => e.into_response(), + } +} + /// POST /sessions/:id/label #[derive(Deserialize)] pub struct LabelBody { diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 9e88a6c..3024d4f 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -30,6 +30,9 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + const GIT_COMMIT: &str = env!("GIT_COMMIT"); + info!("helios-server v{} ({})", env!("CARGO_PKG_VERSION"), GIT_COMMIT); + let api_key = std::env::var("HELIOS_API_KEY") .unwrap_or_else(|_| "dev-secret".to_string()); @@ -52,10 +55,14 @@ async fn main() -> anyhow::Result<()> { .route("/sessions/:id/windows/minimize-all", post(api::minimize_all)) .route("/sessions/:id/windows/:window_id/focus", post(api::focus_window)) .route("/sessions/:id/windows/:window_id/maximize", post(api::maximize_and_focus)) + .route("/sessions/:id/version", get(api::client_version)) + .route("/sessions/:id/upload", post(api::upload_file)) + .route("/sessions/:id/download", get(api::download_file)) .layer(middleware::from_fn_with_state(state.clone(), require_api_key)); let app = Router::new() .route("/ws", get(ws_handler::ws_upgrade)) + .route("/version", get(api::server_version)) .merge(protected) .with_state(state); diff --git a/crates/server/src/ws_handler.rs b/crates/server/src/ws_handler.rs index a5ad66e..c44ac8d 100644 --- a/crates/server/src/ws_handler.rs +++ b/crates/server/src/ws_handler.rs @@ -89,6 +89,8 @@ async fn handle_client_message(session_id: Uuid, msg: ClientMessage, state: &App ClientMessage::ScreenshotResponse { request_id, .. } | ClientMessage::ExecResponse { request_id, .. } | ClientMessage::ListWindowsResponse { request_id, .. } + | ClientMessage::VersionResponse { request_id, .. } + | ClientMessage::DownloadResponse { request_id, .. } | ClientMessage::Ack { request_id } | ClientMessage::Error { request_id, .. } => { let rid = *request_id; diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b4cf0cf..aad773d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -9,7 +9,7 @@ $ErrorActionPreference = "Stop" -$url = "https://github.com/agent-helios/helios-remote/releases/latest/download/helios-remote-client-windows.exe" +$url = "https://agent-helios.me/downloads/helios-remote/helios-remote-client-windows.exe" $dest = "$env:USERPROFILE\Desktop\Helios Remote.exe" Write-Host "Downloading helios-remote client..."