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 }