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