Public/Connect-SSHTunnel.ps1

function Connect-SSHTunnel {
    <#
    .SYNOPSIS
        Create an SSH tunnel for database or service access.
 
    .PARAMETER Target
        Server alias (from config.json) or hostname.
 
    .PARAMETER RemotePort
        Remote port number or database type name (mysql, postgres, etc.).
 
    .PARAMETER LocalPort
        Local port to bind (defaults to same as remote).
 
    .PARAMETER RemoteHost
        Remote host for the tunnel (default: localhost).
 
    .EXAMPLE
        Connect-SSHTunnel myserver postgres
        tunnel myserver mysql 3307
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Target,

        [Parameter(Position = 1)]
        [string]$RemotePort = "3306",

        [Parameter(Position = 2)]
        [int]$LocalPort = 0,

        [Parameter()]
        [string]$RemoteHost = "localhost"
    )

    $config = Get-ScriptConfig

    if (-not $config) {
        Write-Host "Please configure config.json before using SSH commands." -ForegroundColor Red
        return
    }

    $servers = @{}
    $serverKeyFiles = @{}
    $serverUsers = @{}
    if ($config.ssh.servers) {
        foreach ($key in $config.ssh.servers.PSObject.Properties.Name) {
            $servers[$key] = $config.ssh.servers.$key.hostname
            if ($config.ssh.servers.$key.keyFile) {
                $serverKeyFiles[$key] = $config.ssh.servers.$key.keyFile
            }
            if ($config.ssh.servers.$key.user) {
                $serverUsers[$key] = $config.ssh.servers.$key.user
            }
        }
    }

    $dbPorts = @{}
    if ($config.ssh.databasePorts) {
        foreach ($key in $config.ssh.databasePorts.PSObject.Properties.Name) {
            $dbPorts[$key] = $config.ssh.databasePorts.$key
        }
    }

    if ($dbPorts.ContainsKey($RemotePort.ToLower())) {
        $RemotePort = $dbPorts[$RemotePort.ToLower()]
        if ($LocalPort -eq 0) {
            $LocalPort = $RemotePort
        }
    }
    else {
        try {
            $RemotePort = [int]$RemotePort
        }
        catch {
            Write-Host "Invalid port: $RemotePort" -ForegroundColor Red
            Write-Host "Use a port number or one of: $($dbPorts.Keys -join ', ')" -ForegroundColor Yellow
            return
        }
    }

    if ($LocalPort -eq 0) {
        $LocalPort = $RemotePort
    }

    $keyFile = $null
    $configUser = $null
    if ($servers.ContainsKey($Target)) {
        $Server = $servers[$Target]
        if ($serverKeyFiles.ContainsKey($Target)) {
            $keyFile = $serverKeyFiles[$Target]
        }
        if ($serverUsers.ContainsKey($Target)) {
            $configUser = $serverUsers[$Target]
        }
    }
    else {
        $Server = $Target
    }

    $credsDir = Join-Path $script:ToolkitRoot "creds"

    $keyFilePath = $null
    if ($keyFile) {
        if ([System.IO.Path]::IsPathRooted($keyFile)) {
            $keyFilePath = $keyFile
        }
        else {
            $keyFilePath = Join-Path $credsDir $keyFile
        }
        if (-not (Test-Path $keyFilePath)) {
            Write-Host "Key file not found: $keyFilePath" -ForegroundColor Red
            return
        }
    }

    $username = $null
    $password = $null
    $cred = $null

    if (-not $keyFile) {
        $credFile = $config.ssh.credentialFile
        if (-not $credFile) {
            $credFile = "ssh-credentials.xml"
        }
        $credPath = Join-Path $credsDir $credFile

        if (-not (Test-Path $credPath)) {
            Write-Host "Credential file not found: $credPath" -ForegroundColor Red
            return
        }

        $cred = Import-Clixml $credPath
        $username = $cred.UserName
        $password = $cred.GetNetworkCredential().Password
    }
    else {
        if ($configUser) {
            $username = $configUser
        }
        else {
            $credFile = $config.ssh.credentialFile
            if ($credFile) {
                $credPath = Join-Path $credsDir $credFile
                if (Test-Path $credPath) {
                    $cred = Import-Clixml $credPath
                    $username = $cred.UserName
                }
            }
        }

        if (-not $username) {
            Write-Host "Username required. Add 'user' to server config in config.json" -ForegroundColor Red
            return
        }
    }

    Write-Host "Tunnel: localhost:$LocalPort -> ${RemoteHost}:${RemotePort} (via $Server)" -ForegroundColor Cyan

    $useWSL = $false
    $wsl = Get-Command wsl -ErrorAction SilentlyContinue
    if ($wsl) {
        $wslCheck = wsl echo "ok" 2>&1
        if ($wslCheck -eq "ok") {
            $useWSL = $true
        }
    }

    if ($keyFile) {
        $winSsh = Get-Command ssh.exe -ErrorAction SilentlyContinue
        if ($winSsh) {
            & ssh.exe -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i $keyFilePath -N -L "${LocalPort}:${RemoteHost}:${RemotePort}" "$username@$Server"
        }
        elseif ($useWSL) {
            $escapedPath = $keyFilePath -replace '\\', '/'
            $wslKeyPath = (wsl wslpath -u "'$escapedPath'").Trim()
            wsl bash -c "ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i '$wslKeyPath' -N -L ${LocalPort}:${RemoteHost}:${RemotePort} $username@$Server"
        }
        else {
            try {
                Import-Module Posh-SSH -ErrorAction Stop
                $session = New-SSHSession -ComputerName $Server -KeyFile $keyFilePath -AcceptKey
                New-SSHLocalPortForward -SSHSession $session -BoundHost "127.0.0.1" -BoundPort $LocalPort -RemoteAddress $RemoteHost -RemotePort $RemotePort | Out-Null
                Start-Sleep -Seconds 999999
            }
            catch {
                Write-Host "Tunnel setup failed: $($_.Exception.Message)" -ForegroundColor Red
                return
            }
            finally {
                if ($session) {
                    Remove-SSHSession -SessionId $session.SessionId | Out-Null
                }
            }
        }
    }
    elseif ($useWSL) {
        $hasSshpass = wsl bash -c "command -v sshpass >/dev/null 2>&1 && echo 'yes' || echo 'no'"
        if ($hasSshpass -match 'no') {
            Write-Host "Installing sshpass in WSL..." -ForegroundColor Yellow
            wsl bash -c "sudo apt-get update && sudo apt-get install -y sshpass"
        }
        wsl bash -c "SSHPASS='$password' sshpass -e ssh -o StrictHostKeyChecking=no -N -L ${LocalPort}:${RemoteHost}:${RemotePort} $username@$Server"
    }
    else {
        $nativeSsh = Get-Command ssh.exe -ErrorAction SilentlyContinue
        if ($nativeSsh) {
            $sshArgs = @(
                "-o", "StrictHostKeyChecking=no",
                "-o", "IdentitiesOnly=yes",
                "-N",
                "-L", "${LocalPort}:${RemoteHost}:${RemotePort}",
                "$username@$Server"
            )
            & ssh.exe @sshArgs
        }
        else {
            try {
                Import-Module Posh-SSH -ErrorAction Stop
                $session = New-SSHSession -ComputerName $Server -Credential $cred -AcceptKey
                New-SSHLocalPortForward -SSHSession $session -BoundHost "127.0.0.1" -BoundPort $LocalPort -RemoteAddress $RemoteHost -RemotePort $RemotePort | Out-Null
                Start-Sleep -Seconds 999999
            }
            catch {
                Write-Host "Tunnel setup failed: $($_.Exception.Message)" -ForegroundColor Red
                return
            }
            finally {
                if ($session) {
                    Remove-SSHSession -SessionId $session.SessionId | Out-Null
                }
            }
        }
    }
}