Portal.psm1

#
# Script module for module 'Portal'
#

#Requires -Version 7.1
#Requires -PSEdition Core

using namespace System.Management.Automation


#region Enum
enum Scopes {
    # Scopes in which a connection can be opened

    Undefined = 1
    Console = 2
    External = 3
}
#endregion Enums

#region Classes
class Item {
    # Name
    [ValidateLength(1, 50)]
    [ValidatePattern("^([a-zA-Z0-9_\-]+)$")]
    [string] $Name
    # Description
    [string] $Description

    [hashtable] Splat() {
        $Hashtable = [ordered] @{}
        foreach ($p in $this.PSObject.Properties) {
            $this.PSObject.Properties | ForEach-Object -Process {
                $Hashtable[$p.Name] = $p.Value
            }
        }
        return $Hashtable
    }
}
class Client : Item {
    # Executable
    [string] $Executable
    # Command template
    [string] $TokenizedArgs
    # Default port
    [UInt16] $DefaultPort
    # Default connection scope
    [Scopes] $DefaultScope
    # Does this client require a user
    [bool] $RequiresUser
    # Tokens
    static [string] $HostToken = "<host>"
    static [string] $PortToken = "<port>"
    static [string] $UserToken = "<user>"

    Client(
        [string] $Name,
        [string] $Executable,
        [string] $TokenizedArgs,
        [UInt16] $DefaultPort,
        [Scopes] $DefaultScope,
        [string] $Description
    ) {
        $this.Name = $Name
        $this.Executable = $Executable
        [Client]::ValidateTokenizedArgs($TokenizedArgs)
        $this.TokenizedArgs = $TokenizedArgs
        $this.DefaultPort = $DefaultPort
        $this.DefaultScope = $DefaultScope
        $this.RequiresUser = [Client]::UserTokenExists($TokenizedArgs)
        $this.Description = $Description
    }

    static [void] ValidateTokenizedArgs(
        [string] $TokenizedArgs
    ) {
        @(
            [Client]::HostToken,
            [Client]::PortToken
        ) | ForEach-Object -Process {
            if ($TokenizedArgs -notmatch $_) {
                throw "The argument line does not contain the following token: {0}." -f $_
            }
        }
    }

    hidden static [bool] UserTokenExists([string] $TokenizedArgs) {
        return $TokenizedArgs -match [Client]::UserToken
    }

    [string] GenerateArgs(
        [string] $Hostname,
        [UInt16] $Port
    ) {
        return $this.TokenizedArgs.Replace(
            [Client]::HostToken, $Hostname
        ).Replace(
            [Client]::PortToken, $Port
        )
    }

    [string] GenerateArgs(
        [string] $Hostname,
        [UInt16] $Port,
        [string] $User
    ) {
        return $this.GenerateArgs($Hostname, $Port).Replace(
            [Client]::UserToken, $User
        )
    }

    [string] ToString() {
        return "{0}, Description: `"{1}`", Scope: {2}, Command: `"{3} {4}`"" -f (
            $this.Name,
            $this.Description,
            $this.DefaultScope,
            $this.Executable,
            $this.TokenizedArgs.Replace(
                "<port>", "<port:{0}>" -f $this.DefaultPort
            )
        )
    }
}
class Connection : Item {
    # Hostname
    [ValidatePattern("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")]
    [string] $Hostname
    # Port
    [UInt16] $Port
    # Default client
    [string] $DefaultClient
    # Default user
    [string] $DefaultUser

    Connection(
        [String] $Name,
        [String] $Hostname,
        [UInt16] $Port,
        [string] $DefaultClient,
        [string] $DefaultUser,
        [string] $Description
    ) {
        $this.Name = $Name
        $this.Hostname = $Hostname.ToLower()
        $this.Port = $Port
        $this.DefaultClient = $DefaultClient
        $this.DefaultUser = $DefaultUser
        $this.Description = $Description
    }

    [bool] IsDefaultPort() {
        return $this.Port -eq 0
    }

