// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) Naos Project 2019. All rights reserved.
//
//
// If this is in a project then it is sourced from NuGet package,
// it will be overwritten with package update except in Naos.Recipes.WinRM source.
//
// --------------------------------------------------------------------------------------------------------------------
#if NaosWinRM
namespace Naos.WinRM
#else
namespace Naos.Recipes.WinRM
#endif
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Cryptography;
///
/// Custom base exception to allow global catching of internally generated errors.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Rm", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Rm", Justification = "Name/spelling is correct.")]
[Serializable]
#if NaosWinRM
public
#else
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
abstract class NaosWinRmBaseException : Exception
{
///
/// Initializes a new instance of the class.
///
protected NaosWinRmBaseException()
: base()
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
protected NaosWinRmBaseException(string message)
: base(message)
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
/// Inner exception.
protected NaosWinRmBaseException(string message, Exception innerException)
: base(message, innerException)
{
}
///
/// Initializes a new instance of the class.
///
/// Serialization info.
/// Reading context.
protected NaosWinRmBaseException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
///
/// Custom exception for when trying to execute
///
[Serializable]
#if NaosWinRM
public
#else
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
class TrustedHostMissingException : NaosWinRmBaseException
{
///
/// Initializes a new instance of the class.
///
public TrustedHostMissingException()
: base()
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
public TrustedHostMissingException(string message)
: base(message)
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
/// Inner exception.
public TrustedHostMissingException(string message, Exception innerException)
: base(message, innerException)
{
}
///
/// Initializes a new instance of the class.
///
/// Serialization info.
/// Reading context.
protected TrustedHostMissingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
///
/// Custom exception for when things go wrong running remote commands.
///
[Serializable]
#if NaosWinRM
public
#else
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
class RemoteExecutionException : NaosWinRmBaseException
{
///
/// Initializes a new instance of the class.
///
public RemoteExecutionException()
: base()
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
public RemoteExecutionException(string message)
: base(message)
{
}
///
/// Initializes a new instance of the class.
///
/// Exception message.
/// Inner exception.
public RemoteExecutionException(string message, Exception innerException)
: base(message, innerException)
{
}
///
/// Initializes a new instance of the class.
///
/// Serialization info.
/// Reading context.
protected RemoteExecutionException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
///
/// Manages various remote tasks on a machine using the WinRM protocol.
///
#if NaosWinRM
public
#else
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
interface IManageMachines
{
///
/// Gets the IP address of the machine being managed.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
string IpAddress { get; }
///
/// Executes a user initiated reboot.
///
/// Can override default behavior of a forceful reboot (kick users off).
void Reboot(bool force = true);
///
/// Sends a file to the remote machine at the provided file path on that target computer.
///
/// File path to write the contents to on the remote machine.
/// Payload to write to the file.
/// Optionally writes the bytes in appended mode or not (default is NOT).
/// Optionally will overwrite a file that is already there [can NOT be used with 'appended'] (default is NOT).
void SendFile(string filePathOnTargetMachine, byte[] fileContents, bool appended = false, bool overwrite = false);
///
/// Retrieves a file from the remote machines and returns a checksum verified byte array.
///
/// File path to fetch the contents of on the remote machine.
/// Bytes of the specified files (throws if missing).
byte[] RetrieveFile(string filePathOnTargetMachine);
///
/// Runs an arbitrary command using "CMD.exe /c".
///
/// Command to run in "CMD.exe".
/// Parameters to be passed to the command.
/// Console output of the command.
string RunCmd(string command, ICollection commandParameters = null);
///
/// Runs an arbitrary command using "CMD.exe /c" on localhost instead of the provided remote computer..
///
/// Command to run in "CMD.exe".
/// Parameters to be passed to the command.
/// Console output of the command.
string RunCmdOnLocalhost(string command, ICollection commandParameters = null);
///
/// Runs an arbitrary script block on localhost instead of the provided remote computer.
///
/// Script block.
/// Parameters to be passed to the script block.
/// Collection of objects that were the output from the script block.
ICollection RunScriptOnLocalhost(string scriptBlock, ICollection scriptBlockParameters = null);
///
/// Runs an arbitrary script block.
///
/// Script block.
/// Parameters to be passed to the script block.
/// Collection of objects that were the output from the script block.
ICollection RunScript(string scriptBlock, ICollection scriptBlockParameters = null);
}
///
#if NaosWinRM
public
#else
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
class MachineManager : IManageMachines
{
private readonly long fileChunkSizeThresholdByteCount;
private readonly long fileChunkSizePerSend;
private readonly long fileChunkSizePerRetrieve;
private readonly string userName;
private readonly SecureString password;
private readonly bool autoManageTrustedHosts;
private static readonly object SyncTrustedHosts = new object();
///
/// Initializes a new instance of the class.
///
/// IP address of machine to interact with.
/// Username to use to connect.
/// Password to use to connect.
/// Optionally specify whether to update the TrustedHost list prior to execution or assume it's handled elsewhere (default is FALSE).
/// Optionally specify file size that will trigger chunking the file rather than sending as one file (150000 is default).
/// Optionally specify size of each chunk that is sent when a file is being chunked for send.
/// Optionally specify size of each chunk that is received when a file is being chunked for fetch.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "byte", Justification = "Name/spelling is correct.")]
public MachineManager(
string ipAddress,
string userName,
SecureString password,
bool autoManageTrustedHosts = false,
long fileChunkSizeThresholdByteCount = 150000,
long fileChunkSizePerSend = 100000,
long fileChunkSizePerRetrieve = 100000)
{
this.IpAddress = ipAddress;
this.userName = userName;
this.password = password;
this.autoManageTrustedHosts = autoManageTrustedHosts;
this.fileChunkSizeThresholdByteCount = fileChunkSizeThresholdByteCount;
this.fileChunkSizePerSend = fileChunkSizePerSend;
this.fileChunkSizePerRetrieve = fileChunkSizePerRetrieve;
}
///
/// Locally updates the trusted hosts to have the ipAddress provided.
///
/// IP Address to add to local trusted hosts.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "notUsedOutput", Justification = "Prefer to see that output is generated and not used...")]
public static void AddIpAddressToLocalTrustedHosts(string ipAddress)
{
lock (SyncTrustedHosts)
{
var currentTrustedHosts = GetListOfIpAddressesFromLocalTrustedHosts().ToList();
if (!currentTrustedHosts.Contains(ipAddress) && !TrustedHostListIsWildCard(currentTrustedHosts))
{
currentTrustedHosts.Add(ipAddress);
var newValue = currentTrustedHosts.Any() ? string.Join(",", currentTrustedHosts) : ipAddress;
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var command = new Command("Set-Item");
command.Parameters.Add("Path", @"WSMan:\localhost\Client\TrustedHosts");
command.Parameters.Add("Value", newValue);
command.Parameters.Add("Force", true);
var notUsedOutput = RunLocalCommand(runspace, command);
}
}
}
}
///
/// Locally updates the trusted hosts to remove the ipAddress provided (if applicable).
///
/// IP Address to remove from local trusted hosts.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "notUsedOutput", Justification = "Prefer to see that output is generated and not used...")]
public static void RemoveIpAddressFromLocalTrustedHosts(string ipAddress)
{
lock (SyncTrustedHosts)
{
var currentTrustedHosts = GetListOfIpAddressesFromLocalTrustedHosts().ToList();
if (currentTrustedHosts.Contains(ipAddress))
{
currentTrustedHosts.Remove(ipAddress);
// can't pass null must be an empty string...
var newValue = currentTrustedHosts.Any() ? string.Join(",", currentTrustedHosts) : string.Empty;
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var command = new Command("Set-Item");
command.Parameters.Add("Path", @"WSMan:\localhost\Client\TrustedHosts");
command.Parameters.Add("Value", newValue);
command.Parameters.Add("Force", true);
var notUsedOutput = RunLocalCommand(runspace, command);
}
}
}
}
///
/// Locally updates the trusted hosts to have the ipAddress provided.
///
/// List of the trusted hosts.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Want a method due to amount of logic.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip", Justification = "Name/spelling is correct.")]
public static IReadOnlyCollection GetListOfIpAddressesFromLocalTrustedHosts()
{
lock (SyncTrustedHosts)
{
try
{
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var command = new Command("Get-Item");
command.Parameters.Add("Path", @"WSMan:\localhost\Client\TrustedHosts");
var response = RunLocalCommand(runspace, command);
var valueProperty = response.Single().Properties.Single(_ => _.Name == "Value");
var value = valueProperty.Value.ToString();
var ret = string.IsNullOrEmpty(value) ? new string[0] : value.Split(',');
return ret;
}
}
catch (RemoteExecutionException remoteException)
{
// if we don't have any trusted hosts then just ignore...
if (
remoteException.Message.Contains(
"Cannot find path 'WSMan:\\localhost\\Client\\TrustedHosts' because it does not exist."))
{
return new List();
}
throw;
}
}
}
///
public string IpAddress { get; private set; }
///
public void Reboot(bool force = true)
{
var forceAddIn = force ? " -Force" : string.Empty;
var restartScriptBlock = "{ Restart-Computer" + forceAddIn + " }";
this.RunScript(restartScriptBlock);
}
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Disposal logic is correct.")]
public void SendFile(string filePathOnTargetMachine, byte[] fileContents, bool appended = false, bool overwrite = false)
{
if (fileContents == null)
{
throw new ArgumentNullException(nameof(fileContents));
}
if (appended && overwrite)
{
throw new ArgumentException("Cannot run with overwrite AND appended.");
}
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var sessionObject = this.BeginSession(runspace);
var verifyFileDoesntExistScriptBlock = @"
{
param($filePath)
if (Test-Path $filePath)
{
throw ""File already exists at: $filePath""
}
}";
if (!appended && !overwrite)
{
this.RunScriptUsingSession(
verifyFileDoesntExistScriptBlock,
new[] { filePathOnTargetMachine },
runspace,
sessionObject);
}
var firstSendUsingSession = true;
if (fileContents.Length <= this.fileChunkSizeThresholdByteCount)
{
this.SendFileUsingSession(filePathOnTargetMachine, fileContents, appended, overwrite, runspace, sessionObject);
}
else
{
// deconstruct and send pieces as appended...
var nibble = new List();
foreach (byte currentByte in fileContents)
{
if (nibble.Count < this.fileChunkSizePerSend)
{
nibble.Add(currentByte);
}
else
{
nibble.Add(currentByte);
this.SendFileUsingSession(filePathOnTargetMachine, nibble.ToArray(), !firstSendUsingSession, overwrite && firstSendUsingSession, runspace, sessionObject);
firstSendUsingSession = false;
nibble.Clear();
}
}
// flush the "buffer"...
if (nibble.Any())
{
this.SendFileUsingSession(filePathOnTargetMachine, nibble.ToArray(), true, false, runspace, sessionObject);
nibble.Clear();
}
}
var expectedChecksum = ComputeSha256Hash(fileContents);
var verifyChecksumScriptBlock = @"
{
param($filePath, $expectedChecksum)
$fileToCheckFileInfo = New-Object System.IO.FileInfo($filePath)
if (-not $fileToCheckFileInfo.Exists)
{
# If the file can't be found, try looking for it in the current directory.
$fileToCheckFileInfo = New-Object System.IO.FileInfo($filePath)
if (-not $fileToCheckFileInfo.Exists)
{
throw ""Can't find the file specified to calculate a checksum on: $filePath""
}
}
$fileToCheckFileStream = $fileToCheckFileInfo.OpenRead()
$provider = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
$hashBytes = $provider.ComputeHash($fileToCheckFileStream)
$fileToCheckFileStream.Close()
$fileToCheckFileStream.Dispose()
$base64 = [System.Convert]::ToBase64String($hashBytes)
$calculatedChecksum = [System.String]::Empty
foreach ($byte in $hashBytes)
{
$calculatedChecksum = $calculatedChecksum + $byte.ToString(""X2"")
}
if($calculatedChecksum -ne $expectedChecksum)
{
Write-Error ""Checksums don't match on File: $filePath - Expected: $expectedChecksum - Actual: $calculatedChecksum""
}
}";
this.RunScriptUsingSession(
verifyChecksumScriptBlock,
new[] { filePathOnTargetMachine, expectedChecksum },
runspace,
sessionObject);
this.EndSession(sessionObject, runspace);
runspace.Close();
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "notUsedResults", Justification = "Prefer to see that output is generated and not used...")]
private void SendFileUsingSession(
string filePathOnTargetMachine,
byte[] fileContents,
bool appended,
bool overwrite,
Runspace runspace,
object sessionObject)
{
if (appended && overwrite)
{
throw new ArgumentException("Cannot run with overwrite AND appended.");
}
var commandName = appended ? "Add-Content" : "Set-Content";
var forceAddIn = overwrite ? " -Force" : string.Empty;
var sendFileScriptBlock = @"
{
param($filePath, $fileContents)
$parentDir = Split-Path $filePath
if (-not (Test-Path $parentDir))
{
md $parentDir | Out-Null
}
" + commandName + @" -Path $filePath -Encoding Byte -Value $fileContents" + forceAddIn + @"
}";
var arguments = new object[] { filePathOnTargetMachine, fileContents };
var notUsedResults = this.RunScriptUsingSession(sendFileScriptBlock, arguments, runspace, sessionObject);
}
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Disposal logic is correct.")]
public byte[] RetrieveFile(string filePathOnTargetMachine)
{
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var sessionObject = this.BeginSession(runspace);
var verifyFileExistsScriptBlock = @"
{
param($filePath)
if (-not (Test-Path $filePath))
{
throw ""File doesn't exist at: $filePath""
}
$file = ls $filePath
Write-Output $file.Length
}";
var fileSizeRaw = this.RunScriptUsingSession(
verifyFileExistsScriptBlock,
new[] { filePathOnTargetMachine },
runspace,
sessionObject);
var fileSize = (long)long.Parse(fileSizeRaw.Single().ToString());
var getChecksumScriptBlock = @"
{
param($filePath)
$fileToCheckFileInfo = New-Object System.IO.FileInfo($filePath)
if (-not $fileToCheckFileInfo.Exists)
{
# If the file can't be found, try looking for it in the current directory.
$fileToCheckFileInfo = New-Object System.IO.FileInfo($filePath)
if (-not $fileToCheckFileInfo.Exists)
{
throw ""Can't find the file specified to calculate a checksum on: $filePath""
}
}
$fileToCheckFileStream = $fileToCheckFileInfo.OpenRead()
$provider = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
$hashBytes = $provider.ComputeHash($fileToCheckFileStream)
$fileToCheckFileStream.Close()
$fileToCheckFileStream.Dispose()
$base64 = [System.Convert]::ToBase64String($hashBytes)
$calculatedChecksum = [System.String]::Empty
foreach ($byte in $hashBytes)
{
$calculatedChecksum = $calculatedChecksum + $byte.ToString(""X2"")
}
# trimming off leading and trailing curly braces '{ }'
$trimmedChecksum = $calculatedChecksum.Substring(1, $calculatedChecksum.Length - 2)
Write-Output $trimmedChecksum
}";
var remoteChecksumRaw = this.RunScriptUsingSession(
getChecksumScriptBlock,
new[] { filePathOnTargetMachine },
runspace,
sessionObject);
var remoteChecksum = remoteChecksumRaw.Single();
var bytes = new List();
if (fileSize <= this.fileChunkSizeThresholdByteCount)
{
var bytesRaw = this.RetrieveFileUsingSession(filePathOnTargetMachine, runspace, sessionObject);
bytes.AddRange(bytesRaw);
}
else
{
// deconstruct and fetch pieces...
var lastNibblePoint = 0;
for (var nibblePoint = 0; nibblePoint < fileSize; nibblePoint++)
{
if ((nibblePoint - lastNibblePoint) >= this.fileChunkSizePerRetrieve)
{
var remainingBytes = fileSize - nibblePoint;
var nibbleSize = remainingBytes < this.fileChunkSizePerRetrieve
? remainingBytes
: this.fileChunkSizePerRetrieve;
var nibble = this.RetrieveFileUsingSession(
filePathOnTargetMachine,
runspace,
sessionObject,
lastNibblePoint,
nibbleSize);
bytes.AddRange(nibble);
lastNibblePoint = nibblePoint;
}
}
}
var byteArray = bytes.ToArray();
var actualChecksum = ComputeSha256Hash(byteArray);
if (string.Equals(remoteChecksum.ToString(), actualChecksum.ToString(), StringComparison.CurrentCultureIgnoreCase))
{
throw new RemoteExecutionException("Checksum didn't match after file was downloaded.");
}
this.EndSession(sessionObject, runspace);
runspace.Close();
return byteArray;
}
}
private byte[] RetrieveFileUsingSession(string filePathOnTargetMachine, Runspace runspace, object sessionObject, long nibbleStart = 0, long nibbleSize = 0)
{
if (nibbleStart != 0 && nibbleSize == 0)
{
nibbleSize = this.fileChunkSizePerRetrieve;
}
var fetchFileScriptBlock = @"
{
param($filePath, $nibbleStart, $nibbleSize)
if (-not (Test-Path $filePath))
{
throw ""Expected file to fetch missing at: $filePath""
}
$allBytes = [System.IO.File]::ReadAllBytes($filePath)
if (($nibbleStart -eq 0) -and ($nibbleSize -eq 0))
{
Write-Output $allBytes
}
else
{
$nibble = new-object byte[] $nibbleSize
[Array]::Copy($allBytes, $nibbleStart, $nibble, 0, $nibbleSize)
Write-Output $nibble
}
}";
var arguments = new object[] { filePathOnTargetMachine, nibbleStart, nibbleSize };
var bytesRaw = this.RunScriptUsingSession(fetchFileScriptBlock, arguments, runspace, sessionObject);
var bytes = bytesRaw.Select(_ => (byte)byte.Parse(_.ToString())).ToArray();
return bytes;
}
///
public string RunCmd(string command, ICollection commandParameters = null)
{
var scriptBlock = BuildCmdScriptBlock(command, commandParameters);
var outputObjects = this.RunScript(scriptBlock);
var ret = string.Join(Environment.NewLine, outputObjects);
return ret;
}
///
public string RunCmdOnLocalhost(string command, ICollection commandParameters = null)
{
var scriptBlock = BuildCmdScriptBlock(command, commandParameters);
var outputObjects = this.RunScriptOnLocalhost(scriptBlock);
var ret = string.Join(Environment.NewLine, outputObjects);
return ret;
}
private static string BuildCmdScriptBlock(string command, ICollection commandParameters)
{
var line = " `\"" + command + "`\"";
foreach (var commandParameter in commandParameters ?? new List())
{
line += " `\"" + commandParameter + "`\"";
}
line = "\"" + line + "\"";
var scriptBlock = "{ &cmd.exe /c " + line + " 2>&1 | Write-Output }";
return scriptBlock;
}
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Disposal logic is correct.")]
public ICollection RunScriptOnLocalhost(string scriptBlock, ICollection scriptBlockParameters = null)
{
List ret;
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
// just send a null session for localhost execution
ret = this.RunScriptUsingSession(scriptBlock, scriptBlockParameters, runspace, null);
runspace.Close();
}
return ret;
}
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Disposal logic is correct.")]
public ICollection RunScript(string scriptBlock, ICollection scriptBlockParameters = null)
{
List ret;
using (var runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
var sessionObject = this.BeginSession(runspace);
ret = this.RunScriptUsingSession(scriptBlock, scriptBlockParameters, runspace, sessionObject);
this.EndSession(sessionObject, runspace);
runspace.Close();
}
return ret;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "unneededOutput", Justification = "Prefer to see that output is generated and not used...")]
private void EndSession(object sessionObject, Runspace runspace)
{
if (this.autoManageTrustedHosts)
{
RemoveIpAddressFromLocalTrustedHosts(this.IpAddress);
}
var removeSessionCommand = new Command("Remove-PSSession");
removeSessionCommand.Parameters.Add("Session", sessionObject);
var unneededOutput = RunLocalCommand(runspace, removeSessionCommand);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "MachineManager", Justification = "Name/spelling is correct.")]
private object BeginSession(Runspace runspace)
{
if (this.autoManageTrustedHosts)
{
AddIpAddressToLocalTrustedHosts(this.IpAddress);
}
var trustedHosts = GetListOfIpAddressesFromLocalTrustedHosts();
if (!trustedHosts.Contains(this.IpAddress) && !TrustedHostListIsWildCard(trustedHosts))
{
throw new TrustedHostMissingException(
"Cannot execute a remote command with out the IP address being added to the trusted hosts list. Please set MachineManager to handle this automatically or add the address manually: "
+ this.IpAddress);
}
var powershellCredentials = new PSCredential(this.userName, this.password);
var sessionOptionsCommand = new Command("New-PSSessionOption");
sessionOptionsCommand.Parameters.Add("OperationTimeout", 0);
sessionOptionsCommand.Parameters.Add("IdleTimeout", TimeSpan.FromMinutes(20).TotalMilliseconds);
var sessionOptionsObject = RunLocalCommand(runspace, sessionOptionsCommand).Single().BaseObject;
var sessionCommand = new Command("New-PSSession");
sessionCommand.Parameters.Add("ComputerName", this.IpAddress);
sessionCommand.Parameters.Add("Credential", powershellCredentials);
sessionCommand.Parameters.Add("SessionOption", sessionOptionsObject);
var sessionObject = RunLocalCommand(runspace, sessionCommand).Single().BaseObject;
return sessionObject;
}
private List RunScriptUsingSession(
string scriptBlock,
ICollection scriptBlockParameters,
Runspace runspace,
object sessionObject)
{
using (var powershell = PowerShell.Create())
{
powershell.Runspace = runspace;
Collection output;
// session will implicitly assume remote - if null then localhost...
if (sessionObject != null)
{
var variableNameArgs = "scriptBlockArgs";
var variableNameSession = "invokeCommandSession";
powershell.Runspace.SessionStateProxy.SetVariable(variableNameSession, sessionObject);
var argsAddIn = string.Empty;
if (scriptBlockParameters != null && scriptBlockParameters.Count > 0)
{
powershell.Runspace.SessionStateProxy.SetVariable(
variableNameArgs,
scriptBlockParameters.ToArray());
argsAddIn = " -ArgumentList $" + variableNameArgs;
}
var fullScript = "$sc = " + scriptBlock + Environment.NewLine + "Invoke-Command -Session $"
+ variableNameSession + argsAddIn + " -ScriptBlock $sc";
powershell.AddScript(fullScript);
output = powershell.Invoke();
}
else
{
var fullScript = "$sc = " + scriptBlock + Environment.NewLine + "Invoke-Command -ScriptBlock $sc";
powershell.AddScript(fullScript);
foreach (var scriptBlockParameter in scriptBlockParameters ?? new List())
{
powershell.AddArgument(scriptBlockParameter);
}
output = powershell.Invoke(scriptBlockParameters);
}
this.ThrowOnError(powershell, scriptBlock);
var ret = output.Cast().ToList();
return ret;
}
}
private static bool TrustedHostListIsWildCard(IReadOnlyCollection trustedHostList)
{
return trustedHostList.Count == 1 && trustedHostList.Single() == "*";
}
private static List RunLocalCommand(Runspace runspace, Command arbitraryCommand)
{
using (var powershell = PowerShell.Create())
{
powershell.Runspace = runspace;
powershell.Commands.AddCommand(arbitraryCommand);
var output = powershell.Invoke();
ThrowOnError(powershell, arbitraryCommand.CommandText, "localhost");
var ret = output.ToList();
return ret;
}
}
private void ThrowOnError(PowerShell powershell, string attemptedScriptBlock)
{
ThrowOnError(powershell, attemptedScriptBlock, this.IpAddress);
}
private static void ThrowOnError(PowerShell powershell, string attemptedScriptBlock, string ipAddress)
{
if (powershell.Streams.Error.Count > 0)
{
var errorString = string.Join(
Environment.NewLine,
powershell.Streams.Error.Select(
_ =>
(_.ErrorDetails == null ? null : _.ErrorDetails.ToString() + " at " + _.ScriptStackTrace)
?? (_.Exception == null ? "Naos.WinRM: No error message available" : _.Exception.ToString() + " at " + _.ScriptStackTrace)));
throw new RemoteExecutionException(
"Failed to run script (" + attemptedScriptBlock + ") on " + ipAddress + " got errors: "
+ errorString);
}
}
private static string ComputeSha256Hash(byte[] bytes)
{
using (var provider = new SHA256Managed())
{
var hashBytes = provider.ComputeHash(bytes);
var calculatedChecksum = string.Empty;
foreach (byte x in hashBytes)
{
calculatedChecksum += string.Format(CultureInfo.InvariantCulture, "{0:x2}", x);
}
return calculatedChecksum;
}
}
}
///
/// Manages various remote tasks on a machine using the WinRM protocol.
///
#if NaosWinRM
public
#else
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.CodeDom.Compiler.GeneratedCode("Naos.Recipes.WinRM", "See package version number")]
internal
#endif
static class StringExtensions
{
///
/// Converts the source string into a secure string. Caller should dispose of the secure string appropriately.
///
/// The source string.
/// A secure version of the source string.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is expected to dispose of object.")]
public static SecureString ToSecureString(this string source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
var result = new SecureString();
foreach (var character in source.ToCharArray())
{
result.AppendChar(character);
}
result.MakeReadOnly();
return result;
}
}
}