Public/WaykClientSession.ps1


function Get-WaykPSRemotingPath
{
    [CmdletBinding()]
    param(
    )

    if ($IsWindows) {
        $WaykClient = Get-WaykClientCommand
        $WaykBinPath = $(Get-Item $WaykClient).Directory.FullName
        $PSRemotingPath = Join-Path $WaykBinPath 'psremoting'
    } elseif ($IsMacOS) {
        $WaykBinPath = "/Applications/WaykClient.app/Contents/MacOS"
        $PSRemotingPath = Join-Path $WaykBinPath 'psremoting'
    } elseif ($IsLinux) {
        $WaykLibPath = "/usr/lib"
        $PSRemotingPath = Join-Path $WaykLibPath 'psremoting'
    }

    $PSRemotingPath
}

function Enter-WaykPSEnvironment
{
    [CmdletBinding()]
    param(
    )

    if ($PSEdition -eq 'Desktop') {
        throw "Wayk PowerShell remoting requires PowerShell 7 or later"
    }

    $PSRemotingPath = Get-WaykPSRemotingPath

    if (-Not (Test-Path $PSRemotingPath -PathType 'Container')) {
        throw "Could not find required Wayk PSRemoting directory: `"$PSRemotingPath`""
    }

    $EnvPaths = $Env:PATH -Split $([IO.Path]::PathSeparator) | Where-Object { $_ -ne $PSRemotingPath }
    $Env:PATH = $(@($PSRemotingPath) + $EnvPaths) -Join $([IO.Path]::PathSeparator)
}

function Exit-WaykPSEnvironment
{
    [CmdletBinding()]
    param(
    )

    Remove-Item Env:JETSOCAT_ARGS -ErrorAction 'SilentlyContinue'

    $PSRemotingPath = Get-WaykPSRemotingPath

    if (Test-Path $PSRemotingPath -PathType 'Container') {
        $EnvPaths = $Env:PATH -Split $([IO.Path]::PathSeparator) | Where-Object { $_ -ne $PSRemotingPath }
        $Env:PATH = $EnvPaths -Join $([IO.Path]::PathSeparator)
    }
}

function Connect-WaykPSSession
{
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    Enter-WaykPSEnvironment

    $UserName = $Credential.UserName
    $Password = $Credential.GetNetworkCredential().Password

    $WaykClient = Get-WaykClientCommand

    $CommandInput = [PSCustomObject]@{
        Hostname = $HostName
        Username = $UserName
        Password = $Password
    } | ConvertTo-Json -Compress | Out-String

    if ($IsWindows) {
        $WaykClient = $WaykClient -Replace '.exe', '.com'
    }

    $CommandOutput = $CommandInput | & "$WaykClient" 'pwsh' '-' 2>$null

    $CommandOutput = $CommandOutput | ConvertFrom-Json

    if (-Not $CommandOutput.Success) {
        throw "Failed to connect to ${HostName} with user ${UserName} with error $($CommandOutput.Error)"
    }

    $JetUrl = $CommandOutput.JetUrl
    $Env:JETSOCAT_ARGS = "forward $JetUrl -"

    return $CommandOutput
}

function New-WaykPSSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    $UserName = $Credential.UserName
    $Result = Connect-WaykPSSession -HostName:$HostName -Credential:$Credential
    New-PSSession -UserName:$UserName -HostName:$HostName -SSHTransport
    Exit-WaykPSEnvironment
}

function Enter-WaykPSSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    $UserName = $Credential.UserName
    $Result = Connect-WaykPSSession -HostName:$HostName -Credential:$Credential
    Enter-PSSession -UserName:$UserName -HostName:$HostName -SSHTransport
    Exit-WaykPSEnvironment
}

function DecodeBase64UrlSafe($Base64Url)
{
    # short circuit on empty strings
    if ($Base64Url -eq [string]::Empty) {
        return [string]::Empty
    }

    # put the standard unsafe characters back
    $s = $Base64Url.Replace('-', '+').Replace('_', '/')

    # put the padding back
    switch ($s.Length % 4) {
        0 { break; }             # no padding needed
        2 { $s += '=='; break; } # two pad chars
        3 { $s += '='; break; }  # one pad char
        default { throw "Invalid Base64Url string" }
    }

    return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s))
}

function Connect-WaykRdpSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [String] $RdpOutputFile
    )

    $WaykClient = Get-WaykClientCommand

    $CommandInput = [PSCustomObject]@{
        Hostname = $HostName
    } | ConvertTo-Json -Compress | Out-String

    if ($IsWindows) {
        $WaykClient = $WaykClient -Replace '.exe', '.com'
    }

    $OldInputEncoding = [Console]::InputEncoding
    [Console]::InputEncoding = [Text.UTF8Encoding]::new($false)

    $CommandOutput = $CommandInput | & "$WaykClient" 'rdp-tcp' '-' 2>$null

    [Console]::InputEncoding = $OldInputEncoding

    $CommandOutput = $CommandOutput | ConvertFrom-Json

    if (-Not $CommandOutput.Success) {
        throw "Failed to connect to ${HostName} with error $($CommandOutput.Error)"
    }

    $RdpConfig = DecodeBase64UrlSafe($CommandOutput.RdpConfig)

    if ($RdpOutputFile) {
        $RdpConfig | Out-File -FilePath $RdpOutputFile
        return
    }

    return $RdpConfig
}

function Enter-WaykRdpSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [String] $UserName,
        [String] $Domain
    )

    if ($IsWindows -And ($UserName -Or $Domain)) {
        # There is no simple way to specify user name and domain to the mstsc,
        # they are only can be stored on the machine globally for the specified
        # hostname using cmdkey command, which may leave a trace on the machine
        throw "-UserName/-Domain arguements are not supported on the Windows platform"
    }

    $RdpConfigFile = $(New-TemporaryFile) -Replace "[.][^.]+$", ".rdp"

    Connect-WaykRdpSession -HostName:$HostName -RdpOutputFile:$RdpConfigFile

    $RdpArgs = @("${RdpConfigFile}")

    if ($IsWindows) {
        $RdpApp = "mstsc"
        Start-Process -FilePath:$RdpApp -ArgumentList:$RdpArgs
    } else {
        $RdpApp = "xfreerdp"
        $RdpArgs += "/sec:nla"
        $RdpArgs += "/cert-ignore"
        $RdpArgs += "/from-stdin"
        if ($UserName) {
            $RdpArgs += "/u:${UserName}"
        }
        if ($Domain) {
            $RdpArgs += "/d:${Domain}"
        }

        Start-Process -FilePath:$RdpApp -ArgumentList:$RdpArgs -Wait
    }
}

Function Get-RandomPort
{
    Param(
        [Int] $MaxTries = 100
    );

    $Result = -1;
    $NbTries = 0;

    do {
        $NbTries += 1;

        $Port = Get-Random -Min 10001 -Max 32767;
        if (-Not (Test-Connection localhost -TcpPort $Port)) {
            $Result = $Port;
        }
    } while (($Result -lt 0) -and ($NbTries -lt $MaxTries));

    return $Result;
}

Function Spawn-JetsocatWithLocalTcpListener
{
    Param(
        [String] $JetPipe,
        [Int] $TcpPort
    );

    # Give access to jetsocat (and fake ssh)
    Enter-WaykPSEnvironment

    # Spawn local jetsocat tunnel for ssh ↔ jetsocat ↔ wss/jet-tcp/… ↔ gateway
    Start-Process -FilePath "jetsocat" -ArgumentList "forward", "$JetPipe", "tcp-listen://localhost:$TcpPort"

    # Get back access to system ssh
    Exit-WaykPSEnvironment

    # Wait a bit for jetsocat to be ready
    Start-Sleep -Seconds 0.5
}

function Enter-WaykSshSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [String] $UserName
    )

    $WaykClient = Get-WaykClientCommand

    $CommandInput = [PSCustomObject]@{
        Hostname = $HostName
    } | ConvertTo-Json -Compress | Out-String

    if ($IsWindows) {
        $WaykClient = $WaykClient -Replace '.exe', '.com'
    }

    $CommandOutput = $CommandInput | & "$WaykClient" 'ssh' '-' 2>$null

    $CommandOutput = $CommandOutput | ConvertFrom-Json

    if (-Not $CommandOutput.Success) {
        throw "Failed to connect to ${HostName} with user ${UserName} with error $($CommandOutput.Error)"
    }

    $JetUrl = $CommandOutput.JetUrl
    $PortToUse = Get-RandomPort
    Spawn-JetsocatWithLocalTcpListener $JetUrl $PortToUse

    $Prgm = "ssh"
    $Args = "localhost", "-p", "$PortToUse", "-l", "${UserName}"
    if ($IsWindows) {
        $Args += "-o", "GlobalKnownHostsFile=NUL", "-o", "UserKnownHostsFile=NUL"
    } else {
        $Args += "-o", "GlobalKnownHostsFile=/dev/null", "-o", "UserKnownHostsFile=/dev/null"
    }

    Start-Process -FilePath:$Prgm -ArgumentList:$Args -Wait
}

function Connect-WaykSftpSession
{
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    Enter-WaykPSEnvironment

    $UserName = $Credential.UserName
    $Password = $Credential.GetNetworkCredential().Password

    $WaykClient = Get-WaykClientCommand

    $CommandInput = [PSCustomObject]@{
        Hostname = $HostName
        Username = $UserName
        Password = $Password
    } | ConvertTo-Json -Compress | Out-String

    if ($IsWindows) {
        $WaykClient = $WaykClient -Replace '.exe', '.com'
    }

    $CommandOutput = $CommandInput | & "$WaykClient" 'sftp' '-' 2>$null

    $CommandOutput = $CommandOutput | ConvertFrom-Json

    if (-Not $CommandOutput.Success) {
        throw "Failed to connect to ${HostName} with user ${UserName} with error $($CommandOutput.Error)"
    }

    $JetUrl = $CommandOutput.JetUrl
    $Env:JETSOCAT_ARGS = "forward $JetUrl -"

    return $CommandOutput
}

function New-WaykSftpSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    $UserName = $Credential.UserName
    $Result = Connect-WaykSftpSession -HostName:$HostName -Credential:$Credential
    New-SftpSession -UserName:$UserName -HostName:$HostName -SSHTransport
    Exit-WaykPSEnvironment
}

function Enter-WaykSftpSession
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $HostName,
        [Parameter(Mandatory=$true)]
        [PSCredential] $Credential
    )

    $UserName = $Credential.UserName
    $Result = Connect-WaykSftpSession -HostName:$HostName -Credential:$Credential
    $Args = "-D", "jetsocat"
    Start-Process -FilePath:"sftp" -ArgumentList:$Args -Wait
    Exit-WaykPSEnvironment
}