    [string] ToString() {
        return "{0}, Description `"{1}`", Default client {2}, Target {3}:{4}" -f (
            $this.Name,
            $this.Description,
            $this.DefaultClient,
            $this.Hostname,
            $(
                if ($this.IsDefaultPort()) {
                    "default"
                }
                else {
                    $this.Port.ToString()
                }
            )
        )
    }
}
class Inventory {
    # Title for the inventory file
    [string] $Title = "Portal inventory"
    # description for the inventory file
    [string] $Description = "Portal inventory file where the connections and clients are stored"
    # Version of the inventory file
    [string] $Version = "0.1.0"
    # Path to the inventory file
    [string] $Path = [Inventory]::GetPath()
    # Collection of Clients
    [Client[]] $Clients
    # Collection of Connections
    [Connection[]] $Connections
    # Encoding for inventory file
    static [string] $Encoding = "utf-8"
    # Name of the environement variable to use a custom path to the inventory file
    static [string] $EnvVariable = "PORTAL_INVENTORY"

    static [string] GetPath() {
        foreach ($Target in @("Process", "User", "Machine")) {
            $Value = [System.Environment]::GetEnvironmentVariable(
                [Inventory]::EnvVariable,
                [System.EnvironmentVariableTarget]::"$Target"
            )
            if ($Value) { return $Value }
        }
        return Join-Path -Path $env:APPDATA -ChildPath "Portal/inventory.json"
    }

    [void] ReadFile() {
        # Get content from the file
        $GetContentParams = @{
            Path        = $this.Path
            Raw         = $true
            Encoding    = [Inventory]::Encoding
            ErrorAction = "Stop"
        }
        try {
            $Items = Get-Content @GetContentParams | ConvertFrom-Json -AsHashtable
        }
        catch {
            throw "Cannot open inventory: {0}" -f $_.Exception.Message
        }

        # Check version of the inventory
        if ($Items.Version -ne $this.Version) {
            throw (
                "Version of the inventory file is not supported.",
                "Current version: `"{0}`", Expected version: `"{1}`"" -f (
                    $Items.Version, $this.Version
                )
            )
        }

        # Re-initialize Clients array
        $this.Clients = @()

        # Add every Client to inventory object
        foreach ($c in $Items.Clients) {
            $this.Clients += New-Object -TypeName Client -ArgumentList @(
                $c.Name,
                $c.Executable,
                $c.TokenizedArgs,
                $c.DefaultPort,
                $c.DefaultScope,
                $c.Description
            )
        }

        # Check if Client name duplicates exist
        if ($this.ClientNameDuplicatesExist()) {
            Write-Warning -Message ("Fix the inventory by renaming the duplicated client names in the inventory file: {0}" -f (
                    [Inventory]::GetPath()
                )
            )
        }

        # Re-initialize Connections array
        $this.Connections = @()

        # Add every Connection to inventory object
        foreach ($c in $Items.Connections) {
            $this.Connections += New-Object -TypeName Connection -ArgumentList @(
                $c.Name,
                $c.Hostname,
                $c.Port,
                $c.DefaultClient,
                $c.DefaultUser,
                $c.Description
            )
        }

        # Check if Connection name duplicates exist
        if ($this.ConnectionNameDuplicatesExist()) {
            Write-Warning -Message (
                "Fix the inventory by renaming the duplicated connection names in the inventory file: {0}" -f (
                    [Inventory]::GetPath()
                )
            )
        }
    }

    [void] SaveFile() {
        $Items = [ordered] @{
            Title       = $this.Title
            Description = $this.Description
            Version     = $this.Version
            Clients     = @()
            Connections = @()
        }

        foreach ($c in $this.Clients) {
            $Items.Clients += $c.Splat()
        }

        foreach ($c in $this.Connections) {
            $Connection = $c.Splat()
            $Items.Connections += $Connection
        }

        $Json = ConvertTo-Json -InputObject $Items -Depth 3

        if (Test-Path -Path $this.Path -PathType Leaf) {
            $BackupPath = "{0}.backup" -f $this.Path
            Copy-Item -Path $this.Path -Destination $BackupPath -Force
        }
        else {
            New-Item -Path $this.Path -ItemType File -Force | Out-Null
        }

        Set-Content -Path $this.Path -Value $Json -Encoding ([Inventory]::Encoding) -Force
    }

    hidden [bool] ClientNameDuplicatesExist() {
        $Duplicates = $this.Clients
        | Group-Object -Property Name
        | Where-Object -Property Count -GT 1

        if ($Duplicates) {
            $Duplicates | ForEach-Object -Process {
                Write-Warning -Message ("It exists more than one client named `"{0}`"." -f $_.Name)
            }
            return $true
        }
        return $false
    }

    hidden [bool] ConnectionNameDuplicatesExist() {
        $Duplicates = $this.Connections
        | Group-Object -Property Name
        | Where-Object -Property Count -GT 1

        if ($Duplicates) {
            $Duplicates | ForEach-Object -Process {
                Write-Warning -Message ("It exists more than one connection named `"{0}`"." -f $_.Name)
            }
            return $true
        }
        return $false
    }

    [Client] GetClient([string] $Name) {
        return $this.Clients | Where-Object -Property Name -EQ $Name
    }

    [Connection] GetConnection([string] $Name) {
        return $this.Connections | Where-Object -Property Name -EQ $Name
    }

    [bool] ClientExists([string] $Name) {
        return $this.GetClient($Name).Count -gt 0
    }

    [bool] ConnectionExists([string] $Name) {
        return $this.GetConnection($Name).Count -gt 0
    }

    [void] AddClient([Client] $Client) {
        if ($this.ClientExists($Client.Name)) {
            throw "Cannot add Client `"{0}`" as it already exists." -f $Client.Name
        }
        $this.Clients += $Client
    }

    [void] AddConnection([Connection] $Connection) {
        if ($this.ConnectionExists($Connection.Name)) {
            throw "Cannot add Connection `"{0}`" as it already exists." -f $Connection.Name
        }
        $this.Connections += $Connection
    }

    [void] RemoveClient([string] $Name) {
        $this.Clients = $this.Clients | Where-Object -Property Name -NE $Name
    }

    [void] RemoveConnection([string] $Name) {
        $this.Connections = $this.Connections | Where-Object -Property Name -NE $Name
    }

    [void] RenameClient([string] $Name, [string] $NewName) {
        if ($Name -eq $NewName) {
            throw "The two names are similar."
        }
        elseif (-not $this.ClientExists($Name)) {
            throw "No Client `"{0}`" to rename." -f $Name
        }
        elseif ($this.ClientExists($NewName)) {
            throw "Cannot rename Client `"{0}`" to `"{1}`" as this name is already used." -f $Name, $NewName
        }

        for ($i = 0; $i -lt $this.Clients.count; $i++) {
            if ($this.Clients[$i].Name -eq $Name) {
                $this.Clients[$i].Name = $NewName
            }
        }

        for ($i = 0; $i -lt $this.Connections.count; $i++) {
            if ($this.Connections[$i].DefaultClient -eq $Name) {
                $this.Connections[$i].DefaultClient = $NewName
            }
        }
    }

    [void] RenameConnection([string] $Name, [string] $NewName) {
        if ($Name -eq $NewName) {
            throw "The two names are similar."
        }
        elseif (-not $this.ConnectionExists($Name)) {
            throw "No Connection `"{0}`" to rename." -f $Name
        }
        elseif ($this.ConnectionExists($NewName)) {
            throw "Cannot rename Connection `"{0}`" to `"{1}`" as this name is already used." -f $Name, $NewName
        }

        for ($i = 0; $i -lt $this.Connections.count; $i++) {
            if ($this.Connections[$i].Name -eq $Name) {
                $this.Connections[$i].Name = $NewName
            }
        }
    }
}
class ValidateClientName : ValidateArgumentsAttribute {
    [void] Validate(
        [System.Object] $Argument,
        [System.Management.Automation.EngineIntrinsics] $EngineIntrinsics
    ) {
        $Inventory = Import-Inventory
        if (-not $Inventory.ClientExists($Argument)) {
            Throw "Client `"{0}`" does not exist." -f $Argument
        }
    }
}
class ValidateConnectionName : ValidateArgumentsAttribute {
    [void] Validate(
        [System.Object] $Argument,
        [System.Management.Automation.EngineIntrinsics] $EngineIntrinsics
    ) {
        $Inventory = Import-Inventory
        if (-not $Inventory.ConnectionExists($Argument)) {
            Throw "Connection `"{0}`" does not exist." -f $Argument
        }
    }
}
class ValidateSetClientName : IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        try {
            $Inventory = Import-Inventory
            return $Inventory.Clients | ForEach-Object -Process { $_.Name }
        }
        catch {
            Write-Warning -Message $_.Exception.Message
        }
        return $null
    }
}
class ValidateSetConnectionName : IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        try {
            $Inventory = Import-Inventory
            return $Inventory.Connections | ForEach-Object -Process { $_.Name }
        }
        catch {
            Write-Warning -Message $_.Exception.Message
        }
        return $null
    }
}
#endregion Classes

