lib/TMD.SSH.ps1
Function Invoke-TMDSshScript { param( [CmdletBinding()] [Parameter(mandatory = $false)][String]$Hostname, [Parameter(mandatory = $false)][String]$IPAddress, [Parameter(mandatory = $false)][pscredential]$Credential, [Parameter(mandatory = $false)][String]$Command, [Parameter(mandatory = $false)]$Script, [Parameter(mandatory = $false)][String]$HostSSHKey = "", [Parameter(mandatory = $false)][String]$Username, [Parameter(mandatory = $false)][String]$PrivateKeyFilePath = "", [Parameter(mandatory = $false)][String]$HostSSHKeyFieldName, [Parameter(mandatory = $false)][Switch]$NoAgent, [Parameter(mandatory = $false)][Switch]$Console, [Parameter(mandatory = $false)][Switch]$TrustAnyServer, [Parameter(mandatory = $false)][Switch]$PassThru, [Parameter(mandatory = $false)][Switch]$UseSudo, [Parameter(mandatory = $false)][Switch]$VerboseSsh, [Parameter(mandatory = $false)][string]$SshCommandDelimiter = ';' ) Begin { ## Ensure Hostname is used. If it's not provided, use the IP address provided if (-not $Hostname -and $IPAddress) { $Hostname = $IPAddress } ## Windows uses Plink if ($isWindows) { ## Get the Plink Path $PlinkExeFilePath = 'C:\Program Files\PuTTY\plink.exe' If (-Not (Test-Path $PlinkExeFilePath)) { throw 'Putty is not installed. Download and install from: https://the.earth.li/~sgtatham/putty/latest/w64/putty-64bit-0.74-installer.msi' } ## Define the Standard Arguments String $PlinkStandardArguments = '-batch -ssh -t ' ## Add the correct Username to the SSH Request if (-Not $NoAgent) { $PlinkStandardArguments += '-agent ' } else { $PlinkStandardArguments += '-noagent ' } ## Add the correct Username to the SSH Request if ($Credential) { $PlinkStandardArguments += '-l ' + $Credential.UserName + ' ' } elseif ($Username) { $PlinkStandardArguments += '-l ' + $UserName + ' ' } ## Setup Standard Process Options $PlinkProcInfo = New-Object System.Diagnostics.ProcessStartInfo $PlinkProcInfo.FileName = $PlinkExeFilePath $PlinkProcInfo.UseShellExecute = $false $PlinkProcInfo.CreateNoWindow = $true ## Using Normal actually hides the window ## Because the CreateNoWindow is True $PlinkProcInfo.WindowStyle = 'Hidden' ## Redirect Standard Streams # $PlinkProcInfo.RedirectStandardError = $false ## True # $PlinkProcInfo.RedirectStandardOutput = $false ## True # $PlinkProcInfo.RedirectStandardInput = $false ## True $PlinkProcInfo.RedirectStandardError = $true ## True $PlinkProcInfo.RedirectStandardOutput = $true ## True $PlinkProcInfo.RedirectStandardInput = $true ## True ## If no Host SSH Key was passed, gather the one from the server if (-not $HostSSHKey) { ## Add an 'exit' command to the SSH connection to close once it's started (After the key is retrieved) $PlinkProcInfo.Arguments = $PlinkStandardArguments + ' ' + $Hostname + ' exit' ## Remove the -batch so the SSH Key Test allows input $PlinkProcInfo.Arguments = $PlinkProcInfo.Arguments -replace ' -batch', '' ## Run a "Key Test" SSH Session $PlinkKeyTestProc = New-Object System.Diagnostics.Process $PlinkKeyTestProc.StartInfo = $PlinkProcInfo try { ## Run the Putty Session [Void]$PlinkKeyTestProc.Start() # $PlinkKeyTestProc.StandardInput.WriteLine("y") # Keep reading Standard Out until the stream has ended while (!($PlinkProc.StandardOutput.EndOfStream)) { $Line = $PlinkProc.StandardOutput.ReadLine() ## If the string was ultimately empty if (-Not [string]::IsNullOrEmpty($Line.Trim())) { $global:PuttyStdOut.AppendLine($Line) | Out-Null if ($Console) { Write-Host $Line } } } ## Wait for the Process to Exit $PlinkKeyTestProc.WaitForExit() } catch { throw $_ } finally { $PlinkKeyTestProc.Close() $PlinkKeyTestProc.Dispose() } } } ## Mac SSH uses the native SSH command if ($IsMacOS) { ## Setup Standard Process Options $SshProcInfo = New-Object System.Diagnostics.ProcessStartInfo $SshProcInfo.FileName = '/usr/bin/ssh' # $SshProcInfo.UseShellExecute = $false # $SshProcInfo.CreateNoWindow = $true # $SshProcInfo.WindowStyle = 'Hidden' ## Redirect Standard Streams # $SshProcInfo.RedirectStandardError = $true # $SshProcInfo.RedirectStandardOutput = $true # $SshProcInfo.RedirectStandardInput = $true ## If no Host SSH Key was passed, gather the one from the server if (-not $HostSSHKey) { ## Add an 'exit' command to the SSH connection to close once it's started (After the key is retrieved) $SshProcInfo.Arguments = ' ' + $Hostname + ' exit' ## Run a "Key Test" SSH Session $SshProc = New-Object System.Diagnostics.Process $SshProc.StartInfo = $SshProcInfo try { ## Run the SSH Session [Void]$SshProc.Start() # $SshProc.StandardInput.WriteLine("y") ## Keep reading Standard Out until the stream has ended while (!($SshProc.StandardOutput.EndOfStream)) { $Line = $SshProc.StandardOutput.ReadLine() $global:PuttyStdOut.AppendLine($Line) | Out-Null if ($Console) { Write-Host $Line } } ## Wait for the Process to Exit $PlinkKeyTestProc.WaitForExit() } catch { throw $_ } finally { $PlinkKeyTestProc.Close() $PlinkKeyTestProc.Dispose() } } } } Process { # Windows uses Plink if ($isWindows) { ## Reset the Command Arguments $PlinkProcInfo.Arguments = $PlinkStandardArguments # $PlinkProcInfo.RedirectStandardInput = $true ## Add the SSH Host Key if ($VerboseSsh) { ## Adds verbose output $PlinkProcInfo.Arguments += ' -v' } ## Add Add Authentication Argument for Key pair or Password if ($PrivateKeyFilePath) { ## Authentication will occur with the Private Key file $PlinkProcInfo.Arguments += ' -i "' + $PrivateKeyFilePath + '"' } else { ## Authentication will use the User Password $PlinkProcInfo.Arguments += ' -pw "' + $Credential.GetNetworkCredential().Password + '"' } ## Add the SSH Host Key if ($HostSSHKey) { ## Authentication will use the User Password $PlinkProcInfo.Arguments += ' -hostkey "' + $HostSSHKey + '"' } ## Add hostname and command parameters $PlinkProcInfo.Arguments += ' ' + $Hostname ## Convert the Script into a Command Sequence if ($Script -and -not $Command) { $Commands = [System.Text.StringBuilder]::new() $Script.Values | ForEach-Object { [void]$Commands.Append($_) [void]$Commands.Append($SshCommandDelimiter) } $Command = $Commands.ToString() } ## Add Sudo to the Argument if required if ($UseSudo) { $Command = "/usr/bin/sudo bash -c '" + $Command + "'" ## This syntax is correct for wrapping the command into quotes # $SudoElevated = $False } ## Append the Command to the end $PlinkProcInfo.Arguments += ' "' + $Command + $SshCommandDelimiter + ' exit"' ## Open a new Process to run plink in $PlinkProc = New-Object System.Diagnostics.Process $PlinkProc.StartInfo = $PlinkProcInfo ## Create an Event Handler for Standard Out $global:PuttyStdOut = [System.Text.StringBuilder]::New() $global:PuttyStdErr = [System.Text.StringBuilder]::New() try { ## Start the Plink Process [Void]$PlinkProc.Start() ## Keep reading Standard Out until the stream has ended while (!($PlinkProc.StandardOutput.EndOfStream)) { $Line = $PlinkProc.StandardOutput.ReadLine() $global:PuttyStdOut.AppendLine($Line) | Out-Null if ($Console) { Write-Host $Line } } ## Then read all of Standard Err while (!($PlinkProc.StandardError.EndOfStream)) { $Line = $PlinkProc.StandardError.ReadLine() $global:PuttyStdErr.AppendLine($Line) | Out-Null Write-Host $Line } ## Wait for the Process to Exit $PlinkProc.WaitForExit() <## Disabling to replace this code with the more efficient code from the Get-SSHHostKey ## ## # while (-Not $PlinkProc.HasExited) { # ## Check Standard Output for content # if ($PlinkProc.StandardOutput.Peek()) { # # ## Read all of the standard out content # While (-Not $PlinkProc.StandardOutput.EndOfStream) { # $Line = $PlinkProc.StandardOutput.ReadLine() # if (-Not [string]::IsNullOrEmpty($Line.Trim())) { # $global:PuttyStdOut.AppendLine($Line) | Out-Null # if ($Console) { # Write-Host $Line # } # } # } # } # ## Check if there is content in the StdErr stream # # if ($PlinkProc.StandardError.Peek()) { # # Read it until the end of the stream has been reached # # while (-Not $PlinkProc.StandardError.EndOfStream) { # # $Line = $PlinkProc.StandardError.ReadLine() # # $global:PuttyStdErr.AppendLine($Line) | Out-Null # # if ($Console) { # # Write-Host $Line # # } # # ## If Sudo Elevation is used, and the session has not yet elevated, and there is a password prompt # # if ($UseSudo -and (-Not $SudoElevated) -and ( # # ( $Line -like '[sudo] password*') ` # # -Or ( $Line -like 'Started a shell/command*')` # # )) { # # Write-Verbose 'Elevating via Sudo' # # $PlinkProc.StandardInput.WriteLine($Credential.GetNetworkCredential().Password) # # } # # } # # } # Start-Sleep -Milliseconds 25 # } #> } catch { throw $_ } finally { $PlinkProc.Close() $PlinkProc.Dispose() } ## Convert the output to a String $PuttyOut = $global:PuttyStdOut.toString().Trim() $PuttyErr = $global:PuttyStdErr.toString().Trim() ## Throw on Fatal Errors if ($PuttyErr) { throw $PuttyErr } } } End { ## Return the Session Output if ($PassThru) { return $PuttyOut } } } Function Get-TMDSshServerKey { [CmdletBinding(DefaultParameterSetName = 'ByHostName')] param( [Parameter(mandatory = $false, ParameterSetName = 'ByHostName')] [String]$Hostname, [Parameter(mandatory = $false, ParameterSetName = 'ByIPAddress')] [String]$IPAddress, [Parameter(mandatory = $false)] [String]$SaveToTMField, [Parameter(mandatory = $false)] [Switch]$Console ) Process { ## Ensure Hostname is used. If it's not provided, use the IP address provided $Hostname ??= $IPAddress ## If Windows if ($IsWindows) { ## Get the Plink Path $PlinkExeFilePath = 'C:\Program Files\PuTTY\plink.exe' If (-Not (Test-Path $PlinkExeFilePath)) { throw 'Putty is not installed. Download and install from: https://the.earth.li/~sgtatham/putty/latest/w64/putty-64bit-0.74-installer.msi' } ## Setup Standard Process Options $PlinkProcInfo = New-Object System.Diagnostics.ProcessStartInfo $PlinkProcInfo.FileName = $PlinkExeFilePath $PlinkProcInfo.UseShellExecute = $false $PlinkProcInfo.CreateNoWindow = $true $PlinkProcInfo.WindowStyle = 'Hidden' ## Redirect Standard Streams $PlinkProcInfo.RedirectStandardError = $true $PlinkProcInfo.RedirectStandardOutput = $true $PlinkProcInfo.RedirectStandardInput = $true $PlinkProcInfo.Arguments = '-agent -batch -v -ssh -t ' + $Hostname + ' exit' ## Open a new Process to run plink in $PlinkProc = New-Object System.Diagnostics.Process $PlinkProc.StartInfo = $PlinkProcInfo ## Create an Event Handler for Standard Out $global:PuttyStdOut = [System.Text.StringBuilder]::New() $global:PuttyStdErr = [System.Text.StringBuilder]::New() ## Write Console if requested if ($Console) { Write-Host "Getting SSH Key From Server: "$Hostname } try { ## Start the Plink Process [Void]$PlinkProc.Start() ## Keep reading Standard Out until the stream has ended while (!($PlinkProc.StandardOutput.EndOfStream)) { $Line = $PlinkProc.StandardOutput.ReadLine() $global:PuttyStdOut.AppendLine($Line) | Out-Null if ($Console) { Write-Host $Line } } ## Then read all of Standard Err while (!($PlinkProc.StandardError.EndOfStream)) { $Line = $PlinkProc.StandardError.ReadLine() $global:PuttyStdErr.AppendLine($Line) | Out-Null Write-Host $Line } ## Wait for the Process to Exit $PlinkProc.WaitForExit() } catch { throw $_ } finally { $PlinkProc.Close() $PlinkProc.Dispose() } ## Convert the output to a String $PuttyKeyCollectErrString = $global:PuttyStdErr.toString() # TODO: Check if this can be uncommented # ## Throw on Fatal Errors # if ($PuttyErr) { # Write-Host "SSH ERROR:" $PuttyKeyCollectErrString # } ## Get the SSH Key from the details returned $HostSSHKey = ($PuttyKeyCollectErrString -split $CRLF | Where-Object { $_ -like 'ssh-*' }) -split ' ' | Select-Object -Last 1 if ($Console) { Write-Host "Connection provided Server Key:" -NoNewline Write-Host $HostSSHKey -ForegroundColor Yellow } if ($SaveToTMField) { Write-Host "Saving the SSH Key to field [$SaveToTMField]" Update-TMTaskAsset -Field $SaveToTMField -Value $HostSSHKey } } return $HostSSHKey } } |