package services import ( "bufio" "bytes" "fmt" "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: source /etc/os-release is the most compatible way across Linux distros if out, err := runSSHCommand(client, `. /etc/os-release 2>/dev/null && printf '%s\n' "$PRETTY_NAME"`); err == nil && out != "" { result.OsVersion = strings.TrimSpace(out) } else if out, err := runSSHCommand(client, "uname -sr"); err == nil { result.OsVersion = strings.TrimSpace(out) } // CPU: read /proc/stat twice with 1s sleep in Go to avoid complex shell quoting if cpuUsage, cores, err := getCPUInfo(client); err == nil { result.CPU = cpuUsage + "%" if cores != "" { result.CPU += " (" + cores + " cores)" } result.RawCPUInfo = result.CPU } // Memory: use /proc/meminfo instead of free for better compatibility if out, err := runSSHCommand(client, "awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} END{u=t-a; if(t>0) printf \"%.1f/%.1f (%.0f%%)\", u/1024/1024, t/1024/1024, u*100/t}' /proc/meminfo"); err == nil && out != "" { result.Memory = strings.TrimSpace(out) result.RawMemoryInfo = result.Memory } // Disk: keep real block devices only, exclude all virtual filesystems if out, err := runSSHCommand(client, "df -hP | awk 'NR>1 && ($1 ~ /^\\/dev/ || $1 ~ /^(LABEL=|UUID=)/) {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: try ss first, fallback to netstat, fallback to /proc/net/tcp ports, _ := getListenPorts(client) 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 } func getCPUInfo(client *ssh.Client) (usage string, cores string, err error) { readStat := func() (idle, total float64, e error) { out, e := runSSHCommand(client, "awk '/^cpu /{print $2,$3,$4,$5}' /proc/stat") if e != nil { return 0, 0, e } parts := strings.Fields(strings.TrimSpace(out)) if len(parts) < 4 { return 0, 0, fmt.Errorf("unexpected /proc/stat format") } vals := make([]float64, 4) for i := 0; i < 4; i++ { v, e := strconv.ParseFloat(parts[i], 64) if e != nil { return 0, 0, e } vals[i] = v } user, nice, system, idleTick := vals[0], vals[1], vals[2], vals[3] idle = idleTick total = user + nice + system + idleTick return idle, total, nil } idle1, total1, err := readStat() if err != nil { return "", "", err } time.Sleep(1 * time.Second) idle2, total2, err := readStat() if err != nil { return "", "", err } dTotal := total2 - total1 dIdle := idle2 - idle1 if dTotal > 0 { pct := 100.0 * (dTotal - dIdle) / dTotal if pct < 0 { pct = 0 } if pct > 100 { pct = 100 } usage = fmt.Sprintf("%.1f", pct) } else { usage = "0.0" } if cout, e := runSSHCommand(client, "nproc"); e == nil { cores = strings.TrimSpace(cout) } return usage, cores, nil } func getListenPorts(client *ssh.Client) ([]int, error) { var out string var err error // try ss without -p (no need for process info, more compatible) out, err = runSSHCommand(client, "ss -tln 2>/dev/null | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -n | uniq") if err != nil || strings.TrimSpace(out) == "" { // fallback to netstat out, err = runSSHCommand(client, "netstat -tln 2>/dev/null | awk 'NR>2 {print $4}' | sed 's/.*://' | sort -n | uniq") } if err != nil || strings.TrimSpace(out) == "" { // fallback to /proc/net/tcp out, err = runSSHCommand(client, "awk 'NR>1 {printf \"%04X\\n\", strtonum(\"0x\"$2)}' /proc/net/tcp") } if err != nil { return nil, err } ports := []int{} scanner := bufio.NewScanner(strings.NewReader(out)) for scanner.Scan() { p := strings.TrimSpace(scanner.Text()) if p == "" { continue } // handle both decimal ports and hex ports from /proc/net/tcp var pi int var e error if len(p) == 4 && isHex(p) { var h uint64 h, e = strconv.ParseUint(p, 16, 32) pi = int(h) } else { pi, e = strconv.Atoi(p) } if e == nil && pi > 0 { ports = append(ports, pi) } } return ports, nil } func isHex(s string) bool { for _, c := range s { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true }