#region Private functions
function Import-Inventory {

    <#
 
    .SYNOPSIS
    Import inventory.
 
    .DESCRIPTION
    Creates inventory object, reads inventory file and returns the object.
 
    .INPUTS
    None. You cannot pipe objects to Import-Inventory.
 
    .OUTPUTS
    Inventory. Import-Inventory returns the inventory object.
 
    .EXAMPLE
    PS> Import-Inventory
    (Inventory)
 
    #>


    [OutputType([Inventory])]
    param ()

    process {
        $Inventory = New-Object -TypeName Inventory
        $Inventory.ReadFile()

        $Inventory
    }
}
function New-DefaultClients {

    <#
 
    .SYNOPSIS
    Creates default clients.
 
    .DESCRIPTION
    Creates and returns Client objects with popular programs.
 
    .INPUTS
    None. You cannot pipe objects to New-DefaultClients.
 
    .OUTPUTS
    Client[]. New-DefaultClients returns an array of Client objects.
 
    .EXAMPLE
    PS> New-DefaultClients
    (Client[])
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope = "Function", Target = "*")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Scope = "Function", Target = "*")]
    [OutputType([Client[]])]
    param ()

    process {
        $DefaultClient = @()

        # OpenSSH (Microsoft Windows feature)
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "OpenSSH",
            "C:\Windows\System32\OpenSSH\ssh.exe",
            "-l <user> -p <port> <host>",
            22,
            [Scopes]::Console,
            "OpenSSH (Microsoft Windows feature)"
        )

        # PuTTY using SSH protocol
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "PuTTY_SSH",
            "putty.exe",
            "-ssh -P <port> <user>@<host>",
            22,
            [Scopes]::External,
            "PuTTY using SSH protocol"
        )

        # Microsoft Remote Desktop
        $DefaultClient += New-Object -TypeName Client -ArgumentList @(
            "RD",
            "C:\Windows\System32\mstsc.exe",
            "/v:<host>:<port> /fullscreen",
            3389,
            [Scopes]::External,
            "Microsoft Remote Desktop"
        )

        $DefaultClient
    }
}
#endregion Private functions

