- Implementa Windows Service (raven-service) para operacoes privilegiadas - Comunicacao via Named Pipes sem necessidade de UAC adicional - Adiciona single-instance para evitar multiplos icones na bandeja - Corrige todos os warnings do clippy (rustdesk, lib, usb_control, agent) - Remove fallback de elevacao para evitar UAC desnecessario - USB Policy e RustDesk provisioning agora usam o servico quando disponivel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
846 lines
25 KiB
Rust
846 lines
25 KiB
Rust
//! Modulo RustDesk - Provisionamento e gerenciamento do RustDesk
|
|
//!
|
|
//! Gerencia a instalacao, configuracao e provisionamento do RustDesk.
|
|
//! Como o servico roda como LocalSystem, nao precisa de elevacao.
|
|
|
|
use chrono::Utc;
|
|
use once_cell::sync::Lazy;
|
|
use parking_lot::Mutex;
|
|
use reqwest::blocking::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Digest, Sha256};
|
|
use std::env;
|
|
use std::ffi::OsStr;
|
|
use std::fs::{self, File, OpenOptions};
|
|
use std::io::{self, Write};
|
|
use std::os::windows::process::CommandExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Stdio};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use thiserror::Error;
|
|
use tracing::{error, info, warn};
|
|
|
|
const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest";
|
|
const USER_AGENT: &str = "RavenService/1.0";
|
|
const SERVER_HOST: &str = "rust.rever.com.br";
|
|
const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI=";
|
|
const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI<b*34Vmx_8P";
|
|
const SERVICE_NAME: &str = "RustDesk";
|
|
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
|
|
const LOCAL_SERVICE_CONFIG: &str = r"C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config";
|
|
const LOCAL_SYSTEM_CONFIG: &str = r"C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config";
|
|
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
|
|
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
static PROVISION_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum RustdeskError {
|
|
#[error("HTTP error: {0}")]
|
|
Http(#[from] reqwest::Error),
|
|
|
|
#[error("I/O error: {0}")]
|
|
Io(#[from] io::Error),
|
|
|
|
#[error("Release asset nao encontrado para Windows x86_64")]
|
|
AssetMissing,
|
|
|
|
#[error("Falha ao executar comando {command}: status {status:?}")]
|
|
CommandFailed { command: String, status: Option<i32> },
|
|
|
|
#[error("Falha ao detectar ID do RustDesk")]
|
|
MissingId,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RustdeskResult {
|
|
pub id: String,
|
|
pub password: String,
|
|
pub installed_version: Option<String>,
|
|
pub updated: bool,
|
|
pub last_provisioned_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RustdeskStatus {
|
|
pub installed: bool,
|
|
pub running: bool,
|
|
pub id: Option<String>,
|
|
pub version: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ReleaseAsset {
|
|
name: String,
|
|
browser_download_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ReleaseResponse {
|
|
tag_name: String,
|
|
assets: Vec<ReleaseAsset>,
|
|
}
|
|
|
|
/// Provisiona o RustDesk
|
|
pub fn ensure_rustdesk(
|
|
config_string: Option<&str>,
|
|
password_override: Option<&str>,
|
|
machine_id: Option<&str>,
|
|
) -> Result<RustdeskResult, RustdeskError> {
|
|
let _guard = PROVISION_MUTEX.lock();
|
|
info!("Iniciando provisionamento do RustDesk");
|
|
|
|
// Prepara ACLs dos diretorios de servico
|
|
if let Err(e) = ensure_service_profiles_writable() {
|
|
warn!("Aviso ao preparar ACL: {}", e);
|
|
}
|
|
|
|
// Le ID existente antes de qualquer limpeza
|
|
let preserved_remote_id = read_remote_id_from_profiles();
|
|
if let Some(ref id) = preserved_remote_id {
|
|
info!("ID existente preservado: {}", id);
|
|
}
|
|
|
|
let exe_path = detect_executable_path();
|
|
let (installed_version, freshly_installed) = ensure_installed(&exe_path)?;
|
|
|
|
info!(
|
|
"RustDesk {}: {}",
|
|
if freshly_installed { "instalado" } else { "ja presente" },
|
|
exe_path.display()
|
|
);
|
|
|
|
// Para processos existentes
|
|
let _ = stop_rustdesk_processes();
|
|
|
|
// Limpa perfis apenas se instalacao fresca
|
|
if freshly_installed {
|
|
let _ = purge_existing_rustdesk_profiles();
|
|
}
|
|
|
|
// Aplica configuracao
|
|
if let Some(config) = config_string.filter(|c| !c.trim().is_empty()) {
|
|
if let Err(e) = run_with_args(&exe_path, &["--config", config]) {
|
|
warn!("Falha ao aplicar config inline: {}", e);
|
|
}
|
|
} else {
|
|
let config_path = write_config_files()?;
|
|
if let Err(e) = apply_config(&exe_path, &config_path) {
|
|
warn!("Falha ao aplicar config via CLI: {}", e);
|
|
}
|
|
}
|
|
|
|
// Define senha
|
|
let password = password_override
|
|
.map(|v| v.trim().to_string())
|
|
.filter(|v| !v.is_empty())
|
|
.unwrap_or_else(|| DEFAULT_PASSWORD.to_string());
|
|
|
|
if let Err(e) = set_password(&exe_path, &password) {
|
|
warn!("Falha ao definir senha: {}", e);
|
|
} else {
|
|
let _ = ensure_password_files(&password);
|
|
let _ = propagate_password_profile();
|
|
}
|
|
|
|
// Define ID customizado
|
|
let custom_id = if let Some(ref existing_id) = preserved_remote_id {
|
|
if !freshly_installed {
|
|
Some(existing_id.clone())
|
|
} else {
|
|
define_custom_id(&exe_path, machine_id)
|
|
}
|
|
} else {
|
|
define_custom_id(&exe_path, machine_id)
|
|
};
|
|
|
|
// Inicia servico
|
|
if let Err(e) = ensure_service_running(&exe_path) {
|
|
warn!("Falha ao iniciar servico: {}", e);
|
|
}
|
|
|
|
// Obtem ID final
|
|
let final_id = match query_id_with_retries(&exe_path, 5) {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
read_remote_id_from_profiles()
|
|
.or_else(|| custom_id.clone())
|
|
.ok_or(RustdeskError::MissingId)?
|
|
}
|
|
};
|
|
|
|
// Garante ID em todos os arquivos
|
|
ensure_remote_id_files(&final_id);
|
|
|
|
let version = query_version(&exe_path).ok().or(installed_version);
|
|
let last_provisioned_at = Utc::now().timestamp_millis();
|
|
|
|
info!("Provisionamento concluido. ID: {}, Versao: {:?}", final_id, version);
|
|
|
|
Ok(RustdeskResult {
|
|
id: final_id,
|
|
password,
|
|
installed_version: version,
|
|
updated: freshly_installed,
|
|
last_provisioned_at,
|
|
})
|
|
}
|
|
|
|
/// Retorna status do RustDesk
|
|
pub fn get_status() -> Result<RustdeskStatus, RustdeskError> {
|
|
let exe_path = detect_executable_path();
|
|
let installed = exe_path.exists();
|
|
|
|
let running = if installed {
|
|
query_service_state().map(|s| s == "running").unwrap_or(false)
|
|
} else {
|
|
false
|
|
};
|
|
|
|
let id = if installed {
|
|
query_id(&exe_path).ok().or_else(read_remote_id_from_profiles)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let version = if installed {
|
|
query_version(&exe_path).ok()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(RustdeskStatus {
|
|
installed,
|
|
running,
|
|
id,
|
|
version,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Funcoes Auxiliares
|
|
// =============================================================================
|
|
|
|
fn detect_executable_path() -> PathBuf {
|
|
let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string());
|
|
Path::new(&program_files).join("RustDesk").join("rustdesk.exe")
|
|
}
|
|
|
|
fn ensure_installed(exe_path: &Path) -> Result<(Option<String>, bool), RustdeskError> {
|
|
if exe_path.exists() {
|
|
return Ok((None, false));
|
|
}
|
|
|
|
let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
|
|
.join(CACHE_DIR_NAME);
|
|
fs::create_dir_all(&cache_root)?;
|
|
|
|
let (installer_path, version_tag) = download_latest_installer(&cache_root)?;
|
|
run_installer(&installer_path)?;
|
|
thread::sleep(Duration::from_secs(20));
|
|
|
|
Ok((Some(version_tag), true))
|
|
}
|
|
|
|
fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> {
|
|
let client = Client::builder()
|
|
.user_agent(USER_AGENT)
|
|
.timeout(Duration::from_secs(60))
|
|
.build()?;
|
|
|
|
let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?;
|
|
|
|
let asset = release
|
|
.assets
|
|
.iter()
|
|
.find(|a| a.name.ends_with("x86_64.exe"))
|
|
.ok_or(RustdeskError::AssetMissing)?;
|
|
|
|
let target_path = cache_root.join(&asset.name);
|
|
if target_path.exists() {
|
|
return Ok((target_path, release.tag_name));
|
|
}
|
|
|
|
info!("Baixando RustDesk: {}", asset.name);
|
|
let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?;
|
|
let mut output = File::create(&target_path)?;
|
|
response.copy_to(&mut output)?;
|
|
|
|
Ok((target_path, release.tag_name))
|
|
}
|
|
|
|
fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> {
|
|
let status = hidden_command(installer_path)
|
|
.arg("--silent-install")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
return Err(RustdeskError::CommandFailed {
|
|
command: format!("{} --silent-install", installer_path.display()),
|
|
status: status.code(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn program_data_config_dir() -> PathBuf {
|
|
PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
|
|
.join("RustDesk")
|
|
.join("config")
|
|
}
|
|
|
|
/// Retorna todos os diretorios AppData\Roaming\RustDesk\config de usuarios do sistema
|
|
/// Como o servico roda como LocalSystem, precisamos enumerar os profiles de usuarios
|
|
fn all_user_appdata_config_dirs() -> Vec<PathBuf> {
|
|
let mut dirs = Vec::new();
|
|
|
|
// Enumera C:\Users\*\AppData\Roaming\RustDesk\config
|
|
let users_dir = Path::new("C:\\Users");
|
|
if let Ok(entries) = fs::read_dir(users_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
// Ignora pastas de sistema
|
|
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
if name == "Public" || name == "Default" || name == "Default User" || name == "All Users" {
|
|
continue;
|
|
}
|
|
let rustdesk_config = path.join("AppData").join("Roaming").join("RustDesk").join("config");
|
|
// Verifica se o diretorio pai existe (usuario real)
|
|
if path.join("AppData").join("Roaming").exists() {
|
|
dirs.push(rustdesk_config);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tambem tenta o APPDATA do ambiente (pode ser util em alguns casos)
|
|
if let Ok(appdata) = env::var("APPDATA") {
|
|
let path = Path::new(&appdata).join("RustDesk").join("config");
|
|
if !dirs.contains(&path) {
|
|
dirs.push(path);
|
|
}
|
|
}
|
|
|
|
dirs
|
|
}
|
|
|
|
fn service_profile_dirs() -> Vec<PathBuf> {
|
|
vec![
|
|
PathBuf::from(LOCAL_SERVICE_CONFIG),
|
|
PathBuf::from(LOCAL_SYSTEM_CONFIG),
|
|
]
|
|
}
|
|
|
|
fn remote_id_directories() -> Vec<PathBuf> {
|
|
let mut dirs = Vec::new();
|
|
dirs.push(program_data_config_dir());
|
|
dirs.extend(service_profile_dirs());
|
|
dirs.extend(all_user_appdata_config_dirs());
|
|
dirs
|
|
}
|
|
|
|
fn write_config_files() -> Result<PathBuf, RustdeskError> {
|
|
let config_contents = format!(
|
|
r#"[options]
|
|
key = "{key}"
|
|
relay-server = "{host}"
|
|
custom-rendezvous-server = "{host}"
|
|
api-server = "https://{host}"
|
|
verification-method = "{verification}"
|
|
approve-mode = "{approve}"
|
|
"#,
|
|
host = SERVER_HOST,
|
|
key = SERVER_KEY,
|
|
verification = SECURITY_VERIFICATION_VALUE,
|
|
approve = SECURITY_APPROVE_MODE_VALUE,
|
|
);
|
|
|
|
let main_path = program_data_config_dir().join("RustDesk2.toml");
|
|
write_file(&main_path, &config_contents)?;
|
|
|
|
for service_dir in service_profile_dirs() {
|
|
let service_profile = service_dir.join("RustDesk2.toml");
|
|
let _ = write_file(&service_profile, &config_contents);
|
|
}
|
|
|
|
Ok(main_path)
|
|
}
|
|
|
|
fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
file.write_all(contents.as_bytes())
|
|
}
|
|
|
|
fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> {
|
|
run_with_args(exe_path, &["--import-config", &config_path.to_string_lossy()])
|
|
}
|
|
|
|
fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> {
|
|
run_with_args(exe_path, &["--password", secret])
|
|
}
|
|
|
|
fn define_custom_id(exe_path: &Path, machine_id: Option<&str>) -> Option<String> {
|
|
let value = machine_id.and_then(|raw| {
|
|
let trimmed = raw.trim();
|
|
if trimmed.is_empty() { None } else { Some(trimmed) }
|
|
})?;
|
|
|
|
let custom_id = derive_numeric_id(value);
|
|
if run_with_args(exe_path, &["--set-id", &custom_id]).is_ok() {
|
|
info!("ID deterministico definido: {}", custom_id);
|
|
Some(custom_id)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn derive_numeric_id(machine_id: &str) -> String {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(machine_id.as_bytes());
|
|
let hash = hasher.finalize();
|
|
let mut bytes = [0u8; 8];
|
|
bytes.copy_from_slice(&hash[..8]);
|
|
let value = u64::from_le_bytes(bytes);
|
|
let num = (value % 900_000_000) + 100_000_000;
|
|
format!("{:09}", num)
|
|
}
|
|
|
|
fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
|
|
ensure_service_installed(exe_path)?;
|
|
let _ = run_sc(&["config", SERVICE_NAME, "start=", "auto"]);
|
|
let _ = run_sc(&["start", SERVICE_NAME]);
|
|
remove_rustdesk_autorun_artifacts();
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> {
|
|
if run_sc(&["query", SERVICE_NAME]).is_ok() {
|
|
return Ok(());
|
|
}
|
|
run_with_args(exe_path, &["--install-service"])
|
|
}
|
|
|
|
fn stop_rustdesk_processes() -> Result<(), RustdeskError> {
|
|
let _ = run_sc(&["stop", SERVICE_NAME]);
|
|
thread::sleep(Duration::from_secs(2));
|
|
|
|
let status = hidden_command("taskkill")
|
|
.args(["/F", "/T", "/IM", "rustdesk.exe"])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()?;
|
|
|
|
if status.success() || matches!(status.code(), Some(128)) {
|
|
Ok(())
|
|
} else {
|
|
Err(RustdeskError::CommandFailed {
|
|
command: "taskkill".into(),
|
|
status: status.code(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn purge_existing_rustdesk_profiles() -> Result<(), String> {
|
|
let files = [
|
|
"RustDesk.toml",
|
|
"RustDesk_local.toml",
|
|
"RustDesk2.toml",
|
|
"password",
|
|
"passwd",
|
|
"passwd.txt",
|
|
];
|
|
|
|
for dir in remote_id_directories() {
|
|
if !dir.exists() {
|
|
continue;
|
|
}
|
|
for name in files {
|
|
let path = dir.join(name);
|
|
if path.exists() {
|
|
let _ = fs::remove_file(&path);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_password_files(secret: &str) -> Result<(), String> {
|
|
for dir in remote_id_directories() {
|
|
let password_path = dir.join("RustDesk.toml");
|
|
let _ = write_toml_kv(&password_path, "password", secret);
|
|
|
|
let local_path = dir.join("RustDesk_local.toml");
|
|
let _ = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE);
|
|
let _ = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn propagate_password_profile() -> io::Result<bool> {
|
|
// Encontra um diretorio de usuario que tenha arquivos de config
|
|
let user_dirs = all_user_appdata_config_dirs();
|
|
let src_dir = user_dirs.iter().find(|d| d.join("RustDesk.toml").exists());
|
|
|
|
let Some(src_dir) = src_dir else {
|
|
// Se nenhum usuario tem config, usa ProgramData como fonte
|
|
let pd = program_data_config_dir();
|
|
if !pd.join("RustDesk.toml").exists() {
|
|
return Ok(false);
|
|
}
|
|
return propagate_from_dir(&pd);
|
|
};
|
|
|
|
propagate_from_dir(src_dir)
|
|
}
|
|
|
|
fn propagate_from_dir(src_dir: &Path) -> io::Result<bool> {
|
|
let propagation_files = ["RustDesk.toml", "RustDesk_local.toml", "RustDesk2.toml"];
|
|
let mut propagated = false;
|
|
|
|
for filename in propagation_files {
|
|
let src_path = src_dir.join(filename);
|
|
if !src_path.exists() {
|
|
continue;
|
|
}
|
|
|
|
for dest_root in remote_id_directories() {
|
|
if dest_root == src_dir {
|
|
continue; // Nao copiar para si mesmo
|
|
}
|
|
let target_path = dest_root.join(filename);
|
|
if copy_overwrite(&src_path, &target_path).is_ok() {
|
|
propagated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(propagated)
|
|
}
|
|
|
|
fn ensure_remote_id_files(id: &str) {
|
|
for dir in remote_id_directories() {
|
|
let path = dir.join("RustDesk_local.toml");
|
|
let _ = write_remote_id_value(&path, id);
|
|
}
|
|
}
|
|
|
|
fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let replacement = format!("remote_id = '{}'\n", id);
|
|
if let Ok(existing) = fs::read_to_string(path) {
|
|
let mut replaced = false;
|
|
let mut buffer = String::with_capacity(existing.len() + replacement.len());
|
|
for line in existing.lines() {
|
|
if line.trim_start().starts_with("remote_id") {
|
|
buffer.push_str(&replacement);
|
|
replaced = true;
|
|
} else {
|
|
buffer.push_str(line);
|
|
buffer.push('\n');
|
|
}
|
|
}
|
|
if !replaced {
|
|
buffer.push_str(&replacement);
|
|
}
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
file.write_all(buffer.as_bytes())
|
|
} else {
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
file.write_all(replacement.as_bytes())
|
|
}
|
|
}
|
|
|
|
fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let sanitized = value.replace('\\', "\\\\").replace('"', "\\\"");
|
|
let replacement = format!("{key} = \"{sanitized}\"\n");
|
|
let existing = fs::read_to_string(path).unwrap_or_default();
|
|
let mut replaced = false;
|
|
let mut buffer = String::with_capacity(existing.len() + replacement.len());
|
|
for line in existing.lines() {
|
|
let trimmed = line.trim_start();
|
|
if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) {
|
|
buffer.push_str(&replacement);
|
|
replaced = true;
|
|
} else {
|
|
buffer.push_str(line);
|
|
buffer.push('\n');
|
|
}
|
|
}
|
|
if !replaced {
|
|
buffer.push_str(&replacement);
|
|
}
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(path)?;
|
|
file.write_all(buffer.as_bytes())
|
|
}
|
|
|
|
fn read_remote_id_from_profiles() -> Option<String> {
|
|
for dir in remote_id_directories() {
|
|
for candidate in [dir.join("RustDesk_local.toml"), dir.join("RustDesk.toml")] {
|
|
if let Some(id) = read_remote_id_file(&candidate) {
|
|
if !id.is_empty() {
|
|
return Some(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn read_remote_id_file(path: &Path) -> Option<String> {
|
|
let content = fs::read_to_string(path).ok()?;
|
|
for line in content.lines() {
|
|
if let Some(value) = parse_assignment(line, "remote_id") {
|
|
return Some(value);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn parse_assignment(line: &str, key: &str) -> Option<String> {
|
|
let trimmed = line.trim();
|
|
if !trimmed.starts_with(key) {
|
|
return None;
|
|
}
|
|
let (_, rhs) = trimmed.split_once('=')?;
|
|
let value = rhs.trim().trim_matches(|c| c == '\'' || c == '"');
|
|
if value.is_empty() {
|
|
None
|
|
} else {
|
|
Some(value.to_string())
|
|
}
|
|
}
|
|
|
|
fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result<String, RustdeskError> {
|
|
for attempt in 0..attempts {
|
|
match query_id(exe_path) {
|
|
Ok(value) if !value.trim().is_empty() => return Ok(value),
|
|
_ => {}
|
|
}
|
|
if attempt + 1 < attempts {
|
|
thread::sleep(Duration::from_millis(800));
|
|
}
|
|
}
|
|
Err(RustdeskError::MissingId)
|
|
}
|
|
|
|
fn query_id(exe_path: &Path) -> Result<String, RustdeskError> {
|
|
let output = hidden_command(exe_path).arg("--get-id").output()?;
|
|
if !output.status.success() {
|
|
return Err(RustdeskError::CommandFailed {
|
|
command: format!("{} --get-id", exe_path.display()),
|
|
status: output.status.code(),
|
|
});
|
|
}
|
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
if stdout.is_empty() {
|
|
return Err(RustdeskError::MissingId);
|
|
}
|
|
Ok(stdout)
|
|
}
|
|
|
|
fn query_version(exe_path: &Path) -> Result<String, RustdeskError> {
|
|
let output = hidden_command(exe_path).arg("--version").output()?;
|
|
if !output.status.success() {
|
|
return Err(RustdeskError::CommandFailed {
|
|
command: format!("{} --version", exe_path.display()),
|
|
status: output.status.code(),
|
|
});
|
|
}
|
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
}
|
|
|
|
fn query_service_state() -> Option<String> {
|
|
let output = hidden_command("sc")
|
|
.args(["query", SERVICE_NAME])
|
|
.output()
|
|
.ok()?;
|
|
|
|
if !output.status.success() {
|
|
return None;
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for line in stdout.lines() {
|
|
let lower = line.to_lowercase();
|
|
if lower.contains("running") {
|
|
return Some("running".to_string());
|
|
}
|
|
if lower.contains("stopped") {
|
|
return Some("stopped".to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
|
|
let status = hidden_command("sc")
|
|
.args(args)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err(RustdeskError::CommandFailed {
|
|
command: format!("sc {}", args.join(" ")),
|
|
status: status.code(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> {
|
|
let status = hidden_command(exe_path)
|
|
.args(args)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err(RustdeskError::CommandFailed {
|
|
command: format!("{} {}", exe_path.display(), args.join(" ")),
|
|
status: status.code(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_rustdesk_autorun_artifacts() {
|
|
// Remove atalhos de inicializacao automatica
|
|
let mut startup_paths: Vec<PathBuf> = Vec::new();
|
|
if let Ok(appdata) = env::var("APPDATA") {
|
|
startup_paths.push(
|
|
Path::new(&appdata)
|
|
.join("Microsoft\\Windows\\Start Menu\\Programs\\Startup\\RustDesk.lnk"),
|
|
);
|
|
}
|
|
startup_paths.push(PathBuf::from(
|
|
r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\RustDesk.lnk",
|
|
));
|
|
|
|
for path in startup_paths {
|
|
if path.exists() {
|
|
let _ = fs::remove_file(&path);
|
|
}
|
|
}
|
|
|
|
// Remove entradas de registro
|
|
for hive in ["HKCU", "HKLM"] {
|
|
let reg_path = format!(r"{}\Software\Microsoft\Windows\CurrentVersion\Run", hive);
|
|
let _ = hidden_command("reg")
|
|
.args(["delete", ®_path, "/v", "RustDesk", "/f"])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
}
|
|
}
|
|
|
|
fn ensure_service_profiles_writable() -> Result<(), String> {
|
|
for dir in service_profile_dirs() {
|
|
if !can_write_dir(&dir) {
|
|
fix_profile_acl(&dir)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn can_write_dir(dir: &Path) -> bool {
|
|
if fs::create_dir_all(dir).is_err() {
|
|
return false;
|
|
}
|
|
let probe = dir.join(".raven_acl_probe");
|
|
match OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(&probe)
|
|
{
|
|
Ok(mut file) => {
|
|
if file.write_all(b"ok").is_err() {
|
|
let _ = fs::remove_file(&probe);
|
|
return false;
|
|
}
|
|
let _ = fs::remove_file(&probe);
|
|
true
|
|
}
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
fn fix_profile_acl(target: &Path) -> Result<(), String> {
|
|
let target_str = target.display().to_string();
|
|
|
|
// Como ja estamos rodando como LocalSystem, podemos usar takeown/icacls diretamente
|
|
let _ = hidden_command("takeown")
|
|
.args(["/F", &target_str, "/R", "/D", "Y"])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
|
|
let status = hidden_command("icacls")
|
|
.args([
|
|
&target_str,
|
|
"/grant",
|
|
"*S-1-5-32-544:(OI)(CI)F",
|
|
"*S-1-5-19:(OI)(CI)F",
|
|
"*S-1-5-32-545:(OI)(CI)M",
|
|
"/T",
|
|
"/C",
|
|
"/Q",
|
|
])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.map_err(|e| format!("Erro ao executar icacls: {}", e))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("icacls retornou codigo {}", status.code().unwrap_or(-1)))
|
|
}
|
|
}
|
|
|
|
fn copy_overwrite(src: &Path, dst: &Path) -> io::Result<()> {
|
|
if let Some(parent) = dst.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
if dst.is_dir() {
|
|
fs::remove_dir_all(dst)?;
|
|
} else if dst.exists() {
|
|
fs::remove_file(dst)?;
|
|
}
|
|
fs::copy(src, dst)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn hidden_command(program: impl AsRef<OsStr>) -> Command {
|
|
let mut cmd = Command::new(program);
|
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
cmd
|
|
}
|