mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-03-13 19:31:44 +01:00
Merge pull request #44 from fgbm/main
Fix: connectivity check for hosts using ProxyJump or ProxyCommand
This commit is contained in:
@@ -2,12 +2,15 @@ package connectivity
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,16 +48,18 @@ type HostPingResult struct {
|
|||||||
|
|
||||||
// PingManager manages SSH connectivity checks for multiple hosts
|
// PingManager manages SSH connectivity checks for multiple hosts
|
||||||
type PingManager struct {
|
type PingManager struct {
|
||||||
results map[string]*HostPingResult
|
results map[string]*HostPingResult
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
configFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPingManager creates a new ping manager with the specified timeout
|
// NewPingManager creates a new ping manager with the specified timeout
|
||||||
func NewPingManager(timeout time.Duration) *PingManager {
|
func NewPingManager(timeout time.Duration, configFile string) *PingManager {
|
||||||
return &PingManager{
|
return &PingManager{
|
||||||
results: make(map[string]*HostPingResult),
|
results: make(map[string]*HostPingResult),
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
configFile: configFile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +103,14 @@ func (pm *PingManager) PingHost(ctx context.Context, host config.SSHHost) *HostP
|
|||||||
// Mark as connecting
|
// Mark as connecting
|
||||||
pm.updateStatus(host.Name, StatusConnecting, nil, 0)
|
pm.updateStatus(host.Name, StatusConnecting, nil, 0)
|
||||||
|
|
||||||
|
// If the host uses a ProxyJump or ProxyCommand, we need to use the external SSH command
|
||||||
|
// because implementing jump host support with pure Go ssh library requires
|
||||||
|
// handling authentication for the jump host, which is complex and requires
|
||||||
|
// access to the user's SSH agent or keys.
|
||||||
|
if host.ProxyJump != "" || host.ProxyCommand != "" {
|
||||||
|
return pm.pingWithExternalCommand(ctx, host, start)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the actual hostname and port
|
// Determine the actual hostname and port
|
||||||
hostname := host.Hostname
|
hostname := host.Hostname
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
@@ -159,6 +172,53 @@ func (pm *PingManager) PingHost(ctx context.Context, host config.SSHHost) *HostP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pingWithExternalCommand pings a host using the external SSH command
|
||||||
|
func (pm *PingManager) pingWithExternalCommand(ctx context.Context, host config.SSHHost, start time.Time) *HostPingResult {
|
||||||
|
// Construct the SSH command
|
||||||
|
// ssh -q -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 host exit
|
||||||
|
args := []string{"-q", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no"}
|
||||||
|
|
||||||
|
// Set timeout matching the manager's timeout
|
||||||
|
// Convert duration to seconds (rounding up to ensure we don't timeout too early in the command)
|
||||||
|
timeoutSec := int(pm.timeout.Seconds())
|
||||||
|
if timeoutSec < 1 {
|
||||||
|
timeoutSec = 1
|
||||||
|
}
|
||||||
|
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", timeoutSec))
|
||||||
|
|
||||||
|
// If we have a specific config file, use it
|
||||||
|
if pm.configFile != "" {
|
||||||
|
args = append(args, "-F", pm.configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the host name and the command to run (exit)
|
||||||
|
args = append(args, host.Name, "exit")
|
||||||
|
|
||||||
|
// Create command with context for timeout cancellation
|
||||||
|
// Note: We used pm.timeout for the ssh command option, but we also respect the context deadline
|
||||||
|
cmd := exec.CommandContext(ctx, "ssh", args...)
|
||||||
|
|
||||||
|
// Run the command
|
||||||
|
err := cmd.Run()
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
var status PingStatus
|
||||||
|
if err != nil {
|
||||||
|
// SSH returns non-zero exit code on connection failure
|
||||||
|
status = StatusOffline
|
||||||
|
} else {
|
||||||
|
status = StatusOnline
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.updateStatus(host.Name, status, err, duration)
|
||||||
|
return &HostPingResult{
|
||||||
|
HostName: host.Name,
|
||||||
|
Status: status,
|
||||||
|
Error: err,
|
||||||
|
Duration: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PingAllHosts pings all hosts concurrently and returns a channel of results
|
// PingAllHosts pings all hosts concurrently and returns a channel of results
|
||||||
func (pm *PingManager) PingAllHosts(ctx context.Context, hosts []config.SSHHost) <-chan *HostPingResult {
|
func (pm *PingManager) PingAllHosts(ctx context.Context, hosts []config.SSHHost) <-chan *HostPingResult {
|
||||||
resultChan := make(chan *HostPingResult, len(hosts))
|
resultChan := make(chan *HostPingResult, len(hosts))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewPingManager(t *testing.T) {
|
func TestNewPingManager(t *testing.T) {
|
||||||
pm := NewPingManager(5 * time.Second)
|
pm := NewPingManager(5*time.Second, "")
|
||||||
if pm == nil {
|
if pm == nil {
|
||||||
t.Error("NewPingManager() returned nil")
|
t.Error("NewPingManager() returned nil")
|
||||||
}
|
}
|
||||||
@@ -19,16 +19,16 @@ func TestNewPingManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPingManager_PingHost(t *testing.T) {
|
func TestPingManager_PingHost(t *testing.T) {
|
||||||
pm := NewPingManager(1 * time.Second)
|
pm := NewPingManager(1*time.Second, "")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Test ping method exists and doesn't panic
|
// Test ping method exists and doesn't panic
|
||||||
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
||||||
result := pm.PingHost(ctx, host)
|
result := pm.PingHost(ctx, host)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
t.Error("Expected ping result to be returned")
|
t.Error("Expected ping result to be returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with invalid host
|
// Test with invalid host
|
||||||
invalidHost := config.SSHHost{Name: "invalid", Hostname: "invalid.host.12345", Port: "22"}
|
invalidHost := config.SSHHost{Name: "invalid", Hostname: "invalid.host.12345", Port: "22"}
|
||||||
result = pm.PingHost(ctx, invalidHost)
|
result = pm.PingHost(ctx, invalidHost)
|
||||||
@@ -38,14 +38,14 @@ func TestPingManager_PingHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPingManager_GetStatus(t *testing.T) {
|
func TestPingManager_GetStatus(t *testing.T) {
|
||||||
pm := NewPingManager(1 * time.Second)
|
pm := NewPingManager(1*time.Second, "")
|
||||||
|
|
||||||
// Test unknown host
|
// Test unknown host
|
||||||
status := pm.GetStatus("unknown.host")
|
status := pm.GetStatus("unknown.host")
|
||||||
if status != StatusUnknown {
|
if status != StatusUnknown {
|
||||||
t.Errorf("Expected StatusUnknown for unknown host, got %v", status)
|
t.Errorf("Expected StatusUnknown for unknown host, got %v", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test after ping
|
// Test after ping
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
||||||
@@ -57,21 +57,21 @@ func TestPingManager_GetStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPingManager_PingMultipleHosts(t *testing.T) {
|
func TestPingManager_PingMultipleHosts(t *testing.T) {
|
||||||
pm := NewPingManager(1 * time.Second)
|
pm := NewPingManager(1*time.Second, "")
|
||||||
hosts := []config.SSHHost{
|
hosts := []config.SSHHost{
|
||||||
{Name: "localhost", Hostname: "127.0.0.1", Port: "22"},
|
{Name: "localhost", Hostname: "127.0.0.1", Port: "22"},
|
||||||
{Name: "invalid", Hostname: "invalid.host.12345", Port: "22"},
|
{Name: "invalid", Hostname: "invalid.host.12345", Port: "22"},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Ping each host individually
|
// Ping each host individually
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
result := pm.PingHost(ctx, host)
|
result := pm.PingHost(ctx, host)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
t.Errorf("Expected ping result for host %s", host.Name)
|
t.Errorf("Expected ping result for host %s", host.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that status was set
|
// Check that status was set
|
||||||
status := pm.GetStatus(host.Name)
|
status := pm.GetStatus(host.Name)
|
||||||
if status == StatusUnknown {
|
if status == StatusUnknown {
|
||||||
@@ -81,19 +81,19 @@ func TestPingManager_PingMultipleHosts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPingManager_GetResult(t *testing.T) {
|
func TestPingManager_GetResult(t *testing.T) {
|
||||||
pm := NewPingManager(1 * time.Second)
|
pm := NewPingManager(1*time.Second, "")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Test getting result for unknown host
|
// Test getting result for unknown host
|
||||||
result, exists := pm.GetResult("unknown")
|
result, exists := pm.GetResult("unknown")
|
||||||
if exists || result != nil {
|
if exists || result != nil {
|
||||||
t.Error("Expected no result for unknown host")
|
t.Error("Expected no result for unknown host")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test after ping
|
// Test after ping
|
||||||
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
||||||
pm.PingHost(ctx, host)
|
pm.PingHost(ctx, host)
|
||||||
|
|
||||||
result, exists = pm.GetResult("test")
|
result, exists = pm.GetResult("test")
|
||||||
if !exists || result == nil {
|
if !exists || result == nil {
|
||||||
t.Error("Expected result to exist after ping")
|
t.Error("Expected result to exist after ping")
|
||||||
@@ -114,7 +114,7 @@ func TestPingStatus_String(t *testing.T) {
|
|||||||
{StatusOffline, "offline"},
|
{StatusOffline, "offline"},
|
||||||
{PingStatus(999), "unknown"}, // Invalid status
|
{PingStatus(999), "unknown"}, // Invalid status
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.expected, func(t *testing.T) {
|
t.Run(tt.expected, func(t *testing.T) {
|
||||||
if got := tt.status.String(); got != tt.expected {
|
if got := tt.status.String(); got != tt.expected {
|
||||||
@@ -126,19 +126,19 @@ func TestPingStatus_String(t *testing.T) {
|
|||||||
|
|
||||||
func TestPingHost_Basic(t *testing.T) {
|
func TestPingHost_Basic(t *testing.T) {
|
||||||
// Test that the ping functionality exists
|
// Test that the ping functionality exists
|
||||||
pm := NewPingManager(1 * time.Second)
|
pm := NewPingManager(1*time.Second, "")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"}
|
||||||
|
|
||||||
// Just ensure the function doesn't panic
|
// Just ensure the function doesn't panic
|
||||||
result := pm.PingHost(ctx, host)
|
result := pm.PingHost(ctx, host)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
t.Error("Expected ping result to be returned")
|
t.Error("Expected ping result to be returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that status is set
|
// Test that status is set
|
||||||
status := pm.GetStatus("test")
|
status := pm.GetStatus("test")
|
||||||
if status == StatusUnknown {
|
if status == StatusUnknown {
|
||||||
t.Error("Expected status to be set after ping attempt")
|
t.Error("Expected status to be set after ping attempt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func NewModel(hosts []config.SSHHost, configFile string, searchMode bool, curren
|
|||||||
styles := NewStyles(80) // Default width
|
styles := NewStyles(80) // Default width
|
||||||
|
|
||||||
// Initialize ping manager with 5 second timeout
|
// Initialize ping manager with 5 second timeout
|
||||||
pingManager := connectivity.NewPingManager(5 * time.Second)
|
pingManager := connectivity.NewPingManager(5*time.Second, configFile)
|
||||||
|
|
||||||
// Create the model with default sorting by name
|
// Create the model with default sorting by name
|
||||||
m := Model{
|
m := Model{
|
||||||
|
|||||||
Reference in New Issue
Block a user