#region Public functions
function Add-Portal {

    <#
 
    .SYNOPSIS
    Adds Portal connection.
 
    .DESCRIPTION
    Adds connection entry to the Portal inventory file.
 
    .PARAMETER Name
    Name of the connection.
 
    .PARAMETER Hostname
    Name of the remote host.
 
    .PARAMETER Port
    Port to connect to on the remote host.
    If not set, it will use the default port of the client.
 
    .PARAMETER DefaultClient
    Name of the default client.
 
    .PARAMETER DefaultUser
    Default client to use to connect to the remote host.
 
    .PARAMETER Description
    Short description for the connection.
 
    .INPUTS
    None. You cannot pipe objects to Add-Portal.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Add-Portal -Name myconn -Hostname myhost -DefaultClient SSH
 
    .EXAMPLE
    PS> Add-Portal -Name myrdpconn -Hostname myhost -DefaultClient RDP -Description "My RDP connection"
 
    .EXAMPLE
    PS> Add-Portal -Name mysshconn -Hostname myhost -Port 2222 -DefaultClient SSH -DefaultUser myuser -Description "My SSH connection"
 
    #>


    [OutputType([string])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the remote host."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Hostname,

        [Parameter(
            HelpMessage = "Port to connect to on the remote host."
        )]
        [UInt16] $Port,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Default client to use to connect to the remote host."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [string] $DefaultClient,

        [Parameter(
            HelpMessage = "Default user to use to connect to the remote host."
        )]
        [string] $DefaultUser,

        [Parameter(
            HelpMessage = "Short description of the connection."
        )]
        [string] $Description
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }

        try {
            $Connection = New-Object -TypeName Connection -ArgumentList @(
                $Name,
                $Hostname,
                $Port,
                $DefaultClient,
                $DefaultUser,
                $Description
            )#
        }
        catch {
            Write-Error -Message (
                "Cannot create new connection: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Add Connection {0}" -f $Connection.ToString()
            )
        ) {
            $Inventory.AddConnection($Connection)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Connection `"{0}`" has been added to the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Add-PortalClient {

    <#
 
    .SYNOPSIS
    Adds Portal client.
 
    .DESCRIPTION
    Adds client entry to the Portal inventory file.
 
    .PARAMETER Name
    Name of the client.
 
    .PARAMETER Executable
    Path to the executable program that the client uses.
 
    .PARAMETER Arguments
    String of Arguments to pass to the executable.
    The string should contain the required tokens.
    Please read the documentation of Portal.
 
    .PARAMETER DefaultPort
    Network port to use if the connection has no defined port.
 
    .PARAMETER DefaultScope
    Default scope in which a connection will be opened.
 
    .PARAMETER Description
    Short description for the client.
 
    .INPUTS
    None. You cannot pipe objects to Add-PortalClient.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Add-PortalClient -Name SSH -Executable "ssh.exe" -Arguments "-l <user> -p <port> <host>" -DefaultPort 22
 
    .EXAMPLE
    PS> Add-PortalClient -Name MyCustomClient -Executable "client.exe" -Arguments "--hostname <host> --port <port>" -DefaultPort 666 -DefaultScope External -Description "My custom client"
 
    #>


    [OutputType([string])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Name of the client."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Path to the executable to run as client."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Executable,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Arguments as a tokenized string. Please, read the documentation to get the list of tokens."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Arguments,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Default port to connect to on the remote host."
        )]
        [ValidateNotNullOrEmpty()]
        [UInt16] $DefaultPort,

        [Parameter(
            HelpMessage = "Default scope in which a connection will be opened."
        )]
        [ValidateNotNullOrEmpty()]
        [Scopes] $DefaultScope = [Scopes]::Console,

        [Parameter(
            HelpMessage = "Short description of the client."
        )]
        [string] $Description
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error inventory: {0}" -f $_.Exception.Message
            )
        }

        try {
            [Client]::ValidateTokenizedArgs($Arguments)
        }
        catch {
            Write-Error -Message (
                $_.Exception.Message
            )
        }

        try {
            $Client = New-Object -TypeName Client -ArgumentList @(
                $Name,
                $Executable,
                $Arguments,
                $DefaultPort,
                $DefaultScope,
                $Description
            )
        }
        catch {
            Write-Error -Message (
                "Cannot create new client: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Add Client {0}" -f $Client.ToString()
            )
        ) {
            $Inventory.AddClient($Client)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Client `"{0}`" has been added to the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Get-Portal {

    <#
 
    .SYNOPSIS
    Gets Portal connections.
 
    .DESCRIPTION
    Gets available connections from the Portal inventory file.
    connections can be filtered by their name and/or client name.
 
    .PARAMETER Name
    Filters connections by name.
 
    .INPUTS
    None. You cannot pipe objects to Get-Portal.
 
    .OUTPUTS
    PSCustomObject. Get-Portal returns objects with details of the available connections.
 
    .EXAMPLE
    PS> Get-Portal
    (objects)
 
    .EXAMPLE
    PS> Get-Portal -Name "myproject_*" -Hostname "*.mydomain" -Client "*_myproject"
    (filtered objects)
 
    #>


    [OutputType([PSCustomObject[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = "Filter by connection name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name = "*",

        [Parameter(
            HelpMessage = "Filter by hostname."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Hostname = "*",

        [Parameter(
            HelpMessage = "Filter by client name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Client = "*"
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }

        $Connections = @()
        foreach ($c in $Inventory.Connections) {
            $Connections += [PSCustomObject] @{
                Name          = $c.Name
                Hostname      = $c.Hostname
                Port          = if ($c.IsDefaultPort) {
                    $Inventory.GetClient($c.DefaultClient).DefaultPort
                }
                else {
                    $c.Port
                }
                DefaultClient = $c.DefaultClient
                DefaultUser   = $c.DefaultUser
                Description   = $c.Description
            }
        }

        $Connections
        | Where-Object -Property Name -Like $Name
        | Where-Object -Property Hostname -Like $Hostname
        | Where-Object -Property DefaultClient -Like $Client
        | Sort-Object -Property Name
    }
}
function Get-PortalClient {

    <#
 
    .SYNOPSIS
    Gets Portal clients.
 
    .DESCRIPTION
    Gets available clients from the Portal inventory file.
    Clients can be filtered by their name.
 
    .PARAMETER Name
    Filters clients by name.
 
    .INPUTS
    None. You cannot pipe objects to Get-PortalClient.
 
    .OUTPUTS
    PSCustomObject. Get-PortalClient returns objects with details of the available clients.
 
    .EXAMPLE
    PS> Get-PortalClient
    (objects)
 
    .EXAMPLE
    PS> Get-PortalClient -Name "custom_*"
    (filtered objects)
 
    #>


    [OutputType([PSCustomObject[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            HelpMessage = "Filter by client name."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Name = "*"
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }

        $Clients = @()
        foreach ($c in $Inventory.Clients) {
            $Clients += [PSCustomObject] @{
                Name         = $c.Name
                Command      = "{0} {1}" -f $c.Executable, $c.TokenizedArgs
                DefaultPort  = $c.DefaultPort
                DefaultScope = $c.DefaultScope
                Description  = $c.Description
            }
        }

        $Clients
        | Where-Object -Property Name -Like $Name
        | Sort-Object -Property Name
    }
}
function Get-PortalInventory {

    <#
 
    .SYNOPSIS
    Gets Portal inventory information.
 
    .DESCRIPTION
    Gets detailed information about the Portal inventory.
 
    .INPUTS
    None. You cannot pipe objects to Get-PortalInventory.
 
    .OUTPUTS
    PSCustomObject. Get-PortalInventory returns an object with detailed information.
 
    .EXAMPLE
    PS> Get-PortalInventory
    (objects)
 
    #>


    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param ()

    process {
        $Inventory = New-Object -TypeName Inventory
        $FileExists = $false

        if (Test-Path -Path $Inventory.Path -PathType Leaf) {
            $Inventory.ReadFile()
            $FileExists = $true
        }

        [PSCustomObject] @{
            Path                = $Inventory.Path
            EnvVariable         = [Inventory]::EnvVariable
            FileExists          = $FileExists
            NumberOfClients     = $Inventory.Clients.Count
            NumberOfConnections = $Inventory.Connections.Count
        }
    }
}
function New-PortalInventory {

    <#
 
    .SYNOPSIS
    Creates Portal inventory file.
 
    .DESCRIPTION
    Creates a new inventory file where Portal saves items.
 
    .PARAMETER NoDefaultClients
    Does not add defaults clients to the new inventory.
 
    .PARAMETER Force
    Overwrites existing inventory file.
 
    .PARAMETER PassThru
    Indicates that the cmdlet sends items from the interactive window down the pipeline as input to other commands.
 
    .INPUTS
    None. You cannot pipe objects to New-PortalInventory.
 
    .OUTPUTS
    System.Void. None.
        or if PassThru is set,
    System.String. New-PortalInventory returns a string with the path to the created inventory.
 
    .EXAMPLE
    PS> New-PortalInventory
 
    .EXAMPLE
    PS> New-PortalInventory -NoDefaultClients
 
    .EXAMPLE
    PS> New-PortalInventory -Force
 
    .EXAMPLE
    PS> New-PortalInventory -PassThru
    C:\Users\MyUsername\Portal.json
 
    .EXAMPLE
    PS> New-PortalInventory -NoDefaultClients -Force -PassThru
    C:\Users\MyUsername\Portal.json
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            HelpMessage = "Do not add defaults clients."
        )]
        [switch] $NoDefaultClients,

        [Parameter(
            HelpMessage = "Overwrite existing inventory file."
        )]
        [switch] $Force,

        [Parameter(
            HelpMessage = "Indicates that the cmdlet sends items from the interactive window down the pipeline as input to other commands."
        )]
        [switch] $PassThru
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        $Inventory = New-Object -TypeName Inventory

        if ((Test-Path -Path $Inventory.Path -PathType Leaf) -and -not ($Force.IsPresent)) {
            Write-Error -ErrorAction Stop -Exception (
                [System.IO.IOException] "Inventory file already exists. Use `"-Force`" to overwrite it."
            )
        }

        if ($PSCmdlet.ShouldProcess($Inventory.Path, "Create inventory file")) {
            if (-not $NoDefaultClients.IsPresent) {
                New-DefaultClients | ForEach-Object -Process {
                    $Inventory.AddClient($_)
                }
            }

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Inventory file has been created: {0}" -f $Inventory.Path
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }

        if ($PassThru.IsPresent) {
            Resolve-Path $Inventory.Path | Select-Object -ExpandProperty Path
        }
    }
}
function Open-Portal {

    <#
 
    .SYNOPSIS
    Opens Portal connection.
 
    .DESCRIPTION
    Opens Portal connection which is defined in the inventory.
 
    .PARAMETER Name
    Name of the connection.
 
    .PARAMETER Client
    Name of the client to use to initiate the connection.
 
    .PARAMETER User
    Name of the user to connect with.
 
    .PARAMETER Scope
    Scope in which the connection will be opened.
 
    .INPUTS
    None. You cannot pipe objects to Open-Portal.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Open-Portal myconn
 
    .EXAMPLE
    PS> Open-Portal -Name myconn -Client SSH -User root -Scope Console
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name,

        [Parameter(
            HelpMessage = "Name of the client to use to initiate the connection."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [Alias("c")]
        [string] $Client,

        [Parameter(
            HelpMessage = "Name of the user to connect with."
        )]
        [Alias("u")]
        [string] $User,

        [Parameter(
            HelpMessage = "Scope in which the connection will be opened."
        )]
        [Alias("x")]
        [Scopes] $Scope
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Error import inventory: {0}" -f $_.Exception.Message
            )
        }

        $Invocation = @{}

        $Invocation.Connection = $Inventory.GetConnection($Name)
        Write-Debug -Message ("Open connection {0}" -f $Invocation.Connection.ToString())

        $Invocation.Client = if ($Client) {
            if (-not $Inventory.ClientExists($Client)) {
                Write-Error -Message (
                    "Cannot open connection with the specified client `"{0}`" because it does not exist." -f (
                        $Client
                    )
                )
            }
            $Inventory.GetClient($Client)
        }
        else {
            if (-not $Inventory.ClientExists($Invocation.Connection.DefaultClient)) {
                Write-Error -Message (
                    "Cannot open connection with the default client `"{0}`" because it does not exist." -f (
                        $Invocation.Connection.DefaultClient
                    )
                )
            }
            $Inventory.GetClient($Invocation.Connection.DefaultClient)
        }
        Write-Debug -Message ("Open connection with client {0}" -f $Invocation.Client.ToString())

        $Invocation.Port = if ($Invocation.Connection.IsDefaultPort()) {
            $Invocation.Client.DefaultPort
        }
        else {
            $Invocation.Connection.Port
        }
        Write-Debug -Message ("Open connection on port {0}" -f $Invocation.Port)

        $Invocation.Executable = $Invocation.Client.Executable
        $Invocation.Arguments = if ($Invocation.Client.RequiresUser) {
            if ($User) {
                $Invocation.Client.GenerateArgs(
                    $Invocation.Connection.Hostname,
                    $Invocation.Port,
                    $User
                )
            }
            elseif ($Invocation.Connection.DefaultUser) {
                $Invocation.Client.GenerateArgs(
                    $Invocation.Connection.Hostname,
                    $Invocation.Port,
                    $Invocation.Connection.DefaultUser
                )
            }
            else {
                Write-Error -Message "Cannot open connection: A user must be specified."
            }
        }
        else {
            $Invocation.Client.GenerateArgs(
                $Invocation.Connection.Hostname,
                $Invocation.Port
            )
        }
        $Invocation.Command = "{0} {1}" -f $Invocation.Executable, $Invocation.Arguments
        Write-Debug -Message ("Open connection with command `"{0}`"" -f $Invocation.Command)

        $Invocation.Scope = if ($Scope) {
            $Scope
        }
        else {
            $Invocation.Client.DefaultScope
        }
        Write-Debug -Message ("Open connection in scope `"{0}`"" -f $Invocation.Scope)

        if ($PSCmdlet.ShouldProcess($Invocation.Connection.ToString(), "Initiate connection")) {
            switch ($Invocation.Scope) {
                ([Scopes]::Console) {
                    Invoke-Expression -Command $Invocation.Command
                }
                ([Scopes]::External) {
                    Start-Process -FilePath $Invocation.Executable -ArgumentList $Invocation.Arguments
                }
                ([Scopes]::Undefined) {
                    Write-Error -Message "Cannot open connection: Scope is undefined."
                }
                default {
                    Write-Error -Message "Cannot open connection: Scope is unknown."
                }
            }
        }
    }
}
function Remove-Portal {

    <#
 
    .SYNOPSIS
    Removes Portal connection.
 
    .DESCRIPTION
    Removes connection entry from the Portal inventory file.
 
    .PARAMETER Name
    Name of the connection.
 
    .INPUTS
    None. You cannot pipe objects to Remove-Portal.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Remove-Portal myconn
 
    .EXAMPLE
    PS> Remove-Portal -Name myconn
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Remove Connection {0}" -f $Name
            )
        ) {
            $Inventory.RemoveConnection($Name)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Connection `"{0}`" has been removed from the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Remove-PortalClient {

    <#
 
    .SYNOPSIS
    Removes Portal client.
 
    .DESCRIPTION
    Removes client entry from the Portal inventory file.
 
    .PARAMETER Name
    Name of the client.
 
    .INPUTS
    None. You cannot pipe objects to Remove-PortalClient.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Remove-PortalClient SSH
 
    .EXAMPLE
    PS> Remove-PortalClient -Name SSH
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the client."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [string] $Name
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                "Remove Client {0}" -f $Name
            )
        ) {
            $Inventory.RemoveClient($Name)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Client `"{0}`" has been removed from the inventory." -f $Name
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Rename-Portal {

    <#
 
    .SYNOPSIS
    Renames Portal connection.
 
    .DESCRIPTION
    Renames connection entry from the Portal inventory file.
 
    .PARAMETER Name
    Name of the connection to rename.
 
    .PARAMETER NewName
    New name for the connection.
 
    .INPUTS
    None. You cannot pipe objects to Rename-Portal.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Rename-Portal -Name my_old_conn -NewName my_new_conn
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection to rename."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name,

        [Parameter(
            Position = 1,
            Mandatory = $true,
            HelpMessage = "New name for the connection."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $NewName
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                ("Rename Connection {0} to {1}" -f $Name, $NewName)
            )
        ) {
            $Inventory.RenameConnection($Name, $NewName)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Connection `"{0}`" has been renamed `"{1}`" in the inventory." -f $Name, $NewName
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Rename-PortalClient {

    <#
 
    .SYNOPSIS
    Renames Portal client.
 
    .DESCRIPTION
    Renames client entry from the Portal inventory file.
 
    .PARAMETER Name
    Name of the client to rename.
 
    .PARAMETER NewName
    New name for the client.
 
    .INPUTS
    None. You cannot pipe objects to Rename-PortalClient.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Rename-PortalClient -Name my_old_client -NewName my_new_client
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the client to rename."
        )]
        [ValidateSet([ValidateSetClientName])]
        [ValidateClientName()]
        [string] $Name,

        [Parameter(
            Position = 1,
            Mandatory = $true,
            HelpMessage = "New name for the client."
        )]
        [ValidateNotNullOrEmpty()]
        [string] $NewName
    )

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        try {
            $Inventory = Import-Inventory
        }
        catch {
            Write-Error -Message (
                "Cannot open inventory: {0}" -f $_.Exception.Message
            )
        }

        if ($PSCmdlet.ShouldProcess(
                "Inventory file {0}" -f $Inventory.Path,
                ("Rename Client {0} to {1}" -f $Name, $NewName)
            )
        ) {
            $Inventory.RenameClient($Name, $NewName)

            try {
                $Inventory.SaveFile()
                Write-Verbose -Message (
                    "Client `"{0}`" has been renamed `"{1}`" in the inventory." -f $Name, $NewName
                )
            }
            catch {
                Write-Error -Message (
                    "Cannot save inventory: {0}" -f $_.Exception.Message
                )
            }
        }
    }
}
function Set-PortalInventory {

    <#
 
    .SYNOPSIS
    Sets Portal inventory path.
 
    .DESCRIPTION
    Sets the specific environment variable to overwrite default path to the Portal inventory file.
 
    .PARAMETER Name
    Path to the inventory file.
    This path is set in a environment variable.
    Pass an empty string or null to reset to the default path.
 
    .PARAMETER Target
    Target scope where the environment variable will be saved.
 
    .INPUTS
    None. You cannot pipe objects to Set-PortalInventory.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Set-PortalInventory C:\MyCustomInventory.json
 
    .EXAMPLE
    PS> Set-PortalInventory -Path C:\MyCustomInventory.json
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Path to the inventory file."
        )]
        [AllowEmptyString()]
        [string] $Path,

        [Parameter(
            HelpMessage = "Target scope of the environment variable."
        )]
        [ValidateSet("Process", "User")]
        [string] $Target = "User"
    )

    process {
        $EnvVar = [Inventory]::EnvVariable

        if ($PSCmdlet.ShouldProcess(
                ("{0} environment variable {1}" -f $Target, $EnvVar),
                "Set value {0}" -f $Path
            )
        ) {
            [System.Environment]::SetEnvironmentVariable(
                $EnvVar,
                $Path,
                [System.EnvironmentVariableTarget]::"$Target"
            )
            Write-Verbose -Message ("{0} environment variable `"{1}`" has been set to `"{2}`"." -f $Target, $EnvVar, $Path)
        }
    }
}
function Test-Portal {

    <#
 
    .SYNOPSIS
    Tests Portal connection.
 
    .DESCRIPTION
    Tests Portal connection which is defined in the inventory.
 
    .PARAMETER Name
    Name of the connection.
 
    .INPUTS
    None. You cannot pipe objects to Test-Portal.
 
    .OUTPUTS
    System.String. Test-Portal returns a string with the status of the remote host.
 
    .EXAMPLE
    PS> Test-Portal myconn
    (status)
 
    .EXAMPLE
    PS> Test-Portal -Name myconn
    (status)
 
    #>


    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Name of the connection."
        )]
        [ValidateSet([ValidateSetConnectionName])]
        [ValidateConnectionName()]
        [string] $Name
    )

    process {
        $Inventory = Import-Inventory
        $Status = "Unknown"

        $Connection = $Inventory.GetConnection($Name)

        $Port = if ($Connection.IsDefaultPort()) {
            $Inventory.GetClient($Connection.DefaultClient).DefaultPort
        }
        else {
            $Connection.Port
        }

        $TestConnectionParams = @{
            TargetName     = $Connection.Hostname
            TcpPort        = $Port
            TimeoutSeconds = 3
            ErrorAction    = "Stop"
        }

        try {
            if (Test-Connection @TestConnectionParams) {
                Write-Information -MessageData (
                    "Connection {0} is up on port {1}." -f $Connection.ToString(), $Port
                ) -InformationAction Continue
                $Status = "Online"
            }
            else {
                Write-Error -Message (
                    "Connection: {0} is down on port {1}." -f $Connection.ToString(), $Port
                )
                $Status = "Offline"
            }
        }
        catch {
            Write-Error -Message $_.Exception.Message
            $Status = "CriticalFailure"
        }

        $Status
    }
}
#endregion Public functions