mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 16:31:03 +03:00
fix: Harden Windows installer temp command scripts (#15634)
* fix: Harden Windows installer temp command scripts Signed-off-by: fufesou <linlong1266@gmail.com> * fix: restore stop-service after install preparation failure Signed-off-by: fufesou <linlong1266@gmail.com> * fix(windows): preserve special characters in installer paths Handle carets and exclamation marks safely across cmd.exe parsing stages. Add coverage for special-character paths in the elevated installer handoff. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: installer, validate app name Signed-off-by: fufesou <linlong1266@gmail.com> * update tests Signed-off-by: fufesou <linlong1266@gmail.com> * Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -7212,18 +7212,6 @@ dependencies = [
|
|||||||
"realfft",
|
"realfft",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "runas"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b96d6b6c505282b007a9b009f2aa38b2fd0359b81a0430ceacc60f69ade4c6a0"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"security-framework-sys",
|
|
||||||
"which",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
@@ -7342,7 +7330,6 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"rubato",
|
"rubato",
|
||||||
"runas",
|
|
||||||
"rust-pulsectl",
|
"rust-pulsectl",
|
||||||
"samplerate",
|
"samplerate",
|
||||||
"sciter-rs",
|
"sciter-rs",
|
||||||
|
|||||||
@@ -124,14 +124,18 @@ windows = { version = "0.61", features = [
|
|||||||
"Win32_Security_Authorization",
|
"Win32_Security_Authorization",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
"Win32_System",
|
"Win32_System",
|
||||||
|
"Win32_System_Com",
|
||||||
"Win32_System_Diagnostics",
|
"Win32_System_Diagnostics",
|
||||||
"Win32_System_Diagnostics_ToolHelp",
|
"Win32_System_Diagnostics_ToolHelp",
|
||||||
"Win32_System_Environment",
|
"Win32_System_Environment",
|
||||||
"Win32_System_IO",
|
"Win32_System_IO",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
"Win32_System_Pipes",
|
"Win32_System_Pipes",
|
||||||
|
"Win32_System_Registry",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
] }
|
] }
|
||||||
winreg = "0.11"
|
winreg = "0.11"
|
||||||
windows-service = "0.6"
|
windows-service = "0.6"
|
||||||
@@ -140,7 +144,6 @@ remote_printer = { path = "libs/remote_printer" }
|
|||||||
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||||
shared_memory = "0.12"
|
shared_memory = "0.12"
|
||||||
tauri-winrt-notification = "0.1"
|
tauri-winrt-notification = "0.1"
|
||||||
runas = "1.2"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
|
|||||||
@@ -98,12 +98,19 @@ use windows_service::{
|
|||||||
use winreg::{enums::*, RegKey};
|
use winreg::{enums::*, RegKey};
|
||||||
|
|
||||||
mod acl;
|
mod acl;
|
||||||
|
mod installer_handoff;
|
||||||
|
mod installer_shell;
|
||||||
pub(crate) use acl::current_process_user_sid_string;
|
pub(crate) use acl::current_process_user_sid_string;
|
||||||
pub use acl::{
|
pub use acl::{
|
||||||
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
|
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
|
||||||
set_path_permission_for_portable_service_shmem_file,
|
set_path_permission_for_portable_service_shmem_file,
|
||||||
validate_path_for_portable_service_shmem_dir,
|
validate_path_for_portable_service_shmem_dir,
|
||||||
};
|
};
|
||||||
|
use installer_handoff::run_cmds;
|
||||||
|
use installer_shell::{
|
||||||
|
embedded_shortcut_commands, embedded_tray_shortcut_commands, escape_nested_cmd_ampersands,
|
||||||
|
shortcut_bytes, validate_install_value,
|
||||||
|
};
|
||||||
|
|
||||||
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
|
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
|
||||||
pub const EXPLORER_EXE: &'static str = "explorer.exe";
|
pub const EXPLORER_EXE: &'static str = "explorer.exe";
|
||||||
@@ -113,6 +120,17 @@ const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS";
|
|||||||
const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS";
|
const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS";
|
||||||
pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER";
|
pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER";
|
||||||
|
|
||||||
|
fn validate_install_app_name(app_name: &str) -> ResultType<()> {
|
||||||
|
if app_name.is_empty()
|
||||||
|
|| !app_name
|
||||||
|
.chars()
|
||||||
|
.all(|character| character.is_ascii_alphanumeric() || character == '-')
|
||||||
|
{
|
||||||
|
bail!("Application name must match [a-zA-Z0-9-]+");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let hwnd = GetForegroundWindow();
|
let hwnd = GetForegroundWindow();
|
||||||
@@ -1499,6 +1517,7 @@ fn get_after_install(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let app_name = crate::get_app_name();
|
let app_name = crate::get_app_name();
|
||||||
let ext = app_name.to_lowercase();
|
let ext = app_name.to_lowercase();
|
||||||
|
let nested_exe = escape_nested_cmd_ampersands(exe);
|
||||||
|
|
||||||
// reg delete HKEY_CURRENT_USER\Software\Classes for
|
// reg delete HKEY_CURRENT_USER\Software\Classes for
|
||||||
// https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a
|
// https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a
|
||||||
@@ -1534,17 +1553,17 @@ fn get_after_install(
|
|||||||
{start_menu_shortcuts}
|
{start_menu_shortcuts}
|
||||||
{reg_printer}
|
{reg_printer}
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\"
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\",0\"
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f
|
||||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\"
|
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\" --play \\\"%%1\\\"\"
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext} /f
|
reg add HKEY_CLASSES_ROOT\\{ext} /f
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext} /f /v \"URL Protocol\" /t REG_SZ /d \"\"
|
reg add HKEY_CLASSES_ROOT\\{ext} /f /v \"URL Protocol\" /t REG_SZ /d \"\"
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell /f
|
reg add HKEY_CLASSES_ROOT\\{ext}\\shell /f
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open /f
|
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open /f
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f
|
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f
|
||||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" \\\"%%1\\\"\"
|
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\" \\\"%%1\\\"\"
|
||||||
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=out action=allow program=\"{exe}\" enable=yes
|
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=out action=allow program=\"{exe}\" enable=yes
|
||||||
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes
|
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes
|
||||||
{create_service}
|
{create_service}
|
||||||
@@ -1578,48 +1597,38 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
|
|||||||
let app_name = crate::get_app_name();
|
let app_name = crate::get_app_name();
|
||||||
|
|
||||||
let current_exe = std::env::current_exe()?;
|
let current_exe = std::env::current_exe()?;
|
||||||
|
let cur_exe = current_exe
|
||||||
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
|
.to_str()
|
||||||
let cur_exe = current_exe.to_str().unwrap_or("").to_owned();
|
.ok_or_else(|| anyhow!("Current executable path is not valid Unicode"))?
|
||||||
let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe);
|
.to_owned();
|
||||||
let mk_shortcut = write_cmds(
|
for value in [&path, &exe, &cur_exe] {
|
||||||
format!(
|
validate_install_value(value)?;
|
||||||
"
|
}
|
||||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
let config_path = Config::file();
|
||||||
sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
|
validate_install_value(
|
||||||
|
config_path
|
||||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
.to_str()
|
||||||
oLink.TargetPath = \"{exe}\"
|
.ok_or_else(|| anyhow!("Configuration path is not valid Unicode"))?,
|
||||||
{shortcut_icon_location}
|
)?;
|
||||||
oLink.Save
|
let shortcut_icon_location = get_custom_icon(&path, &cur_exe);
|
||||||
"
|
if let Some(icon) = shortcut_icon_location.as_deref() {
|
||||||
),
|
validate_install_value(icon)?;
|
||||||
"vbs",
|
}
|
||||||
|
// The elevated runner expands this to `%~f0.dir`, beside its protected copy.
|
||||||
|
// Do not stage privileged shortcut artifacts in the user-writable `%TEMP%`.
|
||||||
|
let tmp_path = "%RUSTDESK_OUTPUT_DIR%".to_owned();
|
||||||
|
let mk_shortcut_commands = embedded_shortcut_commands(
|
||||||
|
shortcut_bytes(&exe, None, shortcut_icon_location.as_deref())?,
|
||||||
|
&format!("{app_name}.lnk"),
|
||||||
"mk_shortcut",
|
"mk_shortcut",
|
||||||
)?
|
);
|
||||||
.to_str()
|
let uninstall_shortcut_commands = embedded_shortcut_commands(
|
||||||
.unwrap_or("")
|
shortcut_bytes(&exe, Some("--uninstall"), Some("msiexec.exe"))?,
|
||||||
.to_owned();
|
&format!("Uninstall {app_name}.lnk"),
|
||||||
// https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd
|
|
||||||
let uninstall_shortcut = write_cmds(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
|
||||||
sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
|
||||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
|
||||||
oLink.TargetPath = \"{exe}\"
|
|
||||||
oLink.Arguments = \"--uninstall\"
|
|
||||||
oLink.IconLocation = \"msiexec.exe\"
|
|
||||||
oLink.Save
|
|
||||||
"
|
|
||||||
),
|
|
||||||
"vbs",
|
|
||||||
"uninstall_shortcut",
|
"uninstall_shortcut",
|
||||||
)?
|
);
|
||||||
.to_str()
|
let tray_shortcut_commands =
|
||||||
.unwrap_or("")
|
embedded_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?;
|
||||||
.to_owned();
|
|
||||||
let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?;
|
|
||||||
let mut reg_value_desktop_shortcuts = "0".to_owned();
|
let mut reg_value_desktop_shortcuts = "0".to_owned();
|
||||||
let mut reg_value_start_menu_shortcuts = "0".to_owned();
|
let mut reg_value_start_menu_shortcuts = "0".to_owned();
|
||||||
let mut reg_value_printer = "0".to_owned();
|
let mut reg_value_printer = "0".to_owned();
|
||||||
@@ -1660,15 +1669,12 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
|
|||||||
// Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895
|
// Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895
|
||||||
let dels = format!(
|
let dels = format!(
|
||||||
"
|
"
|
||||||
if exist \"{mk_shortcut}\" del /f /q \"{mk_shortcut}\"
|
|
||||||
if exist \"{uninstall_shortcut}\" del /f /q \"{uninstall_shortcut}\"
|
|
||||||
if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
|
|
||||||
if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\"
|
if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\"
|
||||||
if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
||||||
if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\"
|
if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\"
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string();
|
let src_exe = cur_exe.clone();
|
||||||
|
|
||||||
// potential bug here: if run_cmd cancelled, but config file is changed.
|
// potential bug here: if run_cmd cancelled, but config file is changed.
|
||||||
if let Some(lic) = get_license() {
|
if let Some(lic) = get_license() {
|
||||||
@@ -1681,7 +1687,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name}
|
|||||||
"".to_owned()
|
"".to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("
|
format!("
|
||||||
cscript \"{tray_shortcut}\"
|
{tray_shortcut_commands}
|
||||||
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
||||||
")
|
")
|
||||||
};
|
};
|
||||||
@@ -1716,11 +1722,11 @@ reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\"
|
|||||||
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
|
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
|
||||||
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
|
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
|
||||||
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
|
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
|
||||||
reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\"
|
reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{nested_exe}\\\" --uninstall\"
|
||||||
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||||
reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0
|
reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0
|
||||||
cscript \"{mk_shortcut}\"
|
{mk_shortcut_commands}
|
||||||
cscript \"{uninstall_shortcut}\"
|
{uninstall_shortcut_commands}
|
||||||
{tray_shortcuts}
|
{tray_shortcuts}
|
||||||
{shortcuts}
|
{shortcuts}
|
||||||
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
||||||
@@ -1730,7 +1736,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
|||||||
{install_remote_printer}
|
{install_remote_printer}
|
||||||
{sleep}
|
{sleep}
|
||||||
",
|
",
|
||||||
display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()),
|
display_icon = shortcut_icon_location.as_deref().unwrap_or(exe.as_str()),
|
||||||
|
nested_exe = escape_nested_cmd_ampersands(&exe),
|
||||||
version = crate::VERSION.replace("-", "."),
|
version = crate::VERSION.replace("-", "."),
|
||||||
build_date = crate::BUILD_DATE,
|
build_date = crate::BUILD_DATE,
|
||||||
after_install = get_after_install(
|
after_install = get_after_install(
|
||||||
@@ -1836,10 +1843,9 @@ pub fn uninstall_me(kill_self: bool) -> ResultType<()> {
|
|||||||
run_cmds(get_uninstall(kill_self, true), true, "uninstall")
|
run_cmds(get_uninstall(kill_self, true), true, "uninstall")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathBuf> {
|
fn write_vbs(cmds: String, tip: &str) -> ResultType<PathBuf> {
|
||||||
let mut cmds = cmds;
|
const UTF16LE_BOM: &[u8] = &[0xFF, 0xFE];
|
||||||
let mut tmp = std::env::temp_dir();
|
let mut tmp = std::env::temp_dir();
|
||||||
// When dir contains these characters, the bat file will not execute in elevated mode.
|
|
||||||
if vec!["&", "@", "^"]
|
if vec!["&", "@", "^"]
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.any(|s| tmp.to_string_lossy().to_string().contains(s))
|
.any(|s| tmp.to_string_lossy().to_string().contains(s))
|
||||||
@@ -1848,31 +1854,14 @@ fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathB
|
|||||||
tmp = dir;
|
tmp = dir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext));
|
tmp.push(format!("{}_{}.vbs", crate::get_app_name(), tip));
|
||||||
let mut file = std::fs::File::create(&tmp)?;
|
let mut file = fs::File::create(&tmp)?;
|
||||||
if ext == "bat" {
|
let cmds = cmds.replace("\r\n", "\n").replace('\n', "\r\n");
|
||||||
let tmp2 = get_undone_file(&tmp)?;
|
let mut utf16: Vec<u16> = cmds.encode_utf16().collect();
|
||||||
std::fs::File::create(&tmp2).ok();
|
file.write_all(UTF16LE_BOM)?;
|
||||||
cmds = format!(
|
file.write_all(to_le(&mut utf16))?;
|
||||||
"
|
|
||||||
{cmds}
|
|
||||||
if exist \"{path}\" del /f /q \"{path}\"
|
|
||||||
",
|
|
||||||
path = tmp2.to_string_lossy()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// in case cmds mixed with \r\n and \n, make sure all ending with \r\n
|
|
||||||
// in some windows, \r\n required for cmd file to run
|
|
||||||
cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n");
|
|
||||||
if ext == "vbs" {
|
|
||||||
let mut v: Vec<u16> = cmds.encode_utf16().collect();
|
|
||||||
// utf8 -> utf16le which vbs support it only
|
|
||||||
file.write_all(to_le(&mut v))?;
|
|
||||||
} else {
|
|
||||||
file.write_all(cmds.as_bytes())?;
|
|
||||||
}
|
|
||||||
file.sync_all()?;
|
file.sync_all()?;
|
||||||
return Ok(tmp);
|
Ok(tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_le(v: &mut [u16]) -> &[u8] {
|
fn to_le(v: &mut [u16]) -> &[u8] {
|
||||||
@@ -1882,37 +1871,6 @@ fn to_le(v: &mut [u16]) -> &[u8] {
|
|||||||
unsafe { v.align_to().1 }
|
unsafe { v.align_to().1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_undone_file(tmp: &Path) -> ResultType<PathBuf> {
|
|
||||||
Ok(tmp.with_file_name(format!(
|
|
||||||
"{}.undone",
|
|
||||||
tmp.file_name()
|
|
||||||
.ok_or(anyhow!("Failed to get filename of {:?}", tmp))?
|
|
||||||
.to_string_lossy()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
|
|
||||||
let tmp = write_cmds(cmds, "bat", tip)?;
|
|
||||||
let tmp2 = get_undone_file(&tmp)?;
|
|
||||||
let tmp_fn = tmp.to_str().unwrap_or("");
|
|
||||||
// https://github.com/rustdesk/rustdesk/issues/6786#issuecomment-1879655410
|
|
||||||
// Specify cmd.exe explicitly to avoid the replacement of cmd commands.
|
|
||||||
let res = runas::Command::new("cmd.exe")
|
|
||||||
.args(&["/C", &tmp_fn])
|
|
||||||
.show(show)
|
|
||||||
.force_prompt(true)
|
|
||||||
.status();
|
|
||||||
if !show {
|
|
||||||
allow_err!(std::fs::remove_file(tmp));
|
|
||||||
}
|
|
||||||
let _ = res?;
|
|
||||||
if tmp2.exists() {
|
|
||||||
allow_err!(std::fs::remove_file(tmp2));
|
|
||||||
bail!("{} failed", tip);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_blank_screen(v: bool) {
|
pub fn toggle_blank_screen(v: bool) {
|
||||||
let v = if v { TRUE } else { FALSE };
|
let v = if v { TRUE } else { FALSE };
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -2288,7 +2246,7 @@ pub fn create_shortcut(id: &str) -> ResultType<()> {
|
|||||||
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
|
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
|
||||||
let filename = id.replace(':', "_");
|
let filename = id.replace(':', "_");
|
||||||
let shortcut_icon_location = get_shortcut_icon_location("", &exe);
|
let shortcut_icon_location = get_shortcut_icon_location("", &exe);
|
||||||
let shortcut = write_cmds(
|
let shortcut = write_vbs(
|
||||||
format!(
|
format!(
|
||||||
"
|
"
|
||||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||||
@@ -2302,7 +2260,6 @@ Set oLink = oWS.CreateShortcut(sLinkFile)
|
|||||||
oLink.Save
|
oLink.Save
|
||||||
"
|
"
|
||||||
),
|
),
|
||||||
"vbs",
|
|
||||||
"connect_shortcut",
|
"connect_shortcut",
|
||||||
)?
|
)?
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -3245,29 +3202,52 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
|
|||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_install_service_commands(path: &str, exe: &str) -> ResultType<String> {
|
||||||
|
let app_name = crate::get_app_name();
|
||||||
|
for value in [path, exe] {
|
||||||
|
validate_install_value(value)?;
|
||||||
|
}
|
||||||
|
let config_path = Config::file();
|
||||||
|
validate_install_value(
|
||||||
|
config_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Configuration path is not valid Unicode"))?,
|
||||||
|
)?;
|
||||||
|
let shortcut_icon_location = get_custom_icon(path, exe);
|
||||||
|
if let Some(icon) = shortcut_icon_location.as_deref() {
|
||||||
|
validate_install_value(icon)?;
|
||||||
|
}
|
||||||
|
let tray_shortcut_commands =
|
||||||
|
embedded_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?;
|
||||||
|
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
|
||||||
|
Ok(format!(
|
||||||
|
"
|
||||||
|
chcp 65001
|
||||||
|
taskkill /F /IM {app_name}.exe{filter}
|
||||||
|
{tray_shortcut_commands}
|
||||||
|
copy /Y \"%RUSTDESK_OUTPUT_DIR%\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
||||||
|
{import_config}
|
||||||
|
{create_service}
|
||||||
|
",
|
||||||
|
import_config = get_import_config(exe),
|
||||||
|
create_service = get_create_service(exe),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn install_service() -> bool {
|
pub fn install_service() -> bool {
|
||||||
log::info!("Installing service...");
|
log::info!("Installing service...");
|
||||||
let _installing = crate::platform::InstallingService::new();
|
let _installing = crate::platform::InstallingService::new();
|
||||||
let (_, path, _, exe) = get_install_info();
|
let (_, path, _, exe) = get_install_info();
|
||||||
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
|
|
||||||
let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default();
|
|
||||||
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
|
|
||||||
Config::set_option("stop-service".into(), "".into());
|
Config::set_option("stop-service".into(), "".into());
|
||||||
|
let cmds = match get_install_service_commands(&path, &exe) {
|
||||||
|
Ok(cmds) => cmds,
|
||||||
|
Err(err) => {
|
||||||
|
Config::set_option("stop-service".into(), "Y".into());
|
||||||
|
log::error!("Failed to prepare service installation: {err}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed);
|
crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed);
|
||||||
let cmds = format!(
|
|
||||||
"
|
|
||||||
chcp 65001
|
|
||||||
taskkill /F /IM {app_name}.exe{filter}
|
|
||||||
cscript \"{tray_shortcut}\"
|
|
||||||
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
|
||||||
{import_config}
|
|
||||||
{create_service}
|
|
||||||
if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
|
|
||||||
",
|
|
||||||
app_name = crate::get_app_name(),
|
|
||||||
import_config = get_import_config(&exe),
|
|
||||||
create_service = get_create_service(&exe),
|
|
||||||
);
|
|
||||||
if let Err(err) = run_cmds(cmds, false, "install") {
|
if let Err(err) = run_cmds(cmds, false, "install") {
|
||||||
Config::set_option("stop-service".into(), "Y".into());
|
Config::set_option("stop-service".into(), "Y".into());
|
||||||
crate::ipc::EXIT_RECV_CLOSE.store(true, Ordering::Relaxed);
|
crate::ipc::EXIT_RECV_CLOSE.store(true, Ordering::Relaxed);
|
||||||
@@ -3728,39 +3708,13 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tray_shortcut(
|
|
||||||
install_dir: &str,
|
|
||||||
exe: &str,
|
|
||||||
icon_source_exe: &str,
|
|
||||||
tmp_path: &str,
|
|
||||||
) -> ResultType<String> {
|
|
||||||
let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe);
|
|
||||||
Ok(write_cmds(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
|
||||||
sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\"
|
|
||||||
|
|
||||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
|
||||||
oLink.TargetPath = \"{exe}\"
|
|
||||||
oLink.Arguments = \"--tray\"
|
|
||||||
{shortcut_icon_location}
|
|
||||||
oLink.Save
|
|
||||||
",
|
|
||||||
app_name = crate::get_app_name(),
|
|
||||||
),
|
|
||||||
"vbs",
|
|
||||||
"tray_shortcut",
|
|
||||||
)?
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_import_config(exe: &str) -> String {
|
fn get_import_config(exe: &str) -> String {
|
||||||
if config::is_outgoing_only() {
|
if config::is_outgoing_only() {
|
||||||
return "".to_string();
|
return "".to_string();
|
||||||
}
|
}
|
||||||
|
let exe = escape_nested_cmd_ampersands(exe);
|
||||||
|
let config_path = Config::file();
|
||||||
|
let config_path = escape_nested_cmd_ampersands(config_path.to_str().unwrap_or(""));
|
||||||
format!("
|
format!("
|
||||||
sc stop {app_name}
|
sc stop {app_name}
|
||||||
sc delete {app_name}
|
sc delete {app_name}
|
||||||
@@ -3770,7 +3724,6 @@ sc stop {app_name}
|
|||||||
sc delete {app_name}
|
sc delete {app_name}
|
||||||
",
|
",
|
||||||
app_name = crate::get_app_name(),
|
app_name = crate::get_app_name(),
|
||||||
config_path=Config::file().to_str().unwrap_or(""),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3784,6 +3737,7 @@ fn get_create_service(exe: &str) -> String {
|
|||||||
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
|
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
|
||||||
", app_name = crate::get_app_name())
|
", app_name = crate::get_app_name())
|
||||||
} else {
|
} else {
|
||||||
|
let exe = escape_nested_cmd_ampersands(exe);
|
||||||
format!("
|
format!("
|
||||||
sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\"
|
sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\"
|
||||||
sc start {app_name}
|
sc start {app_name}
|
||||||
@@ -4681,6 +4635,28 @@ mod tests {
|
|||||||
assert_eq!(chr, None)
|
assert_eq!(chr, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_app_names_enforce_ascii_command_safety() {
|
||||||
|
assert!(validate_install_app_name("RustDesk-Admin1").is_ok());
|
||||||
|
for app_name in ["", "RustDesk_Admin", "RustDesk&whoami", "RustDesk应用"] {
|
||||||
|
assert!(
|
||||||
|
validate_install_app_name(app_name).is_err(),
|
||||||
|
"unsafe application name was accepted: {app_name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vbs_files_use_utf16le_with_bom_and_crlf() {
|
||||||
|
const EXPECTED: &[u8] = &[0xFF, 0xFE, b'a', 0, b'\r', 0, b'\n', 0, b'b', 0];
|
||||||
|
let tip = format!("vbs_encoding_{}", uuid::Uuid::new_v4().simple());
|
||||||
|
let path = write_vbs("a\nb".to_owned(), &tip).expect("VBS file should be written");
|
||||||
|
let bytes = std::fs::read(&path).expect("VBS file should be readable");
|
||||||
|
std::fs::remove_file(path).expect("VBS file should be removed");
|
||||||
|
|
||||||
|
assert_eq!(bytes, EXPECTED);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_pointer_width = "64"))]
|
#[cfg(not(target_pointer_width = "64"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_pids_with_args_from_wmic_output() {
|
fn test_get_pids_with_args_from_wmic_output() {
|
||||||
|
|||||||
288
src/platform/windows/installer_handoff.rs
Normal file
288
src/platform/windows/installer_handoff.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use super::{
|
||||||
|
installer_shell::{
|
||||||
|
get_system_executable, path_for_cmd_assignment, path_for_cmd_environment,
|
||||||
|
run_elevated_and_wait, trusted_install_environment,
|
||||||
|
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH,
|
||||||
|
},
|
||||||
|
validate_install_app_name, ResultType,
|
||||||
|
};
|
||||||
|
use hbb_common::{
|
||||||
|
bail, log,
|
||||||
|
sha2::{Digest, Sha256},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{self, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CERTUTIL_RELATIVE_PATH: &str = "certutil.exe";
|
||||||
|
const CHCP_RELATIVE_PATH: &str = "chcp.com";
|
||||||
|
const FINDSTR_RELATIVE_PATH: &str = "findstr.exe";
|
||||||
|
const UTF8_CODE_PAGE: u32 = 65001;
|
||||||
|
const INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE: u32 = 0x5253_0001;
|
||||||
|
const INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0002;
|
||||||
|
const INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_0003;
|
||||||
|
const INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_0004;
|
||||||
|
const BATCH_CODE_PAGE_FAILURE_EXIT_CODE: u32 = 0x5253_0005;
|
||||||
|
const BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE: u32 = 0x5253_0006;
|
||||||
|
const BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE: u32 = 0x5253_0007;
|
||||||
|
const SHA256_HASH_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
type BatchHash = [u8; SHA256_HASH_LENGTH];
|
||||||
|
|
||||||
|
struct InstallCommandScript {
|
||||||
|
path: PathBuf,
|
||||||
|
expected_hash: BatchHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for InstallCommandScript {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = fs::remove_file(&self.path) {
|
||||||
|
if err.kind() != io::ErrorKind::NotFound {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to remove temporary installer file {:?}: {err}",
|
||||||
|
self.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_install_commands(commands: &str) -> ResultType<String> {
|
||||||
|
let commands = commands.replace("\r\n", "\n").replace('\n', "\r\n");
|
||||||
|
let chcp_path = get_system_executable(CHCP_RELATIVE_PATH)?;
|
||||||
|
let chcp = path_for_cmd_environment(&chcp_path)?;
|
||||||
|
Ok(format!(
|
||||||
|
"@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n\
|
||||||
|
\"{chcp}\" {UTF8_CODE_PAGE} > nul || exit /b \
|
||||||
|
{BATCH_CODE_PAGE_FAILURE_EXIT_CODE}\r\n\
|
||||||
|
{}\r\n\
|
||||||
|
if exist \"%~f0.dir\" exit /b {BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE}\r\n\
|
||||||
|
md \"%~f0.dir\" || exit /b {BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE}\r\n\
|
||||||
|
set \"RUSTDESK_OUTPUT_DIR=%~f0.dir\"\r\n{commands}\r\nexit /b 0\r\n",
|
||||||
|
trusted_install_environment()?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_install_script(cmds: String) -> ResultType<InstallCommandScript> {
|
||||||
|
let directory = std::env::temp_dir();
|
||||||
|
path_for_cmd_environment(&directory)?;
|
||||||
|
let commands = prepare_install_commands(&cmds)?;
|
||||||
|
let expected_hash = Sha256::digest(commands.as_bytes()).into();
|
||||||
|
let path = directory.join(format!(
|
||||||
|
"rustdesk_install_{}.bat",
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
));
|
||||||
|
let mut file = fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(&path)?;
|
||||||
|
let script = InstallCommandScript {
|
||||||
|
path,
|
||||||
|
expected_hash,
|
||||||
|
};
|
||||||
|
file.write_all(commands.as_bytes())?;
|
||||||
|
file.sync_all()?;
|
||||||
|
Ok(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_hash_pattern(hash: &BatchHash) -> String {
|
||||||
|
hash.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" *")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verified_install_bootstrap(
|
||||||
|
script: &InstallCommandScript,
|
||||||
|
runner_directory: &Path,
|
||||||
|
) -> ResultType<String> {
|
||||||
|
let source = path_for_cmd_assignment(&script.path)?;
|
||||||
|
let runner = runner_directory.join(format!(
|
||||||
|
"rustdesk_install_{}.bat",
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
));
|
||||||
|
let runner = path_for_cmd_assignment(&runner)?;
|
||||||
|
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
|
||||||
|
let certutil_path = get_system_executable(CERTUTIL_RELATIVE_PATH)?;
|
||||||
|
let findstr_path = get_system_executable(FINDSTR_RELATIVE_PATH)?;
|
||||||
|
let cmd = path_for_cmd_assignment(&cmd_path)?;
|
||||||
|
let certutil = path_for_cmd_assignment(&certutil_path)?;
|
||||||
|
let findstr = path_for_cmd_assignment(&findstr_path)?;
|
||||||
|
// Short names preserve headroom under the Windows 7 ShellExecuteExW 2,048
|
||||||
|
// UTF-16-character parameter limit. Paths are stored with delayed expansion
|
||||||
|
// disabled, then expanded indirectly so literal `!` survives: S=source,
|
||||||
|
// R=runner, Q=cmd.exe, H=certutil.exe, F=findstr.exe, C=created flag, E=exit code.
|
||||||
|
Ok(format!(
|
||||||
|
"setlocal DisableDelayedExpansion & set \"S={source}\" & set \"R={runner}\" & \
|
||||||
|
set \"Q={cmd}\" & set \"H={certutil}\" & set \"F={findstr}\" & \
|
||||||
|
set \"C=0\" & set \"E=0\" & setlocal EnableDelayedExpansion & \
|
||||||
|
if exist \"!R!\" (set \"E={INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE}\") else (\
|
||||||
|
set \"C=1\" & copy /Y \"!S!\" \"!R!\" > nul || \
|
||||||
|
(set \"E={INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE}\") & \
|
||||||
|
if \"!E!\"==\"0\" (\"!H!\" -hashfile \"!R!\" SHA256 > \"!R!.hash\" || \
|
||||||
|
set \"E={INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE}\") & \
|
||||||
|
if \"!E!\"==\"0\" (\"!F!\" /R /I /X /C:\"{}\" \"!R!.hash\" > nul || \
|
||||||
|
set \"E={INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE}\") & \
|
||||||
|
if \"!E!\"==\"0\" (\"!Q!\" /D /E:ON /V:OFF /C \"\"!R!\"\" & \
|
||||||
|
set \"E=!errorlevel!\")) & \
|
||||||
|
if \"!C!\"==\"1\" (rd /s /q \"!R!.dir\" > nul 2>&1 & \
|
||||||
|
del /f /q \"!R!\" \"!R!.*\" > nul 2>&1) & exit /b !E!",
|
||||||
|
install_hash_pattern(&script.expected_hash),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verified_install_parameters(script: &InstallCommandScript) -> ResultType<String> {
|
||||||
|
let system_directory = get_system_executable("")?;
|
||||||
|
Ok(format!(
|
||||||
|
"/D /E:ON /V:ON /C {}",
|
||||||
|
verified_install_bootstrap(script, &system_directory)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
|
||||||
|
validate_install_app_name(&crate::get_app_name())?;
|
||||||
|
let script = write_install_script(cmds)?;
|
||||||
|
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
|
||||||
|
let parameters = verified_install_parameters(&script)?;
|
||||||
|
let exit_code = run_elevated_and_wait(&cmd_path, ¶meters, show)?;
|
||||||
|
if exit_code != 0 {
|
||||||
|
bail!(
|
||||||
|
"{tip} failed with elevated exit code {exit_code}: {}",
|
||||||
|
elevated_install_failure_reason(exit_code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elevated_install_failure_reason(exit_code: u32) -> &'static str {
|
||||||
|
match exit_code {
|
||||||
|
INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE => "protected runner already exists",
|
||||||
|
INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE => "failed to copy protected runner",
|
||||||
|
INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE => "failed to hash protected runner",
|
||||||
|
INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE => "protected runner hash mismatch",
|
||||||
|
BATCH_CODE_PAGE_FAILURE_EXIT_CODE => "failed to set the installer code page",
|
||||||
|
BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE => "installer output directory already exists",
|
||||||
|
BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE => {
|
||||||
|
"failed to create the installer output directory"
|
||||||
|
}
|
||||||
|
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE => "failed to decode an embedded shortcut",
|
||||||
|
_ => "installer command failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::installer_shell::{
|
||||||
|
embedded_shortcut_commands, shortcut_bytes, WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS,
|
||||||
|
};
|
||||||
|
use super::*;
|
||||||
|
use ::windows::Win32::System::Threading;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_install_handoff_verifies_before_execution() {
|
||||||
|
let marker = std::env::temp_dir().join(format!(
|
||||||
|
"rustdesk_install_marker_{}",
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
));
|
||||||
|
let runner_dir = std::env::temp_dir().join(format!(
|
||||||
|
"rustdesk_install_!RUSTDESK_HANDOFF_EXPAND!&^@()runner_{}",
|
||||||
|
uuid::Uuid::new_v4().simple()
|
||||||
|
));
|
||||||
|
std::fs::create_dir(&runner_dir).expect("runner directory should be created");
|
||||||
|
let shortcut_commands = embedded_shortcut_commands(
|
||||||
|
shortcut_bytes(r"C:\RustDesk.exe", None, None)
|
||||||
|
.expect("native shortcut should be generated"),
|
||||||
|
"test.lnk",
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
let script = write_install_script(format!(
|
||||||
|
"if \"%PROGRAMDATA%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
|
||||||
|
if \"%PUBLIC%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
|
||||||
|
{shortcut_commands}\r\n\
|
||||||
|
> \"{}\" echo verified",
|
||||||
|
marker.display()
|
||||||
|
))
|
||||||
|
.expect("install script should be created");
|
||||||
|
let bootstrap = verified_install_bootstrap(&script, &runner_dir)
|
||||||
|
.expect("native verifier bootstrap should be generated");
|
||||||
|
assert_native_handoff_structure(&script, &shortcut_commands, &bootstrap);
|
||||||
|
|
||||||
|
let output = run_install_bootstrap_for_test(&bootstrap);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"unchanged script failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert!(marker.exists(), "verified install script must execute");
|
||||||
|
std::fs::remove_file(&marker).expect("test marker should be removed");
|
||||||
|
assert_replaced_install_script_is_rejected(&script, &runner_dir, &marker);
|
||||||
|
std::fs::remove_dir(runner_dir).expect("runner directory should be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_native_handoff_structure(
|
||||||
|
script: &InstallCommandScript,
|
||||||
|
shortcut_commands: &str,
|
||||||
|
bootstrap: &str,
|
||||||
|
) {
|
||||||
|
assert!(shortcut_commands.contains("certutil"));
|
||||||
|
assert!(shortcut_commands.contains("-decode"));
|
||||||
|
assert!(!shortcut_commands.to_ascii_lowercase().contains("cscript"));
|
||||||
|
assert!(!shortcut_commands
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("powershell"));
|
||||||
|
let win7_hash_pattern = script
|
||||||
|
.expected_hash
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" *");
|
||||||
|
assert!(bootstrap.contains(&format!("/R /I /X /C:\"{win7_hash_pattern}\"")));
|
||||||
|
let parameters =
|
||||||
|
verified_install_parameters(script).expect("elevated parameters should be generated");
|
||||||
|
assert!(bootstrap.contains("certutil.exe"));
|
||||||
|
assert!(bootstrap.contains("findstr.exe"));
|
||||||
|
assert!(!bootstrap.to_ascii_lowercase().contains("powershell"));
|
||||||
|
assert!(parameters.encode_utf16().count() < WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_replaced_install_script_is_rejected(
|
||||||
|
script: &InstallCommandScript,
|
||||||
|
runner_dir: &Path,
|
||||||
|
marker: &Path,
|
||||||
|
) {
|
||||||
|
std::fs::write(
|
||||||
|
&script.path,
|
||||||
|
format!("> \"{}\" echo hijacked\r\n", marker.display()),
|
||||||
|
)
|
||||||
|
.expect("install script should be replaceable");
|
||||||
|
let replaced = verified_install_bootstrap(&script, &runner_dir)
|
||||||
|
.expect("replacement verifier should be generated");
|
||||||
|
let output = run_install_bootstrap_for_test(&replaced);
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"replaced script unexpectedly passed verification"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output.status.code(),
|
||||||
|
Some(INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE as i32)
|
||||||
|
);
|
||||||
|
assert!(!marker.exists(), "replaced script must not execute");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_install_bootstrap_for_test(bootstrap: &str) -> std::process::Output {
|
||||||
|
let cmd = get_system_executable(CMD_RELATIVE_PATH).expect("system cmd.exe should resolve");
|
||||||
|
let mut command = std::process::Command::new(cmd);
|
||||||
|
command
|
||||||
|
.env("PROGRAMDATA", "rustdesk_untrusted")
|
||||||
|
.env("PUBLIC", "rustdesk_untrusted")
|
||||||
|
.env("RUSTDESK_HANDOFF_EXPAND", "expanded");
|
||||||
|
command.raw_arg(format!("/D /E:ON /V:ON /C {bootstrap}"));
|
||||||
|
command
|
||||||
|
.creation_flags(Threading::CREATE_NO_WINDOW.0)
|
||||||
|
.output()
|
||||||
|
.expect("native verifier should run")
|
||||||
|
}
|
||||||
|
}
|
||||||
300
src/platform/windows/installer_shell.rs
Normal file
300
src/platform/windows/installer_shell.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
use super::{wide_string, ResultType};
|
||||||
|
use hbb_common::{
|
||||||
|
anyhow::anyhow,
|
||||||
|
bail,
|
||||||
|
base64::{engine::general_purpose::STANDARD, Engine as _},
|
||||||
|
log,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
ffi::OsString,
|
||||||
|
io, mem,
|
||||||
|
os::windows::ffi::OsStringExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use windows::{
|
||||||
|
core::{Interface, PCWSTR},
|
||||||
|
Win32::{
|
||||||
|
Foundation::{self, CloseHandle, HANDLE},
|
||||||
|
System::{Com, SystemInformation, Threading},
|
||||||
|
UI::{
|
||||||
|
Shell::{
|
||||||
|
self, FOLDERID_ProgramData, FOLDERID_Public, SHGetKnownFolderPath, KF_FLAG_DEFAULT,
|
||||||
|
},
|
||||||
|
WindowsAndMessaging,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) const CMD_RELATIVE_PATH: &str = "cmd.exe";
|
||||||
|
pub(super) const BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE: u32 = 0x5253_0008;
|
||||||
|
pub(super) const WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS: usize = 2048;
|
||||||
|
const SHORTCUT_ICON_INDEX: i32 = 0;
|
||||||
|
|
||||||
|
pub(super) fn shortcut_bytes(
|
||||||
|
target_path: &str,
|
||||||
|
arguments: Option<&str>,
|
||||||
|
icon_location: Option<&str>,
|
||||||
|
) -> ResultType<Vec<u8>> {
|
||||||
|
let _com = initialize_shell_com()?;
|
||||||
|
let link: Shell::IShellLinkW =
|
||||||
|
unsafe { Com::CoCreateInstance(&Shell::ShellLink, None, Com::CLSCTX_INPROC_SERVER) }?;
|
||||||
|
let target_path = wide_string(target_path);
|
||||||
|
unsafe { link.SetPath(PCWSTR(target_path.as_ptr())) }?;
|
||||||
|
if let Some(arguments) = arguments {
|
||||||
|
let arguments = wide_string(arguments);
|
||||||
|
unsafe { link.SetArguments(PCWSTR(arguments.as_ptr())) }?;
|
||||||
|
}
|
||||||
|
if let Some(icon_location) = icon_location {
|
||||||
|
let icon_location = wide_string(icon_location);
|
||||||
|
unsafe { link.SetIconLocation(PCWSTR(icon_location.as_ptr()), SHORTCUT_ICON_INDEX) }?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = unsafe { Shell::SHCreateMemStream(None) }
|
||||||
|
.ok_or_else(|| anyhow!("Failed to create shortcut memory stream"))?;
|
||||||
|
let persist: Com::IPersistStream = link.cast()?;
|
||||||
|
unsafe { persist.Save(&stream, true) }?;
|
||||||
|
let mut stat = Com::STATSTG::default();
|
||||||
|
unsafe { stream.Stat(&mut stat, Com::STATFLAG_NONAME) }?;
|
||||||
|
let size = usize::try_from(stat.cbSize).map_err(|_| anyhow!("Shortcut data is too large"))?;
|
||||||
|
let read_size = u32::try_from(size).map_err(|_| anyhow!("Shortcut data is too large"))?;
|
||||||
|
let mut bytes = vec![0; size];
|
||||||
|
let mut bytes_read = 0;
|
||||||
|
unsafe {
|
||||||
|
stream.Seek(0, Com::STREAM_SEEK_SET, None)?;
|
||||||
|
stream
|
||||||
|
.Read(bytes.as_mut_ptr().cast(), read_size, Some(&mut bytes_read))
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
if bytes_read != read_size {
|
||||||
|
bail!("Failed to read complete shortcut data");
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn embedded_shortcut_commands(bytes: Vec<u8>, filename: &str, name: &str) -> String {
|
||||||
|
let encoded = STANDARD.encode(bytes);
|
||||||
|
let encoded_path = format!("%~f0.{name}.b64");
|
||||||
|
format!(
|
||||||
|
"> \"{encoded_path}\" echo {encoded}\r\n\
|
||||||
|
certutil -f -decode \"{encoded_path}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn embedded_tray_shortcut_commands(
|
||||||
|
app_name: &str,
|
||||||
|
exe: &str,
|
||||||
|
icon_location: Option<&str>,
|
||||||
|
) -> ResultType<String> {
|
||||||
|
let filename = format!("{app_name} Tray.lnk");
|
||||||
|
Ok(embedded_shortcut_commands(
|
||||||
|
shortcut_bytes(exe, Some("--tray"), icon_location)?,
|
||||||
|
&filename,
|
||||||
|
"tray_shortcut",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_install_value(value: &str) -> ResultType<()> {
|
||||||
|
if value.contains(['\0', '"', '%', '\r', '\n', '|', '<', '>']) {
|
||||||
|
bail!("Installer path or name contains characters unsafe for cmd.exe");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_system_executable(relative_path: &str) -> ResultType<PathBuf> {
|
||||||
|
let mut buffer = vec![0u16; Foundation::MAX_PATH as usize];
|
||||||
|
let len = unsafe { SystemInformation::GetSystemDirectoryW(Some(&mut buffer)) } as usize;
|
||||||
|
if len == 0 {
|
||||||
|
return Err(io::Error::last_os_error().into());
|
||||||
|
}
|
||||||
|
if len >= buffer.len() {
|
||||||
|
bail!("Windows system directory path is too long");
|
||||||
|
}
|
||||||
|
buffer.truncate(len);
|
||||||
|
let mut path = PathBuf::from(OsString::from_wide(&buffer));
|
||||||
|
path.push(relative_path);
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_known_folder(id: &windows::core::GUID) -> ResultType<PathBuf> {
|
||||||
|
let value = unsafe { SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None) }?;
|
||||||
|
let path = unsafe { value.to_string() };
|
||||||
|
unsafe { Com::CoTaskMemFree(Some(value.0.cast())) };
|
||||||
|
Ok(PathBuf::from(path?))
|
||||||
|
}
|
||||||
|
|
||||||
|
// `%VAR%` is expanded before cmd.exe executes even inside a quoted `set` assignment.
|
||||||
|
// Reject all `%` so a handoff path cannot alter the elevated bootstrap before hash verification.
|
||||||
|
// https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1
|
||||||
|
pub(super) fn path_for_cmd_environment(path: &Path) -> ResultType<&str> {
|
||||||
|
let value = path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Path is not valid Unicode: {:?}", path))?;
|
||||||
|
if value.contains(['\0', '"', '%', '\r', '\n']) {
|
||||||
|
bail!("Path is unsafe for an elevated cmd.exe handoff: {:?}", path);
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap assignments are parsed before DisableDelayedExpansion takes effect.
|
||||||
|
// Escape carets first so `^!` preserves each literal exclamation mark.
|
||||||
|
pub(super) fn path_for_cmd_assignment(path: &Path) -> ResultType<String> {
|
||||||
|
Ok(path_for_cmd_environment(path)?
|
||||||
|
.replace('^', "^^")
|
||||||
|
.replace('!', "^!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn trusted_install_environment() -> ResultType<String> {
|
||||||
|
let system = get_system_executable("")?;
|
||||||
|
let program_data = get_known_folder(&FOLDERID_ProgramData)?;
|
||||||
|
let public = get_known_folder(&FOLDERID_Public)?;
|
||||||
|
trusted_install_environment_from_paths(&system, &program_data, &public)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trusted_install_environment_from_paths(
|
||||||
|
system: &Path,
|
||||||
|
program_data: &Path,
|
||||||
|
public: &Path,
|
||||||
|
) -> ResultType<String> {
|
||||||
|
let windows = system
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow!("System directory has no parent"))?;
|
||||||
|
let cmd = system.join(CMD_RELATIVE_PATH);
|
||||||
|
// These paths are parsed once from the protected BAT, with delayed expansion disabled.
|
||||||
|
let system = path_for_cmd_environment(system)?;
|
||||||
|
let windows = path_for_cmd_environment(windows)?;
|
||||||
|
let cmd = path_for_cmd_environment(&cmd)?;
|
||||||
|
let program_data = path_for_cmd_environment(program_data)?;
|
||||||
|
let public = path_for_cmd_environment(public)?;
|
||||||
|
Ok(format!(
|
||||||
|
"set \"ComSpec={cmd}\" & set \"PATH={system}\" & \
|
||||||
|
set \"SystemRoot={windows}\" & set \"WINDIR={windows}\" & \
|
||||||
|
set \"ProgramData={program_data}\" & set \"PUBLIC={public}\" & \
|
||||||
|
set \"PATHEXT=.COM;.EXE;.BAT;.CMD\" & \
|
||||||
|
set \"NoDefaultCurrentDirectoryInExePath=1\""
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellComGuard;
|
||||||
|
|
||||||
|
impl Drop for ShellComGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { Com::CoUninitialize() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_shell_com() -> ResultType<Option<ShellComGuard>> {
|
||||||
|
let result = unsafe {
|
||||||
|
Com::CoInitializeEx(
|
||||||
|
None,
|
||||||
|
Com::COINIT_APARTMENTTHREADED | Com::COINIT_DISABLE_OLE1DDE,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result == Foundation::RPC_E_CHANGED_MODE {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if result.is_err() {
|
||||||
|
bail!(
|
||||||
|
"Failed to initialize COM: HRESULT 0x{:08X}",
|
||||||
|
result.0 as u32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Some(ShellComGuard))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProcessHandle(HANDLE);
|
||||||
|
|
||||||
|
impl Drop for ProcessHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = unsafe { CloseHandle(self.0) } {
|
||||||
|
log::warn!("Failed to close elevated process handle: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elevated_working_directory(executable: &Path) -> ResultType<&Path> {
|
||||||
|
executable
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow!("Elevated executable has no parent directory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn run_elevated_and_wait(
|
||||||
|
executable: &Path,
|
||||||
|
parameters: &str,
|
||||||
|
show: bool,
|
||||||
|
) -> ResultType<u32> {
|
||||||
|
let parameter_chars = parameters.encode_utf16().count();
|
||||||
|
if parameter_chars >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS {
|
||||||
|
bail!("Elevated command is too long: {parameter_chars} UTF-16 characters");
|
||||||
|
}
|
||||||
|
let _com = initialize_shell_com()?;
|
||||||
|
let verb = wide_string("runas");
|
||||||
|
let working_directory = wide_string(path_for_cmd_environment(elevated_working_directory(
|
||||||
|
executable,
|
||||||
|
)?)?);
|
||||||
|
let executable = wide_string(path_for_cmd_environment(executable)?);
|
||||||
|
let parameters = wide_string(parameters);
|
||||||
|
let mut info = Shell::SHELLEXECUTEINFOW::default();
|
||||||
|
info.cbSize = mem::size_of::<Shell::SHELLEXECUTEINFOW>() as u32;
|
||||||
|
info.fMask = Shell::SEE_MASK_NOCLOSEPROCESS | Shell::SEE_MASK_NOASYNC;
|
||||||
|
info.lpVerb = PCWSTR(verb.as_ptr());
|
||||||
|
info.lpFile = PCWSTR(executable.as_ptr());
|
||||||
|
info.lpParameters = PCWSTR(parameters.as_ptr());
|
||||||
|
info.lpDirectory = PCWSTR(working_directory.as_ptr());
|
||||||
|
info.nShow = if show {
|
||||||
|
WindowsAndMessaging::SW_SHOWNORMAL.0
|
||||||
|
} else {
|
||||||
|
WindowsAndMessaging::SW_HIDE.0
|
||||||
|
};
|
||||||
|
unsafe { Shell::ShellExecuteExW(&mut info) }?;
|
||||||
|
if info.hProcess.0.is_null() {
|
||||||
|
bail!("Windows did not return an elevated process handle");
|
||||||
|
}
|
||||||
|
let process = ProcessHandle(info.hProcess);
|
||||||
|
let wait_result = unsafe { Threading::WaitForSingleObject(process.0, Threading::INFINITE) };
|
||||||
|
if wait_result == Foundation::WAIT_FAILED {
|
||||||
|
return Err(io::Error::last_os_error().into());
|
||||||
|
}
|
||||||
|
if wait_result != Foundation::WAIT_OBJECT_0 {
|
||||||
|
bail!("Unexpected elevated process wait result: {}", wait_result.0);
|
||||||
|
}
|
||||||
|
let mut exit_code = 0;
|
||||||
|
unsafe { Threading::GetExitCodeProcess(process.0, &mut exit_code) }?;
|
||||||
|
Ok(exit_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape `^` before using it to escape `&` so both survive nested cmd.exe parsing.
|
||||||
|
// https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc754250(v=ws.11)
|
||||||
|
pub(super) fn escape_nested_cmd_ampersands(value: &str) -> String {
|
||||||
|
value.replace('^', "^^").replace('&', "^&")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_values_enforce_command_safety() {
|
||||||
|
assert!(validate_install_value(r"C:\safe ! path").is_ok());
|
||||||
|
assert!(validate_install_value(r"C:\Program Files (x86)\RustDesk").is_ok());
|
||||||
|
assert!(validate_install_value(r"C:\Users\R&D\RustDesk.exe").is_ok());
|
||||||
|
assert!(validate_install_value(r"C:\A&^ B\RustDesk.exe").is_ok());
|
||||||
|
for character in ['\0', '"', '%', '\r', '\n', '|', '<', '>'] {
|
||||||
|
let value = format!(r"C:\unsafe{character}path");
|
||||||
|
assert!(
|
||||||
|
validate_install_value(&value).is_err(),
|
||||||
|
"cmd.exe control character was accepted: {character:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_commands_escape_while_protected_environment_preserves_carets() {
|
||||||
|
assert_eq!(
|
||||||
|
escape_nested_cmd_ampersands(r"C:\A&^ B\RustDesk.exe"),
|
||||||
|
r"C:\A^&^^ B\RustDesk.exe"
|
||||||
|
);
|
||||||
|
let path = Path::new(r"C:\Win^Root\System32");
|
||||||
|
let environment = trusted_install_environment_from_paths(path, path, path).unwrap();
|
||||||
|
assert!(environment.contains(r#"set "PATH=C:\Win^Root\System32""#));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user