Files
lan-manager/server/services/ssh.go

114 lines
3.5 KiB
Go

package services
import (
"bufio"
"bytes"
"lan-manager/server/config"
"lan-manager/server/models"
"strconv"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
func GetSSHInfo(ip string, port int, user, pass string) (*models.SSHInfoResult, error) {
if port <= 0 {
port = 22
}
cfg := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(pass),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: time.Duration(config.Load().SSHTimeout) * time.Second,
}
client, err := ssh.Dial("tcp", ip+":"+strconv.Itoa(port), cfg)
if err != nil {
return nil, err
}
defer client.Close()
result := &models.SSHInfoResult{}
// Hostname
if out, err := runSSHCommand(client, "hostname"); err == nil {
result.Hostname = strings.TrimSpace(out)
}
// OS Version
if out, err := runSSHCommand(client, "cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"'"); err == nil && out != "" {
result.OsVersion = strings.TrimSpace(out)
} else if out, err := runSSHCommand(client, "uname -sr"); err == nil {
result.OsVersion = strings.TrimSpace(out)
}
// CPU (two samples from /proc/stat for accurate real-time usage)
cpuScript := `c1=$(awk '/^cpu /{print $2+$4,$2+$4+$5}' /proc/stat) && sleep 1 && c2=$(awk '/^cpu /{print $2+$4,$2+$4+$5}' /proc/stat) && awk 'BEGIN{split("'"$c1"'", a, " "); split("'"$c2"'", b, " "); if(b[2]-a[2]>0) printf "%.1f", 100*(b[1]-a[1])/(b[2]-a[2]); else print "0.0"}'`
if out, err := runSSHCommand(client, cpuScript); err == nil && out != "" {
cpuUsage := strings.TrimSpace(out)
if cpuUsage == "" {
cpuUsage = "0.0"
}
cores := ""
if cout, err := runSSHCommand(client, "nproc"); err == nil {
cores = strings.TrimSpace(cout)
}
result.CPU = cpuUsage + "%"
if cores != "" {
result.CPU += " (" + cores + " cores)"
}
result.RawCPUInfo = result.CPU
}
// Memory
if out, err := runSSHCommand(client, "free -m | awk 'NR==2{printf \"%.1f/%.1f (%.0f%%)\", $3/1024,$2/1024,$3*100/$2 }'"); err == nil && out != "" {
result.Memory = strings.TrimSpace(out)
result.RawMemoryInfo = result.Memory
}
// Disk (exclude virtual filesystems, keep all real partitions)
// Use df -hP to prevent long filesystem names from wrapping to next line
if out, err := runSSHCommand(client, "df -hP | awk 'NR>1 && $1 !~ /^(tmpfs|devtmpfs|squashfs|overlay|efivarfs|tracefs|securityfs|pstore|bpf|configfs|fusectl|mqueue|hugetlbfs|rpc_pipefs|nfsd|binfmt_misc|autofs|devpts|proc|sysfs|cgroup|cgroup2|ramfs)$/ {printf \"%s %s/%s (%s) \", $6,$3,$2,$5}'"); err == nil && out != "" {
result.Disk = strings.TrimSpace(out)
result.RawDiskInfo = result.Disk
}
// Uptime
if out, err := runSSHCommand(client, "uptime -p 2>/dev/null || uptime | awk -F',' '{print $1}'"); err == nil {
result.Uptime = strings.TrimSpace(out)
}
// Listen ports
if out, err := runSSHCommand(client, "ss -tlnp | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -n | uniq"); err == nil && out != "" {
ports := []int{}
scanner := bufio.NewScanner(strings.NewReader(out))
for scanner.Scan() {
p := strings.TrimSpace(scanner.Text())
if p == "" {
continue
}
if pi, err := strconv.Atoi(p); err == nil {
ports = append(ports, pi)
}
}
result.ListenPorts = ports
}
return result, nil
}
func runSSHCommand(client *ssh.Client, cmd string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var b bytes.Buffer
session.Stdout = &b
err = session.Run(cmd)
return b.String(), err
}