Files
mRemoteNG/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs

259 lines
13 KiB
C#

using System;
using System.Drawing;
using System.Runtime.Versioning;
using System.Windows.Forms;
using mRemoteNG.App;
using mRemoteNG.Messages;
using mRemoteNG.Resources.Language;
namespace mRemoteNG.Connection.Protocol.PowerShell
{
[SupportedOSPlatform("windows")]
public class ProtocolPowerShell(ConnectionInfo connectionInfo) : ProtocolBase
{
#region Private Fields
private IntPtr _handle;
private readonly ConnectionInfo _connectionInfo = connectionInfo;
private ConsoleControl.ConsoleControl _consoleControl;
#endregion
#region Public Methods
public override bool Connect()
{
try
{
Runtime.MessageCollector?.AddMessage(MessageClass.InformationMsg, "Attempting to start remote PowerShell session.", true);
_consoleControl = new ConsoleControl.ConsoleControl
{
Dock = DockStyle.Fill,
BackColor = ColorTranslator.FromHtml("#012456"),
ForeColor = Color.White,
IsInputEnabled = true,
Padding = new Padding(0, 20, 0, 0)
};
/*
* Prepair powershell script parameter and create script
*/
// Path to the Windows PowerShell executable; can be configured through options.
//string psExe = @"C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe"; //old ps
string psExe = @"C:\Program Files\PowerShell\7\pwsh.exe"; //new ps
//string psExe = @"%LocalAppData%\Microsoft\WindowsApps\wt.exe"; //test for terminal
// Maximum number of login attempts; can be configured through options.
int psLoginAttempts = 3;
string psUsername;
if (string.IsNullOrEmpty(_connectionInfo.Domain))
// Set the username without domain
psUsername = _connectionInfo.Username;
else
// Set the username to Domain\Username if Domain is not empty
psUsername = $"{_connectionInfo.Domain}\\{_connectionInfo.Username}";
/*
* The PowerShell script is designed to facilitate multiple login attempts to a remote host using user-provided credentials,
* with an option to specify the maximum number of attempts.
* It handles username and password entry, attempts to establish a PSSession, and reports on login outcomes, ensuring a graceful exit in case of repeated failures.
*/
string psScriptBlock = $@"
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[String] $Hostname, # The hostname you want to connect to (mandatory parameter)
[String] $Username, # The username, if provided
[String] $Password, # The password for authentication
[int] $LoginAttempts = 3 # The number of login attempts, default set to 3
)
# Dynamically parameters
DynamicParam {{
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary;
# SecurePassword
$ParameterName = 'SecurePassword';
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute];
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute;
$ParameterAttribute.Mandatory = $False;
$AttributeCollection.Add($ParameterAttribute);
try {{
# Try converting the stored password to a secure string
$PSBoundParameters.$($ParameterName) = ConvertTo-SecureString $Password -AsPlainText -Force -ErrorAction Stop;
}}
catch{{
# Create an empty SecureString if the password cannot be converted (if the password is empty)
$PSBoundParameters.$($ParameterName) = [SecureString]::new();
}}
$PSBoundParameters.Password = $null;
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [SecureString], $AttributeCollection);
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter);
return $RuntimeParameterDictionary;
}}
process {{
# Initialize the $cred variable
$cred = $null;
# Check if a username is provided.
# Please note that some logins may not require a password. Therefore, the first attempt can fail if a username is set and a password is not.
if (-not [string]::IsNullOrEmpty($PSBoundParameters.Username)) {{
# Create a PSCredential object with the provided username and password
$cred = New-Object System.Management.Automation.PSCredential ($PSBoundParameters.Username, $PSBoundParameters.SecurePassword);
# It will be needed to determine whether the login credentials were provided or not.
$providedCred = $true;
}}
# At least one login attempt is required to ensure functionality
if ($LoginAttempts -lt 0) {{$LoginAttempts = 1;}}
# Loop for connection attempts for $LoginAttempts
for ($i = 0; $i -lt $LoginAttempts; $i++) {{
<#
The cases for when 'Get-Credential' is needed:
1. `$i -gt 0`: Indicates the first login attempt has failed.
2. `-not $cred`: Implies that no credentials have been sent to the function.
3. `$cred -and $cred.UserName -match ""^([^\\]+\\)$""`: Implies that only the regular Windows domain name is parameterized.
NOTE:
If the regular expression is used in an if statement such as if (.... -match ""^[^\\]+\\$"")...,
there will be conversion problems with the string. This can then lead to errors when executing PowerShell.
To work around this problem, create the $regex variable and enclose the expression in single quotes.
Due to the use of variables, double quotes are no longer required in the if statement, and it can be written as follows: if (.... -match $Regex)....
This approach avoids possible string conversion problems caused by double quotes.
#>
[string] $regex = '^[^\\]+\\$'
if ($i -gt 0 -or (-not $cred) -or ($cred -and $cred.Username -match $regex)){{
# Prompt for credentials with a message and pre-fill username if available
try {{
if (-not [string]::IsNullOrEmpty($cred.UserName)) {{
$cred = Get-Credential -Message $Hostname -UserName $cred.UserName -ErrorAction Stop;
}}
else {{
$cred = Get-Credential -Message $Hostname -ErrorAction Stop;
}}
$providedCred = $false; # provided creds are overwritten
}}
catch {{
# If something is wrong for $cred
$cred = $null
}}
}}
# Try PSSession
try {{
# If credentials are not provided, abort the loop (mean Get-Credential is canceled)
if ( $cred ) {{
Enter-PSSession -ComputerName $Hostname -Credential $cred -ErrorAction Stop;
break; # Successfully entered PSSession, exit the loop
}}
else {{
write-Host '{Language.PsCanceled}';
exit;
}}
}}
# Handle the case when PSSession entry fails
catch [System.Management.Automation.Remoting.PSRemotingTransportException]{{
If (-not $providedCred) {{
Write-Host '{Language.PsConnectionFailed}';
Write-Host;
}}
else {{
$LoginAttempts++;
}}
}}
catch {{
# Handle other exceptions
Write-Host $_.Exception.Message;
Write-Host;
Write-Host '{Language.PsFailed}';
exit;
}}
}}
# Maximum login attempts reached
if ($i -ge $LoginAttempts) {{
Write-Host '{Language.PsLoginAttempts}';
exit;
}}
}}
";
// Setup process for script with arguments
//* The -NoProfile parameter would be a valuable addition but should be able to be deactivated.
string arguments = $@"-NoExit -Command ""& {{ {psScriptBlock} }}"" -Hostname ""'{_connectionInfo.Hostname}'"" -Username ""'{psUsername}'"" -Password ""'{_connectionInfo.Password}'"" -LoginAttempts {psLoginAttempts}";
string hostname = _connectionInfo.Hostname.Trim().ToLower();
bool useLocalHost = hostname == "" || hostname.Equals("localhost");
if (useLocalHost)
{
arguments = $@"-NoExit";
}
_consoleControl.StartProcess(psExe, arguments);
while (!_consoleControl.IsHandleCreated) break;
_handle = _consoleControl.Handle;
NativeMethods.SetParent(_handle, InterfaceControl.Handle);
Resize(this, new EventArgs());
base.Connect();
return true;
}
catch (Exception ex)
{
Runtime.MessageCollector?.AddExceptionMessage(Language.ConnectionFailed, ex);
return false;
}
}
public override void Focus()
{
try
{
NativeMethods.SetForegroundWindow(_handle);
}
catch (Exception ex)
{
Runtime.MessageCollector.AddExceptionMessage(Language.IntAppFocusFailed, ex);
}
}
protected override void Resize(object sender, EventArgs e)
{
try
{
if (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);
}
}
#endregion
#region Enumerations
public enum Defaults
{
Port = 5985
}
#endregion
}
}