// -------------------------------------------------------------------------------------------------------------------- // // 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; } } }