Major rewrite to use native ssh command instead of Go SSH library: BREAKING CHANGE: Now requires ssh command in PATH Benefits: - Full SSH feature support including ProxyCommand - Works with SSH containers and jump hosts - Supports all SSH authentication methods - Consistent behavior with terminal SSH - No more custom SSH client implementation Changes: - Port detection now uses 'ssh hostname command' - Port forwarding uses 'ssh -L localport:localhost:remoteport hostname' - Connection testing uses native ssh command - Removed golang.org/x/crypto/ssh dependency - Updated documentation to reflect SSH compatibility This fixes issues with SSH containers that require ProxyCommand and provides full compatibility with user SSH configurations. Co-authored-by: Ona <no-reply@ona.com>
131 lines
3.6 KiB
Go
131 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// PortsDetectedMsg is sent when ports are detected
|
|
type PortsDetectedMsg struct {
|
|
Ports []int
|
|
}
|
|
|
|
// ErrorMsg is sent when an error occurs
|
|
type ErrorMsg struct {
|
|
Error error
|
|
}
|
|
|
|
// DetectPorts detects open ports on the remote host
|
|
func DetectPorts(host SSHHost) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ports, err := detectRemotePorts(host)
|
|
if err != nil {
|
|
// Log the error for debugging but don't quit the app
|
|
fmt.Fprintf(os.Stderr, "Debug: Port detection failed for %s: %v\n", host.Name, err)
|
|
// Return empty ports list so user can still use manual port forwarding
|
|
return PortsDetectedMsg{Ports: []int{}}
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Debug: Detected %d ports on %s: %v\n", len(ports), host.Name, ports)
|
|
return PortsDetectedMsg{Ports: ports}
|
|
}
|
|
}
|
|
|
|
// detectRemotePorts connects to the remote host and detects open ports using ssh command
|
|
func detectRemotePorts(host SSHHost) ([]int, error) {
|
|
// Try different commands to detect listening ports
|
|
commands := []string{
|
|
"netstat -tlnp 2>/dev/null | grep LISTEN | awk '{print $4}' | cut -d: -f2 | sort -n | uniq",
|
|
"ss -tlnp 2>/dev/null | grep LISTEN | awk '{print $4}' | cut -d: -f2 | sort -n | uniq",
|
|
"lsof -i -P -n 2>/dev/null | grep LISTEN | awk '{print $9}' | cut -d: -f2 | sort -n | uniq",
|
|
}
|
|
|
|
var output []byte
|
|
var err error
|
|
|
|
for _, cmd := range commands {
|
|
fmt.Fprintf(os.Stderr, "Debug: Running command on %s: %s\n", host.Name, cmd)
|
|
|
|
// Use ssh command directly - this supports all SSH features including ProxyCommand
|
|
sshCmd := exec.Command("ssh", "-o", "ConnectTimeout=10", "-o", "BatchMode=yes", host.Name, cmd)
|
|
|
|
output, err = sshCmd.Output()
|
|
if err == nil && len(output) > 0 {
|
|
fmt.Fprintf(os.Stderr, "Debug: Command succeeded, got output\n")
|
|
break
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Debug: Command failed: %v\n", err)
|
|
}
|
|
|
|
if err != nil || len(output) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Debug: All port detection commands failed, trying common ports\n")
|
|
// Fallback: try common ports
|
|
return detectCommonPorts(host), nil
|
|
}
|
|
|
|
// Parse the output to extract port numbers
|
|
ports := make([]int, 0)
|
|
lines := strings.Split(string(output), "\n")
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
port, err := strconv.Atoi(line)
|
|
if err == nil && port > 0 && port < 65536 {
|
|
ports = append(ports, port)
|
|
}
|
|
}
|
|
|
|
// Remove duplicates and sort
|
|
ports = removeDuplicates(ports)
|
|
sort.Ints(ports)
|
|
|
|
return ports, nil
|
|
}
|
|
|
|
// detectCommonPorts tries to detect common ports by testing connections through SSH
|
|
func detectCommonPorts(host SSHHost) []int {
|
|
commonPorts := []int{80, 443, 3000, 3001, 4000, 5000, 8000, 8080, 8443, 9000}
|
|
var openPorts []int
|
|
|
|
fmt.Fprintf(os.Stderr, "Debug: Testing common ports on %s\n", host.Name)
|
|
|
|
for _, port := range commonPorts {
|
|
// Test if port is open using SSH to run a quick connection test
|
|
cmd := fmt.Sprintf("timeout 1 bash -c '</dev/tcp/localhost/%d' 2>/dev/null && echo 'open' || echo 'closed'", port)
|
|
sshCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host.Name, cmd)
|
|
|
|
output, err := sshCmd.Output()
|
|
if err == nil && strings.TrimSpace(string(output)) == "open" {
|
|
openPorts = append(openPorts, port)
|
|
fmt.Fprintf(os.Stderr, "Debug: Port %d is open\n", port)
|
|
}
|
|
}
|
|
|
|
return openPorts
|
|
}
|
|
|
|
|
|
|
|
// removeDuplicates removes duplicate integers from a slice
|
|
func removeDuplicates(slice []int) []int {
|
|
keys := make(map[int]bool)
|
|
result := make([]int, 0)
|
|
|
|
for _, item := range slice {
|
|
if !keys[item] {
|
|
keys[item] = true
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
|
|
return result
|
|
} |