use crate::RustdeskProvisioningResult; use chrono::{Local, Utc}; use once_cell::sync::Lazy; use parking_lot::Mutex; use reqwest::blocking::Client; use serde::Deserialize; use serde_json::{Map as JsonMap, Value as JsonValue}; use sha2::{Digest, Sha256}; use std::env; use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::thread; use std::time::Duration; use thiserror::Error; use std::os::windows::process::CommandExt; const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest"; const USER_AGENT: &str = "RavenDesktop/1.0"; const SERVER_HOST: &str = "rust.rever.com.br"; const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI="; const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI> = 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 não encontrado para Windows x86_64")] AssetMissing, #[error("Falha ao executar comando {command}: status {status:?}")] CommandFailed { command: String, status: Option }, #[error("Falha ao detectar ID do RustDesk")] MissingId, } #[derive(Debug, Deserialize)] struct ReleaseAsset { name: String, browser_download_url: String, } #[derive(Debug, Deserialize)] struct ReleaseResponse { tag_name: String, assets: Vec, } /// Auxiliar para definir ID customizado baseado no machine_id fn define_custom_id_from_machine(exe_path: &Path, machine_id: Option<&str>) -> Option { if let Some(value) = machine_id.and_then(|raw| { let trimmed = raw.trim(); if trimmed.is_empty() { None } else { Some(trimmed) } }) { match set_custom_id(exe_path, value) { Ok(custom) => { log_event(format!("ID determinístico definido: {custom}")); Some(custom) } Err(error) => { log_event(format!("Falha ao definir ID determinístico: {error}")); None } } } else { None } } pub fn ensure_rustdesk( config_string: Option<&str>, password_override: Option<&str>, machine_id: Option<&str>, ) -> Result { let _guard = PROVISION_MUTEX.lock(); log_event("Iniciando preparo do RustDesk"); if let Err(error) = ensure_service_profiles_writable_preflight() { log_event(format!( "Aviso: não foi possível preparar ACL dos perfis do serviço ({error}). Continuando mesmo assim; o serviço pode não aplicar a senha." )); } // IMPORTANTE: Ler o ID existente ANTES de qualquer limpeza // Isso preserva o ID quando o Raven é reinstalado mas o RustDesk permanece let preserved_remote_id = read_remote_id_from_profiles(); if let Some(ref id) = preserved_remote_id { log_event(format!("ID existente preservado antes da limpeza: {}", id)); } let exe_path = detect_executable_path(); let (installed_version, freshly_installed) = ensure_installed(&exe_path)?; log_event(if freshly_installed { "RustDesk instalado a partir do instalador mais recente" } else { "RustDesk já instalado, usando binário existente" }); match stop_rustdesk_processes() { Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"), Err(error) => log_event(format!( "Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})" )), } // So limpa perfis se for instalacao fresca (RustDesk nao existia) // Se ja existia, preservamos o ID para manter consistencia if freshly_installed { match purge_existing_rustdesk_profiles() { Ok(_) => log_event("Configurações antigas do RustDesk limpas (instalação fresca)"), Err(error) => log_event(format!( "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" )), } } else { log_event("Mantendo perfis existentes do RustDesk (preservando ID)"); } if let Some(value) = config_string.and_then(|raw| { let trimmed = raw.trim(); if trimmed.is_empty() { None } else { Some(trimmed) } }) { if let Err(error) = run_with_args(&exe_path, &["--config", value]) { log_event(format!("Falha ao aplicar configuração inline: {error}")); } else { log_event("Configuração aplicada via --config"); } } else { let config_path = write_config_files()?; log_event(format!( "Arquivo de configuração atualizado em {}", config_path.display() )); if let Err(error) = apply_config(&exe_path, &config_path) { log_event(format!("Falha ao aplicar configuração via CLI: {error}")); } else { log_event("Configuração aplicada via CLI"); } } let password = password_override .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); if let Err(error) = set_password(&exe_path, &password) { log_event(format!("Falha ao definir senha padrão: {error}")); } else { log_event("Senha padrão definida com sucesso"); log_event("Aplicando senha nos perfis do RustDesk"); match ensure_password_files(&password) { Ok(_) => { log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"); log_password_replication(&password); } Err(error) => log_event(format!("Falha ao persistir senha nos perfis: {error}")), } match propagate_password_profile() { Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"), Err(error) => log_event(format!("Falha ao copiar perfil de senha: {error}")), } match replicate_password_artifacts() { Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"), Err(error) => log_event(format!("Falha ao replicar artefatos de senha: {error}")), } if let Err(error) = enforce_security_flags() { log_event(format!("Falha ao reforçar configuração de senha permanente: {error}")); } } // Se ja existe um ID preservado E o RustDesk nao foi recem-instalado, usa o ID existente // Isso garante que reinstalar o Raven nao muda o ID do RustDesk let custom_id = if let Some(ref existing_id) = preserved_remote_id { if !freshly_installed { log_event(format!("Reutilizando ID existente do RustDesk: {}", existing_id)); Some(existing_id.clone()) } else { // Instalacao fresca - define novo ID baseado no machine_id define_custom_id_from_machine(&exe_path, machine_id) } } else { // Sem ID preservado - define novo ID baseado no machine_id define_custom_id_from_machine(&exe_path, machine_id) }; if let Err(error) = ensure_service_running(&exe_path) { log_event(format!("Falha ao reiniciar serviço do RustDesk: {error}")); } else { log_event("Serviço RustDesk reiniciado/run ativo"); } let reported_id = match query_id_with_retries(&exe_path, 5) { Ok(value) => value, Err(error) => { log_event(format!("Falha ao obter ID após múltiplas tentativas: {error}")); match read_remote_id_from_profiles().or_else(|| custom_id.clone()) { Some(value) => { log_event(format!("ID obtido via arquivos de perfil: {value}")); value } None => return Err(error), } } }; let mut final_id = reported_id.clone(); if let Some(expected) = custom_id.as_ref() { if expected != &reported_id { log_event(format!( "ID retornado difere do determinístico ({expected}) -> reaplicando ID determinístico" )); let mut enforced = false; match set_custom_id(&exe_path, expected) { Ok(_) => match query_id_with_retries(&exe_path, 3) { Ok(rechecked) => { if &rechecked == expected { log_event(format!("ID determinístico aplicado com sucesso: {rechecked}")); final_id = rechecked; enforced = true; } else { log_event(format!( "ID ainda difere após reaplicação (esperado {expected}, reportado {rechecked}); usando ID reportado" )); final_id = rechecked; } } Err(error) => { log_event(format!( "Falha ao consultar ID após reaplicação: {error}; usando ID reportado ({reported_id})" )); final_id = reported_id.clone(); } }, Err(error) => { log_event(format!( "Falha ao reaplicar ID determinístico ({expected}): {error}; usando ID reportado ({reported_id})" )); final_id = reported_id.clone(); } } if !enforced && final_id != *expected { log_event("Aviso: não foi possível aplicar o ID determinístico; manteremos o ID real fornecido pelo serviço"); } } } ensure_remote_id_files(&final_id); let version = query_version(&exe_path).ok().or(installed_version); let last_provisioned_at = Utc::now().timestamp_millis(); let result = RustdeskProvisioningResult { id: final_id.clone(), password: password.clone(), installed_version: version.clone(), updated: freshly_installed, last_provisioned_at, }; // Salva os dados do RustDesk diretamente no arquivo machine-agent.json // para evitar conflitos com o Tauri Store do TypeScript let rustdesk_data = serde_json::json!({ "id": final_id, "password": password, "installedVersion": version, "updated": freshly_installed, "lastProvisionedAt": last_provisioned_at, "lastSyncedAt": serde_json::Value::Null, "lastError": serde_json::Value::Null }); if let Err(error) = upsert_machine_store_value("rustdesk", rustdesk_data) { log_event(format!("Aviso: falha ao salvar dados do RustDesk no store: {error}")); } else { log_event("Dados do RustDesk salvos no machine-agent.json"); } // Sincroniza com o backend imediatamente apos provisionar // O Rust faz o HTTP direto, sem passar pelo CSP do webview if let Err(error) = sync_remote_access_with_backend(&result) { log_event(format!("Aviso: falha ao sincronizar com backend: {error}")); } else { log_event("Acesso remoto sincronizado com backend"); // Atualiza lastSyncedAt no store let synced_data = serde_json::json!({ "id": final_id, "password": password, "installedVersion": version, "updated": freshly_installed, "lastProvisionedAt": last_provisioned_at, "lastSyncedAt": Utc::now().timestamp_millis(), "lastError": serde_json::Value::Null }); if let Err(e) = upsert_machine_store_value("rustdesk", synced_data) { log_event(format!("Aviso: falha ao atualizar lastSyncedAt: {e}")); } else { log_event("lastSyncedAt atualizado com sucesso"); } } log_event(format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); Ok(result) } 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, 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)); } 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 write_config_files() -> Result { let config_contents = build_config_contents(); let main_path = program_data_config_dir().join("RustDesk2.toml"); write_file(&main_path, &config_contents)?; log_event(format!( "Config principal gravada em {}", main_path.display() )); let _ = ensure_service_profiles_writable_preflight(); for service_dir in service_profile_dirs() { let service_profile = service_dir.join("RustDesk2.toml"); if let Err(error) = write_file(&service_profile, &config_contents) { log_event(format!( "Falha ao gravar config no perfil do serviço ({}): {error}", service_profile.display() )); } } if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") { if let Err(error) = write_file(&appdata_path, &config_contents) { log_event(format!( "Falha ao atualizar config no AppData do usuário: {error}" )); } } 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 program_data_config_dir() -> PathBuf { PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string())) .join("RustDesk") .join("config") } fn user_appdata_config_dir() -> Option { env::var("APPDATA") .ok() .map(|value| Path::new(&value).join("RustDesk").join("config")) } fn user_appdata_config_path(filename: &str) -> Option { user_appdata_config_dir().map(|dir| dir.join(filename)) } fn build_config_contents() -> String { 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, ) } fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> { let status = hidden_command(exe_path) .arg("--import-config") .arg(config_path) .stdout(Stdio::null()) .stderr(Stdio::null()) .status()?; if !status.success() { return Err(RustdeskError::CommandFailed { command: format!("{} --import-config {}", exe_path.display(), config_path.display()), status: status.code(), }); } Ok(()) } fn set_password(exe_path: &Path, secret: &str) -> Result<(), RustdeskError> { run_with_args(exe_path, &["--password", secret]) } fn set_custom_id(exe_path: &Path, machine_id: &str) -> Result { let custom_id = derive_numeric_id(machine_id); run_with_args(exe_path, &["--set-id", &custom_id])?; Ok(custom_id) } 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)?; if let Err(error) = configure_service_startup() { log_event(format!( "Aviso: não foi possível reforçar autostart/recuperação do serviço RustDesk: {error}" )); } fn start_sequence() -> Result<(), RustdeskError> { let _ = run_sc(&["stop", SERVICE_NAME]); thread::sleep(Duration::from_secs(2)); let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); run_sc(&["start", SERVICE_NAME]) } let _ = match start_sequence() { Ok(_) => Ok(()), Err(RustdeskError::CommandFailed { command: _, status: Some(5), .. }) => { log_event("SC retornou acesso negado; tentando ajustar ACL dos perfis do serviço..."); ensure_service_profiles_writable_preflight().map_err(|error| RustdeskError::CommandFailed { command: format!("fix_acl ({error})"), status: Some(5), })?; let _ = run_sc(&["stop", SERVICE_NAME]); let _ = start_sequence(); Ok(()) } Err(error) => Err(error), }; remove_rustdesk_autorun_artifacts(); // Revalida se o serviço realmente subiu; se não, reinstala e tenta novamente. match query_service_state() { Some(state) if state.eq_ignore_ascii_case("running") => Ok(()), _ => { log_event("Serviço RustDesk não está em execução após tentativa de start; reaplicando --install-service e start"); let _ = run_with_args(exe_path, &["--install-service"]); let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); if let Err(error) = start_sequence() { log_event(format!( "Falha ao subir o serviço RustDesk mesmo após reinstalação: {error}" )); } Ok(()) } } } fn configure_service_startup() -> Result<(), RustdeskError> { let start_arg = format!("start= {}", "auto"); run_sc(&["config", SERVICE_NAME, &start_arg])?; let reset_arg = format!("reset= {}", "86400"); let actions_arg = "actions= restart/5000/restart/5000/restart/5000"; let failure_actions_applied = run_sc(&["failure", SERVICE_NAME, &reset_arg, actions_arg]).is_ok(); let _ = run_sc(&["failureflag", SERVICE_NAME, "1"]); if failure_actions_applied { log_event("Serviço RustDesk configurado para reiniciar automaticamente em caso de falha"); } else { log_event("Aviso: não foi possível configurar recuperação automática do serviço RustDesk"); } Ok(()) } fn query_service_state() -> Option { 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() { if let Some(pos) = line.find("STATE") { // Example: " STATE : 4 RUNNING" let state = line[pos..].to_string(); if state.to_lowercase().contains("running") { return Some("running".to_string()); } if state.to_lowercase().contains("stopped") { return Some("stopped".to_string()); } } } None } fn remove_rustdesk_autorun_artifacts() { // Remove atalhos de inicialização automática para evitar abrir GUI a cada boot/login. let mut startup_paths: Vec = Vec::new(); if let Ok(appdata) = env::var("APPDATA") { startup_paths.push( Path::new(&appdata) .join("Microsoft") .join("Windows") .join("Start Menu") .join("Programs") .join("Startup") .join("RustDesk.lnk"), ); } startup_paths.push( Path::new("C:\\ProgramData") .join("Microsoft") .join("Windows") .join("Start Menu") .join("Programs") .join("Startup") .join("RustDesk.lnk"), ); for path in startup_paths { if path.exists() { match fs::remove_file(&path) { Ok(_) => log_event(format!("Atalho de inicialização do RustDesk removido: {}", path.display())), Err(error) => log_event(format!( "Falha ao remover atalho de inicialização do RustDesk ({}): {}", path.display(), error )), } } } for hive in ["HKCU", "HKLM"] { let reg_path = format!(r"{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", hive); let status = hidden_command("reg") .args(["delete", ®_path, "/v", "RustDesk", "/f"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); if let Ok(code) = status { if code.success() { log_event(format!("Entrada de auto-run RustDesk removida de {}", reg_path)); } } } } fn stop_rustdesk_processes() -> Result<(), RustdeskError> { if let Err(error) = try_stop_service() { log_event(format!( "Não foi possível parar o serviço RustDesk antes da sincronização: {error}" )); } 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 /F /T /IM rustdesk.exe".into(), status: status.code(), }) } } fn try_stop_service() -> Result<(), RustdeskError> { match run_sc(&["stop", SERVICE_NAME]) { Ok(_) => { thread::sleep(Duration::from_secs(2)); Ok(()) } Err(RustdeskError::CommandFailed { status: Some(code), .. }) if code == 1060 || code == 1062 => Ok(()), Err(RustdeskError::CommandFailed { status: Some(5), .. }) => { stop_service_elevated().map_err(|error| RustdeskError::CommandFailed { command: format!("stop_service_elevated ({error})"), status: Some(5), }) } Err(error) => Err(error), } } 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 ensure_service_installed(exe_path: &Path) -> Result<(), RustdeskError> { if run_sc(&["query", SERVICE_NAME]).is_ok() { return Ok(()); } log_event("Serviço RustDesk não encontrado; instalando via CLI"); run_with_args(exe_path, &["--install-service"])?; Ok(()) } fn query_id_with_retries(exe_path: &Path, attempts: usize) -> Result { let mut last_error: Option = None; for attempt in 0..attempts { match query_id(exe_path) { Ok(value) if !value.trim().is_empty() => return Ok(value), Ok(_) => { last_error = Some(RustdeskError::MissingId); } Err(error) => { last_error = Some(error); } } if attempt + 1 < attempts { thread::sleep(Duration::from_millis(800)); } } Err(last_error.unwrap_or(RustdeskError::MissingId)) } fn query_id(exe_path: &Path) -> Result { 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 { 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 ensure_remote_id_files(id: &str) { for dir in remote_id_directories() { let path = dir.join("RustDesk_local.toml"); match write_remote_id_value(&path, id) { Ok(_) => log_event(format!( "remote_id atualizado para {} em {}", id, path.display() )), Err(error) => log_event(format!( "Falha ao atualizar remote_id em {}: {error}", path.display() )), } } } fn remote_id_directories() -> Vec { let mut dirs = Vec::new(); dirs.push(program_data_config_dir()); for profile in service_profile_dirs() { dirs.push(profile); } if let Some(appdir) = user_appdata_config_dir() { dirs.push(appdir); } dirs } fn service_profile_dirs() -> Vec { vec![ PathBuf::from(LOCAL_SERVICE_CONFIG), PathBuf::from(LOCAL_SYSTEM_CONFIG), ] } fn propagation_destinations() -> Vec { let mut dirs = Vec::new(); dirs.push(program_data_config_dir()); dirs.extend(service_profile_dirs()); dirs } fn ensure_password_files(secret: &str) -> Result<(), String> { let mut errors = Vec::new(); for dir in remote_id_directories() { let password_path = dir.join("RustDesk.toml"); if let Err(error) = write_toml_kv(&password_path, "password", secret) { errors.push(format!("{} -> {}", password_path.display(), error)); } else { log_event(format!( "Senha escrita via fallback em {}", password_path.display() )); } let local_path = dir.join("RustDesk_local.toml"); if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { log_event(format!( "Falha ao ajustar verification-method em {}: {error}", local_path.display() )); } else { log_event(format!( "verification-method atualizado para {} em {}", SECURITY_VERIFICATION_VALUE, local_path.display() )); } let rustdesk2_path = dir.join("RustDesk2.toml"); if let Err(error) = enforce_security_in_rustdesk2(&rustdesk2_path) { log_event(format!( "Falha ao ajustar flags no RustDesk2.toml em {}: {error}", rustdesk2_path.display() )); } if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { log_event(format!( "Falha ao ajustar approve-mode em {}: {error}", local_path.display() )); } else { log_event(format!( "approve-mode atualizado para {} em {}", SECURITY_APPROVE_MODE_VALUE, local_path.display() )); } } if errors.is_empty() { Ok(()) } else { Err(errors.join(" | ")) } } fn enforce_security_flags() -> Result<(), String> { let mut errors = Vec::new(); for dir in remote_id_directories() { let local_path = dir.join("RustDesk_local.toml"); if let Err(error) = write_toml_kv(&local_path, "verification-method", SECURITY_VERIFICATION_VALUE) { errors.push(format!("{} -> {}", local_path.display(), error)); } else { log_event(format!( "verification-method atualizado para {} em {}", SECURITY_VERIFICATION_VALUE, local_path.display() )); } if let Err(error) = write_toml_kv(&local_path, "approve-mode", SECURITY_APPROVE_MODE_VALUE) { errors.push(format!("{} -> {}", local_path.display(), error)); } else { log_event(format!( "approve-mode atualizado para {} em {}", SECURITY_APPROVE_MODE_VALUE, local_path.display() )); } } if errors.is_empty() { Ok(()) } else { Err(errors.join(" | ")) } } fn enforce_security_in_rustdesk2(path: &Path) -> io::Result<()> { write_toml_kv(path, "verification-method", SECURITY_VERIFICATION_VALUE)?; write_toml_kv(path, "approve-mode", SECURITY_APPROVE_MODE_VALUE)?; Ok(()) } fn propagate_password_profile() -> io::Result { let Some(src_dir) = user_appdata_config_dir() else { log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)"); return Ok(false); }; let mut propagated = false; for filename in PROPAGATION_FILES { let src_path = src_dir.join(filename); if !src_path.exists() { continue; } log_event(format!( "Copiando {} para ProgramData/serviços", src_path.display() )); for dest_root in propagation_destinations() { let target_path = dest_root.join(filename); copy_overwrite(&src_path, &target_path)?; log_event(format!( "{} propagado para {}", filename, target_path.display() )); propagated = true; } } if !propagated { log_event("Nenhum arquivo de perfil encontrado para propagação; aplicando fallback"); } Ok(propagated) } fn replicate_password_artifacts() -> io::Result<()> { let Some(src) = user_appdata_config_dir() else { return Ok(()); }; let destinations = propagation_destinations(); let candidates = ["password", "passwd", "passwd.txt"]; for dest in destinations { fs::create_dir_all(&dest)?; for name in candidates { let source_path = src.join(name); if !source_path.exists() { continue; } let metadata = match fs::metadata(&source_path) { Ok(data) => data, Err(_) => continue, }; if !metadata.is_file() || metadata.len() == 0 { continue; } let target_path = dest.join(name); copy_overwrite(&source_path, &target_path)?; log_event(format!( "Artefato de senha {name} replicado para {}", target_path.display() )); } } Ok(()) } fn purge_existing_rustdesk_profiles() -> Result<(), String> { let mut errors = Vec::new(); for dir in remote_id_directories() { match purge_config_dir(&dir) { Ok(true) => { log_event(format!( "Perfis antigos removidos em {}", dir.display() )); } Ok(false) => {} Err(error) => errors.push(format!("{} -> {error}", dir.display())), } } if errors.is_empty() { Ok(()) } else { Err(errors.join(" | ")) } } fn purge_config_dir(dir: &Path) -> Result { if !dir.exists() { return Ok(false); } let mut removed = false; fs::create_dir_all(dir)?; for name in RUSTDESK_CONFIG_FILES { let path = dir.join(name); if path.is_dir() { fs::remove_dir_all(&path)?; removed = true; continue; } if path.exists() { fs::remove_file(&path)?; removed = true; } } Ok(removed) } #[allow(dead_code)] fn run_powershell_elevated(script: &str) -> Result<(), String> { let temp_dir = env::temp_dir(); let payload = temp_dir.join("raven_payload.ps1"); fs::write(&payload, script).map_err(|error| format!("write payload: {error}"))?; let launcher = temp_dir.join("raven_launcher.ps1"); let launcher_body = format!( r#" $ErrorActionPreference='Stop' $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = 'powershell.exe' $psi.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "{payload}"' $psi.Verb = 'runas' $psi.WindowStyle = 'Hidden' $process = [System.Diagnostics.Process]::Start($psi) $process.WaitForExit() exit $process.ExitCode "#, payload = payload.display() ); fs::write(&launcher, launcher_body).map_err(|error| format!("write launcher: {error}"))?; let status = Command::new("powershell") .creation_flags(CREATE_NO_WINDOW) .args([ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &launcher.to_string_lossy(), ]) .status() .map_err(|error| format!("spawn ps: {error}"))?; let _ = fs::remove_file(&launcher); let _ = fs::remove_file(&payload); if let Some(code) = status.code() { if code == 0 || code == 1 { return Ok(()); } } else if status.success() { return Ok(()); } Err(format!("elevated ps exit {:?}", status.code())) } #[allow(dead_code)] fn fix_profile_acl(target: &Path) -> Result<(), String> { let target_str = target.display().to_string(); let transcript = env::temp_dir().join("raven_acl_ps.log"); let log_str = transcript.display().to_string(); let script = format!( r#" $ErrorActionPreference='Stop' Start-Transcript -Path '{log}' -Force try {{ if (-not (Test-Path '{target}')) {{ New-Item -ItemType Directory -Force -Path '{target}' | Out-Null }} & takeown /F '{target}' /R /D Y $takeCode = $LASTEXITCODE & icacls '{target}' /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 $icaCode = $LASTEXITCODE if (($takeCode -eq 0) -and ($icaCode -in 0,1)) {{ exit 0 }} if ($icaCode -ne 0) {{ exit $icaCode }} exit $takeCode }} catch {{ Write-Host ("exception: " + ($_.Exception.Message)) exit 1 }} finally {{ try {{ Stop-Transcript | Out-Null }} catch {{ }} }} "#, target = target_str, log = log_str ); let result = run_powershell_elevated(&script); if result.is_err() { if let Ok(content) = fs::read_to_string(&transcript) { log_event(format!( "ACL transcript para {}:\n{}", target.display(), content )); } } let _ = fs::remove_file(&transcript); result } fn ensure_service_profiles_writable_preflight() -> Result<(), String> { // Verificamos se os diretorios de perfil sao graváveis // Se nao forem, apenas logamos aviso - o Raven Service deve lidar com isso // Nao usamos elevacao para evitar UAC adicional let mut blocked_dirs = Vec::new(); for dir in service_profile_dirs() { if !can_write_dir(&dir) { blocked_dirs.push(dir); } } if blocked_dirs.is_empty() { return Ok(()); } // Apenas logamos aviso - o serviço RavenService deve lidar com permissões log_event(format!( "Aviso: alguns perfis de serviço não são graváveis: {:?}. O Raven Service deve configurar permissões.", blocked_dirs.iter().map(|d| d.display().to_string()).collect::>() )); // Retornamos Ok para não bloquear o fluxo // O Raven Service, rodando como LocalSystem, pode gravar nesses diretórios Ok(()) } fn stop_service_elevated() -> Result<(), String> { // Tentamos parar o serviço RustDesk sem elevação // Se falhar, apenas logamos aviso - o Raven Service pode lidar com isso // Não usamos elevação para evitar UAC adicional let output = Command::new("sc") .args(["stop", "RustDesk"]) .output(); match output { Ok(result) => { if result.status.success() { // Aguarda um pouco para o serviço parar std::thread::sleep(std::time::Duration::from_secs(2)); Ok(()) } else { let stderr = String::from_utf8_lossy(&result.stderr); log_event(format!( "Aviso: não foi possível parar o serviço RustDesk sem elevação: {}", stderr.trim() )); // Retornamos Ok para não bloquear - o serviço pode estar já parado Ok(()) } } Err(e) => { log_event(format!("Aviso: falha ao executar sc stop RustDesk: {e}")); 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 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)?; } if path.is_dir() { fs::remove_dir_all(path)?; } 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 { 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 { 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 { 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 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 log_password_replication(secret: &str) { for dir in remote_id_directories() { let primary = dir.join("RustDesk.toml"); log_password_match(&primary, secret); let local_path = dir.join("RustDesk_local.toml"); log_password_match(&local_path, secret); } } fn log_password_match(path: &Path, secret: &str) { match read_password_from_file(path) { Some(value) if value == secret => { log_event(format!( "Senha confirmada em {} ({})", path.display(), mask_secret(&value) )); } Some(value) => { log_event(format!( "Aviso: senha divergente ({}) em {}", mask_secret(&value), path.display() )); } None => { log_event(format!( "Aviso: chave 'password' não encontrada em {}", path.display() )); } } } fn read_password_from_file(path: &Path) -> Option { let content = fs::read_to_string(path).ok()?; for line in content.lines() { if let Some(value) = parse_assignment(line, "password") { return Some(value); } } None } fn mask_secret(secret: &str) -> String { if secret.is_empty() { return "".to_string(); } let chars: Vec = secret.chars().collect(); if chars.len() <= 4 { return "*".repeat(chars.len()); } let prefix: String = chars.iter().take(2).copied().collect(); let suffix: String = chars .iter() .rev() .take(2) .copied() .collect::>() .into_iter() .rev() .collect(); format!("{}***{}", prefix, suffix) } fn hidden_command(program: impl AsRef) -> Command { let mut cmd = Command::new(program); cmd.creation_flags(CREATE_NO_WINDOW); cmd } 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 log_event(message: impl AsRef) { if let Some(dir) = logs_directory() { if let Err(error) = append_log(dir, message.as_ref()) { eprintln!("[rustdesk][log] Falha ao registrar log: {error}"); } } } fn logs_directory() -> Option { let base = env::var("LOCALAPPDATA").ok()?; Some( Path::new(&base) .join("br.com.esdrasrenan.sistemadechamados") .join("logs"), ) } fn append_log(dir: PathBuf, message: &str) -> io::Result<()> { fs::create_dir_all(&dir)?; let log_path = dir.join("rustdesk.log"); let mut file = OpenOptions::new() .create(true) .append(true) .open(log_path)?; let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); writeln!(file, "[{timestamp}] {message}")?; Ok(()) } fn raven_appdata_root() -> Option { env::var("LOCALAPPDATA") .ok() .map(|value| Path::new(&value).join(APP_IDENTIFIER)) } fn machine_store_path() -> Option { raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME)) } fn read_machine_store_object() -> Option> { let path = machine_store_path()?; let contents = fs::read_to_string(path).ok()?; let value: JsonValue = serde_json::from_str(&contents).ok()?; value.as_object().cloned() } fn write_machine_store_object(map: JsonMap) -> Result<(), String> { let path = machine_store_path().ok_or_else(|| "LOCALAPPDATA não disponível".to_string())?; if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|error| format!("mkdir AppData: {error}"))?; } let serialized = serde_json::to_vec_pretty(&JsonValue::Object(map)) .map_err(|error| format!("serialize machine-agent: {error}"))?; fs::write(&path, serialized).map_err(|error| format!("write machine-agent: {error}"))?; Ok(()) } fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> { let mut map = read_machine_store_object().unwrap_or_default(); map.insert(key.to_string(), value); write_machine_store_object(map) } #[allow(dead_code)] fn machine_store_key_exists(key: &str) -> bool { read_machine_store_object() .map(|map| map.contains_key(key)) .unwrap_or(false) } #[allow(dead_code)] fn acl_flag_file_path() -> Option { raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME)) } #[allow(dead_code)] fn has_acl_unlock_flag() -> bool { if let Some(flag) = acl_flag_file_path() { if flag.exists() { return true; } } machine_store_key_exists(RUSTDESK_ACL_STORE_KEY) } #[allow(dead_code)] fn mark_acl_unlock_flag() { let timestamp = Utc::now().timestamp_millis(); if let Some(flag_path) = acl_flag_file_path() { if let Some(parent) = flag_path.parent() { let _ = fs::create_dir_all(parent); } if let Err(error) = fs::write(&flag_path, timestamp.to_string()) { log_event(format!( "Falha ao gravar flag de ACL em {}: {error}", flag_path.display() )); } } if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) { log_event(format!( "Falha ao registrar flag de ACL no machine-agent: {error}" )); } } fn get_machine_store_path() -> Result { let base = env::var("LOCALAPPDATA") .map_err(|_| RustdeskError::MissingId)?; Ok(Path::new(&base) .join(APP_IDENTIFIER) .join(MACHINE_STORE_FILENAME)) } fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) -> Result<(), RustdeskError> { log_event("Iniciando sincronizacao com backend..."); // Le token e config do store let store_path = get_machine_store_path()?; let store_content = fs::read_to_string(&store_path) .map_err(RustdeskError::Io)?; let store: serde_json::Value = serde_json::from_str(&store_content) .map_err(|_| RustdeskError::MissingId)?; let token = store.get("token") .and_then(|v| v.as_str()) .ok_or(RustdeskError::MissingId)?; let config = store.get("config") .ok_or(RustdeskError::MissingId)?; let machine_id = config.get("machineId") .and_then(|v| v.as_str()) .ok_or(RustdeskError::MissingId)?; let api_base_url = config.get("apiBaseUrl") .and_then(|v| v.as_str()) .unwrap_or("https://tickets.esdrasrenan.com.br"); log_event(format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id)); // Monta payload conforme schema esperado pelo backend // Schema: { machineToken, provider, identifier, password?, url?, username?, notes? } let payload = serde_json::json!({ "machineToken": token, "provider": "RustDesk", "identifier": result.id, "password": result.password, "notes": format!("Versao: {}. Provisionado em: {}", result.installed_version.as_deref().unwrap_or("desconhecida"), result.last_provisioned_at) }); // Faz POST para /api/machines/remote-access let client = Client::builder() .user_agent(USER_AGENT) .timeout(Duration::from_secs(30)) .build()?; let url = format!("{}/api/machines/remote-access", api_base_url); let response = client.post(&url) .header("Content-Type", "application/json") .header("Idempotency-Key", format!("{}:RustDesk:{}", machine_id, result.id)) .body(payload.to_string()) .send()?; if response.status().is_success() { log_event(format!("Sync com backend OK: status {}", response.status())); Ok(()) } else { let status = response.status(); let body = response.text().unwrap_or_default(); let body_preview = if body.len() > 200 { &body[..200] } else { &body }; log_event(format!("Sync com backend falhou: {} - {}", status, body_preview)); Err(RustdeskError::CommandFailed { command: "sync_remote_access".to_string(), status: Some(status.as_u16() as i32) }) } }