diff --git a/README.md b/README.md index 59f8acbec..8dbd037db 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ The following protocols are supported: * rlogin (Remote Login) * Raw Socket Connections * Powershell remoting +* AnyDesk For a detailed feature list and general usage support, refer to the [Documentation](https://mremoteng.readthedocs.io/en/latest/). diff --git a/mRemoteNG/Connection/Protocol/AnyDesk/ProtocolAnyDesk.cs b/mRemoteNG/Connection/Protocol/AnyDesk/ProtocolAnyDesk.cs new file mode 100644 index 000000000..8a7a00a66 --- /dev/null +++ b/mRemoteNG/Connection/Protocol/AnyDesk/ProtocolAnyDesk.cs @@ -0,0 +1,411 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using System.Windows.Forms; +using mRemoteNG.App; +using mRemoteNG.Messages; +using mRemoteNG.Resources.Language; + +namespace mRemoteNG.Connection.Protocol.AnyDesk +{ + [SupportedOSPlatform("windows")] + public class ProtocolAnyDesk : ProtocolBase + { + #region Private Fields + + private IntPtr _handle; + private readonly ConnectionInfo _connectionInfo; + private Process _process; + private const string DefaultAnydeskPath = @"C:\Program Files (x86)\AnyDesk\AnyDesk.exe"; + private const string AlternateAnydeskPath = @"C:\Program Files\AnyDesk\AnyDesk.exe"; + + #endregion + + #region Constructor + + public ProtocolAnyDesk(ConnectionInfo connectionInfo) + { + _connectionInfo = connectionInfo; + } + + #endregion + + #region Public Methods + + public override bool Initialize() + { + return base.Initialize(); + } + + public override bool Connect() + { + try + { + Runtime.MessageCollector?.AddMessage(MessageClass.InformationMsg, + "Attempting to start AnyDesk connection.", true); + + // Validate AnyDesk installation + string anydeskPath = FindAnydeskExecutable(); + if (string.IsNullOrEmpty(anydeskPath)) + { + Runtime.MessageCollector?.AddMessage(MessageClass.ErrorMsg, + "AnyDesk is not installed. Please install AnyDesk to use this protocol.", true); + return false; + } + + // Validate connection info + if (string.IsNullOrEmpty(_connectionInfo.Hostname)) + { + Runtime.MessageCollector?.AddMessage(MessageClass.ErrorMsg, + "AnyDesk ID is required in the Hostname field.", true); + return false; + } + + // Start AnyDesk connection + if (!StartAnydeskConnection(anydeskPath)) + { + return false; + } + + return true; + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage(Language.ConnectionFailed, ex); + return false; + } + } + + public override void Focus() + { + try + { + if (_handle != IntPtr.Zero) + { + NativeMethods.SetForegroundWindow(_handle); + } + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage(Language.IntAppFocusFailed, ex); + } + } + + protected override void Resize(object sender, EventArgs e) + { + try + { + if (_handle == IntPtr.Zero || InterfaceControl.Size == Size.Empty) + return; + + // Use ClientRectangle to account for padding (for connection frame color) + Rectangle clientRect = InterfaceControl.ClientRectangle; + NativeMethods.MoveWindow(_handle, + clientRect.X - SystemInformation.FrameBorderSize.Width, + clientRect.Y - (SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height), + clientRect.Width + SystemInformation.FrameBorderSize.Width * 2, + clientRect.Height + SystemInformation.CaptionHeight + + SystemInformation.FrameBorderSize.Height * 2, true); + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage(Language.IntAppResizeFailed, ex); + } + } + + public override void Close() + { + try + { + // Try to close all AnyDesk processes related to this connection + if (_process != null) + { + try + { + if (!_process.HasExited) + { + _process.Kill(); + } + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage(Language.IntAppKillFailed, ex); + } + finally + { + _process?.Dispose(); + _process = null; + } + } + + // Also try to close by window handle if we have it + if (_handle != IntPtr.Zero) + { + try + { + NativeMethods.SendMessage(_handle, 0x0010, IntPtr.Zero, IntPtr.Zero); // WM_CLOSE + } + catch + { + // Ignore errors when closing by handle + } + _handle = IntPtr.Zero; + } + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage("Error closing AnyDesk connection.", ex); + } + + base.Close(); + } + + #endregion + + #region Private Methods + + private string FindAnydeskExecutable() + { + // Check common installation paths + if (File.Exists(DefaultAnydeskPath)) + { + return DefaultAnydeskPath; + } + + if (File.Exists(AlternateAnydeskPath)) + { + return AlternateAnydeskPath; + } + + // Check if it's in PATH + string pathVariable = Environment.GetEnvironmentVariable("PATH"); + if (pathVariable != null) + { + var paths = pathVariable.Split(Path.PathSeparator); + foreach (var path in paths) + { + var exePath = Path.Combine(path.Trim(), "AnyDesk.exe"); + if (File.Exists(exePath)) + { + return exePath; + } + } + } + + return null; + } + + private bool StartAnydeskConnection(string anydeskPath) + { + try + { + // Build AnyDesk arguments + // Format: AnyDesk.exe [ID|alias@ad] [options] + // Hostname field contains the AnyDesk ID (e.g., 123456789 or alias@ad) + // Username field is optional and not used in the CLI (reserved for future use) + // Password field is piped via stdin when --with-password flag is used + string anydeskId = _connectionInfo.Hostname.Trim(); + string arguments = $"{anydeskId}"; + + // Add --with-password flag if password is provided + bool hasPassword = !string.IsNullOrEmpty(_connectionInfo.Password); + if (hasPassword) + { + arguments += " --with-password"; + } + + // Add --plain flag to minimize UI (optional) + arguments += " --plain"; + + Runtime.MessageCollector?.AddMessage(MessageClass.InformationMsg, + $"Starting AnyDesk with ID: {anydeskId}", true); + + // If password is provided, we need to pipe it to AnyDesk + if (hasPassword) + { + return StartAnydeskWithPassword(anydeskPath, arguments); + } + else + { + return StartAnydeskWithoutPassword(anydeskPath, arguments); + } + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage("Failed to start AnyDesk connection.", ex); + return false; + } + } + + private bool StartAnydeskWithPassword(string anydeskPath, string arguments) + { + try + { + // Use PowerShell to pipe the password to AnyDesk + // This is the recommended way according to AnyDesk documentation + string escapedPassword = _connectionInfo.Password.Replace("'", "''"); + string powershellCommand = $"echo '{escapedPassword}' | & '{anydeskPath}' {arguments}"; + + _process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-WindowStyle Hidden -Command \"{powershellCommand}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = false, + RedirectStandardError = false, + RedirectStandardInput = false + }, + EnableRaisingEvents = true + }; + + _process.Exited += ProcessExited; + _process.Start(); + + // Wait for the AnyDesk window to appear + // Note: The window belongs to the AnyDesk process, not PowerShell + if (!WaitForAnydeskWindow()) + { + Runtime.MessageCollector?.AddMessage(MessageClass.WarningMsg, + "AnyDesk window did not appear within the expected time.", true); + return false; + } + + base.Connect(); + return true; + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage("Failed to start AnyDesk with password.", ex); + return false; + } + } + + private bool StartAnydeskWithoutPassword(string anydeskPath, string arguments) + { + try + { + _process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = anydeskPath, + Arguments = arguments, + UseShellExecute = true + }, + EnableRaisingEvents = true + }; + + _process.Exited += ProcessExited; + _process.Start(); + + // Wait for the AnyDesk window to appear + if (!WaitForAnydeskWindow()) + { + Runtime.MessageCollector?.AddMessage(MessageClass.WarningMsg, + "AnyDesk window did not appear within the expected time.", true); + return false; + } + + base.Connect(); + return true; + } + catch (Exception ex) + { + Runtime.MessageCollector?.AddExceptionMessage("Failed to start AnyDesk connection.", ex); + return false; + } + } + + private bool WaitForAnydeskWindow() + { + // Wait up to 10 seconds for AnyDesk window to appear + int maxWaitTime = 10000; // 10 seconds + int waitInterval = 100; // 100 ms + int elapsedTime = 0; + + while (elapsedTime < maxWaitTime) + { + // Find AnyDesk process by name + Process[] anydeskProcesses = Process.GetProcessesByName("AnyDesk"); + Process processToKeep = null; + try + { + foreach (Process anydeskProcess in anydeskProcesses) + { + try + { + anydeskProcess.Refresh(); + + // Try to get the main window handle + if (anydeskProcess.MainWindowHandle != IntPtr.Zero) + { + _handle = anydeskProcess.MainWindowHandle; + + // Store the actual AnyDesk process for later cleanup + // Dispose the PowerShell process if it's different + if (_process != null && _process.ProcessName != "AnyDesk") + { + _process.Exited -= ProcessExited; + _process = anydeskProcess; + _process.EnableRaisingEvents = true; + _process.Exited += ProcessExited; + processToKeep = anydeskProcess; + } + + // Try to integrate the window + if (InterfaceControl != null) + { + NativeMethods.SetParent(_handle, InterfaceControl.Handle); + Resize(this, new EventArgs()); + } + + return true; + } + } + catch + { + // Ignore errors for individual processes + } + } + } + finally + { + foreach (Process anydeskProcess in anydeskProcesses) + { + if (anydeskProcess != processToKeep) + { + anydeskProcess.Dispose(); + } + } + } + + Thread.Sleep(waitInterval); + elapsedTime += waitInterval; + } + + return false; + } + + private void ProcessExited(object sender, EventArgs e) + { + Event_Closed(this); + } + + #endregion + + #region Enumerations + + public enum Defaults + { + Port = 0 // AnyDesk doesn't use a traditional port from the client side + } + + #endregion + } +} diff --git a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs index 110a4b5db..3fddaa8a4 100644 --- a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs +++ b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs @@ -10,6 +10,7 @@ using System; using mRemoteNG.Connection.Protocol.PowerShell; using mRemoteNG.Connection.Protocol.WSL; using mRemoteNG.Connection.Protocol.Terminal; +using mRemoteNG.Connection.Protocol.AnyDesk; using mRemoteNG.Resources.Language; using System.Runtime.Versioning; @@ -53,6 +54,8 @@ namespace mRemoteNG.Connection.Protocol return new ProtocolWSL(connectionInfo); case ProtocolType.Terminal: return new ProtocolTerminal(connectionInfo); + case ProtocolType.AnyDesk: + return new ProtocolAnyDesk(connectionInfo); case ProtocolType.IntApp: if (connectionInfo.ExtApp == "") { diff --git a/mRemoteNG/Connection/Protocol/ProtocolType.cs b/mRemoteNG/Connection/Protocol/ProtocolType.cs index 20af662bd..d7a607e91 100644 --- a/mRemoteNG/Connection/Protocol/ProtocolType.cs +++ b/mRemoteNG/Connection/Protocol/ProtocolType.cs @@ -43,6 +43,9 @@ namespace mRemoteNG.Connection.Protocol [LocalizedAttributes.LocalizedDescription(nameof(Language.Terminal))] Terminal = 12, + [LocalizedAttributes.LocalizedDescription(nameof(Language.AnyDesk))] + AnyDesk = 13, + [LocalizedAttributes.LocalizedDescription(nameof(Language.ExternalTool))] IntApp = 20 } diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx index 83b9c5c7f..ee3317fee 100644 --- a/mRemoteNG/Language/Language.resx +++ b/mRemoteNG/Language/Language.resx @@ -159,6 +159,9 @@ Automatic update settings + + AnyDesk + ARD (Apple Remote Desktop) diff --git a/mRemoteNGDocumentation/protocols.rst b/mRemoteNGDocumentation/protocols.rst index 36f9c8d5d..454001d80 100644 --- a/mRemoteNGDocumentation/protocols.rst +++ b/mRemoteNGDocumentation/protocols.rst @@ -7,5 +7,6 @@ mRemoteNG supports several remote connection protocols. See each page for more d .. toctree:: :maxdepth: 1 + protocols/anydesk.rst protocols/rdp.rst \ No newline at end of file diff --git a/mRemoteNGDocumentation/protocols/anydesk.rst b/mRemoteNGDocumentation/protocols/anydesk.rst new file mode 100644 index 000000000..75feb2479 --- /dev/null +++ b/mRemoteNGDocumentation/protocols/anydesk.rst @@ -0,0 +1,125 @@ +**************** +AnyDesk Protocol +**************** + +AnyDesk is a remote desktop application that provides a fast and secure connection to remote computers. mRemoteNG supports connecting to AnyDesk through its command-line interface. + +Prerequisites +============= + +Before using the AnyDesk protocol in mRemoteNG, ensure that: + +1. AnyDesk is installed on your local machine +2. The AnyDesk executable is located in one of the default installation paths: + + - ``C:\Program Files (x86)\AnyDesk\AnyDesk.exe`` + - ``C:\Program Files\AnyDesk\AnyDesk.exe`` + - Or in your system's PATH environment variable + +Configuration +============= + +To configure an AnyDesk connection in mRemoteNG: + +Connection Properties +--------------------- + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Property + - Description + * - Protocol + - Select ``AnyDesk`` from the protocol dropdown + * - Hostname + - The AnyDesk ID or alias of the remote computer (e.g., ``123456789`` or ``mycomputer@ad``) + * - Username + - Reserved for future use (currently not used by AnyDesk CLI) + * - Password + - The password for unattended access (optional). If provided, it will be automatically passed to AnyDesk using the ``--with-password`` flag + +Usage Examples +============== + +Basic Connection +---------------- + +For a simple connection without password: + +- **Protocol**: AnyDesk +- **Hostname**: 123456789 + +This will launch AnyDesk and connect to the specified ID. You will be prompted to enter the password manually if required. + +Unattended Access +----------------- + +For automatic connection with password: + +- **Protocol**: AnyDesk +- **Hostname**: 123456789 +- **Password**: your_anydesk_password + +This will automatically pipe the password to AnyDesk for unattended access. + +Using Alias +----------- + +If you have configured an alias in AnyDesk: + +- **Protocol**: AnyDesk +- **Hostname**: mycomputer@ad +- **Password**: your_anydesk_password + +Features +======== + +- **Automatic Password Authentication**: When a password is provided, mRemoteNG uses PowerShell to pipe the password to AnyDesk as recommended by AnyDesk's CLI documentation +- **Window Integration**: The AnyDesk window is embedded within mRemoteNG's interface for a seamless experience +- **Plain Mode**: AnyDesk is launched with the ``--plain`` flag to minimize the AnyDesk UI and provide a cleaner connection experience + +Troubleshooting +=============== + +AnyDesk Not Found +----------------- + +If you receive an error that AnyDesk is not installed: + +1. Verify that AnyDesk is installed on your machine +2. Check that the executable exists in one of the default paths +3. Alternatively, add the AnyDesk installation directory to your system's PATH environment variable + +Connection Issues +----------------- + +If the connection fails or the window doesn't appear: + +1. Verify that the AnyDesk ID is correct +2. Check that the remote computer has AnyDesk running +3. Ensure the password is correct (for unattended access) +4. Check the message log in mRemoteNG for specific error messages + +Window Not Embedding +-------------------- + +If the AnyDesk window doesn't appear embedded in mRemoteNG: + +1. AnyDesk may take a few seconds to launch - wait up to 10 seconds +2. Some AnyDesk versions may not support window embedding +3. Check the Windows Task Manager to verify that AnyDesk.exe is running + +Notes +===== + +- The AnyDesk protocol works with the free version of AnyDesk +- For security reasons, passwords are never stored in plain text - they are encrypted by mRemoteNG +- The Username field is reserved for future use and is currently not utilized by the AnyDesk CLI +- AnyDesk connections are integrated into mRemoteNG's tabbed interface for easy management + +References +========== + +- `AnyDesk Command Line Interface Documentation `_ +- `AnyDesk Official Website `_