Files
1Panel/agent/utils/websocket/process_data.go

520 lines
13 KiB
Go

package websocket
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
agentPsutil "github.com/1Panel-dev/1Panel/agent/utils/psutil"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/process"
)
const defaultTimeout = 10 * time.Second
type WsInput struct {
Type string `json:"type"`
DownloadProgress
PsProcessConfig
SSHSessionConfig
NetConfig
}
type DownloadProgress struct {
Keys []string `json:"keys"`
}
type PsProcessConfig struct {
Pid int32 `json:"pid"`
Name string `json:"name"`
Username string `json:"username"`
}
type SSHSessionConfig struct {
LoginUser string `json:"loginUser"`
LoginIP string `json:"loginIP"`
}
type NetConfig struct {
Port uint32 `json:"port"`
ProcessName string `json:"processName"`
ProcessID int32 `json:"processID"`
}
type PsProcessData struct {
PID int32 `json:"PID"`
Name string `json:"name"`
PPID int32 `json:"PPID"`
Username string `json:"username"`
Status string `json:"status"`
StartTime string `json:"startTime"`
NumThreads int32 `json:"numThreads"`
NumConnections int `json:"numConnections"`
CpuPercent string `json:"cpuPercent"`
DiskRead string `json:"diskRead"`
DiskWrite string `json:"diskWrite"`
CmdLine string `json:"cmdLine"`
Rss string `json:"rss"`
VMS string `json:"vms"`
HWM string `json:"hwm"`
Data string `json:"data"`
Stack string `json:"stack"`
Locked string `json:"locked"`
Swap string `json:"swap"`
Dirty string `json:"dirty"`
PSS string `json:"pss"`
USS string `json:"uss"`
Shared string `json:"shared"`
Text string `json:"text"`
CpuValue float64 `json:"cpuValue"`
RssValue uint64 `json:"rssValue"`
Envs []string `json:"envs"`
OpenFiles []process.OpenFilesStat `json:"openFiles"`
Connects []ProcessConnect `json:"connects"`
}
type ProcessConnect struct {
Type string `json:"type"`
Status string `json:"status"`
Laddr net.Addr `json:"localaddr"`
Raddr net.Addr `json:"remoteaddr"`
PID int32 `json:"PID"`
Name string `json:"name"`
}
type ProcessConnects []ProcessConnect
type sshSession struct {
Username string `json:"username"`
PID int32 `json:"PID"`
Terminal string `json:"terminal"`
Host string `json:"host"`
LoginTime string `json:"loginTime"`
}
func ProcessData(c *Client, inputMsg []byte) {
wsInput := &WsInput{}
err := json.Unmarshal(inputMsg, wsInput)
if err != nil {
global.LOG.Errorf("unmarshal wsInput error,err %s", err.Error())
return
}
switch wsInput.Type {
case "wget":
res, err := getDownloadProcess(wsInput.DownloadProgress)
if err != nil {
return
}
c.Send(res)
case "ps":
res, err := getProcessData(wsInput.PsProcessConfig)
if err != nil {
return
}
c.Send(res)
case "ssh":
res, err := getSSHSessions(wsInput.SSHSessionConfig)
if err != nil {
return
}
c.Send(res)
case "net":
res, err := getNetConnections(wsInput.NetConfig)
if err != nil {
return
}
c.Send(res)
}
}
func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
var result []files.Process
for _, k := range progress.Keys {
value := global.CACHE.Get(k)
if value == "" {
continue
}
downloadProcess := &files.Process{}
if err := json.Unmarshal([]byte(value), downloadProcess); err != nil {
continue
}
downloadProcess.Key = k
result = append(result, *downloadProcess)
if downloadProcess.Status == "" && downloadProcess.Percent == 100 {
global.CACHE.Del(k)
}
}
res, err = json.Marshal(result)
return
}
func handleProcessData(proc *process.Process, processConfig *PsProcessConfig, pidConnections map[int32][]net.ConnectionStat, createTimeResolver *agentPsutil.ProcessCreateTimeResolver) *PsProcessData {
if processConfig.Pid > 0 && processConfig.Pid != proc.Pid {
return nil
}
procData := PsProcessData{
PID: proc.Pid,
}
if procName, err := proc.Name(); err == nil {
procData.Name = procName
} else {
procData.Name = "<UNKNOWN>"
}
if processConfig.Name != "" && !strings.Contains(procData.Name, processConfig.Name) {
return nil
}
if username, err := proc.Username(); err == nil {
procData.Username = username
}
if processConfig.Username != "" && !strings.Contains(procData.Username, processConfig.Username) {
return nil
}
procData.PPID, _ = proc.Ppid()
statusArray, _ := proc.Status()
if len(statusArray) > 0 {
procData.Status = strings.Join(statusArray, ",")
}
createTime, procErr := createTimeResolver.CreateTime(proc)
if procErr == nil {
t := time.Unix(createTime/1000, 0)
procData.StartTime = t.Format("2006-01-02 15:04:05")
}
procData.NumThreads, _ = proc.NumThreads()
procData.CpuValue, _ = proc.CPUPercent()
procData.CpuPercent = fmt.Sprintf("%.2f%%", procData.CpuValue)
if memInfo, err := proc.MemoryInfo(); err == nil {
procData.RssValue = memInfo.RSS
procData.Rss = common.FormatBytes(memInfo.RSS)
} else {
procData.RssValue = 0
}
if connections, ok := pidConnections[proc.Pid]; ok {
procData.NumConnections = len(connections)
}
return &procData
}
func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
processes, err := process.ProcessesWithContext(ctx)
if err != nil {
return
}
connections, err := net.ConnectionsMaxWithContext(ctx, "all", 32768)
if err != nil {
return
}
pidConnections := make(map[int32][]net.ConnectionStat, len(processes))
for _, conn := range connections {
if conn.Pid == 0 {
continue
}
pidConnections[conn.Pid] = append(pidConnections[conn.Pid], conn)
}
result := make([]PsProcessData, 0, len(processes))
createTimeResolver := agentPsutil.NewProcessCreateTimeResolver()
for _, proc := range processes {
procData := handleProcessData(proc, &processConfig, pidConnections, createTimeResolver)
if procData != nil {
result = append(result, *procData)
}
}
res, err = json.Marshal(result)
return
}
func getSSHSessions(config SSHSessionConfig) (res []byte, err error) {
var (
result []sshSession
users []host.UserStat
processes []*process.Process
)
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if sessions, ok := loadLoginctlSSHSessions(ctx); ok && len(sessions) > 0 {
res, err = json.Marshal(filterSSHSessions(sessions, config))
return
}
users, err = host.UsersWithContext(ctx)
if err != nil || len(users) == 0 {
res, err = json.Marshal(result)
return
}
usersByHost := make(map[string][]host.UserStat)
for _, user := range users {
if user.Host == "" {
continue
}
if config.LoginUser != "" && !strings.Contains(user.User, config.LoginUser) {
continue
}
if config.LoginIP != "" && !strings.Contains(user.Host, config.LoginIP) {
continue
}
usersByHost[user.Host] = append(usersByHost[user.Host], user)
}
if len(usersByHost) == 0 {
res, err = json.Marshal(result)
return
}
connections, err := net.ConnectionsMaxWithContext(ctx, "all", 32768)
if err != nil {
res, err = json.Marshal(result)
return
}
pidConnections := make(map[int32][]net.ConnectionStat, 256)
for _, conn := range connections {
if conn.Pid == 0 || conn.Raddr.IP == "" {
continue
}
if _, ok := usersByHost[conn.Raddr.IP]; !ok {
continue
}
pidConnections[conn.Pid] = append(pidConnections[conn.Pid], conn)
}
processes, err = process.ProcessesWithContext(ctx)
if err != nil {
res, err = json.Marshal(result)
return
}
for _, proc := range processes {
name, _ := proc.Name()
if name != "sshd" || proc.Pid == 0 {
continue
}
connections := pidConnections[proc.Pid]
if len(connections) == 0 {
continue
}
cmdline, cmdErr := proc.CmdlineWithContext(ctx)
if cmdErr != nil {
continue
}
for _, conn := range connections {
matchedUsers, exists := usersByHost[conn.Raddr.IP]
if !exists {
continue
}
for _, user := range matchedUsers {
if strings.Contains(cmdline, user.Terminal) {
t := time.Unix(int64(user.Started), 0)
result = append(result, sshSession{
Username: user.User,
Host: user.Host,
Terminal: user.Terminal,
PID: proc.Pid,
LoginTime: t.Format("2006-1-2 15:04:05"),
})
}
}
}
}
res, err = json.Marshal(result)
return
}
func loadLoginctlSSHSessions(ctx context.Context) ([]sshSession, bool) {
if _, err := exec.LookPath("loginctl"); err != nil {
return nil, false
}
output, err := exec.CommandContext(ctx, "loginctl", "list-sessions", "--no-legend", "--no-pager").Output()
if err != nil {
return nil, false
}
var result []sshSession
for _, line := range strings.Split(string(output), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
sessionOutput, err := exec.CommandContext(ctx, "loginctl", "show-session", fields[0], "--no-pager",
"-p", "Name", "-p", "Remote", "-p", "RemoteHost", "-p", "TTY", "-p", "Timestamp", "-p", "Leader", "-p", "Service", "-p", "State").Output()
if err != nil {
continue
}
session, ok := parseLoginctlSSHSession(string(sessionOutput))
if !ok {
continue
}
result = append(result, session)
}
return result, true
}
func filterSSHSessions(sessions []sshSession, config SSHSessionConfig) []sshSession {
result := make([]sshSession, 0, len(sessions))
for _, session := range sessions {
if config.LoginUser != "" && !strings.Contains(session.Username, config.LoginUser) {
continue
}
if config.LoginIP != "" && !strings.Contains(session.Host, config.LoginIP) {
continue
}
result = append(result, session)
}
return result
}
func parseLoginctlSSHSession(output string) (sshSession, bool) {
props := map[string]string{}
for _, line := range strings.Split(output, "\n") {
key, value, ok := strings.Cut(line, "=")
if ok {
props[key] = strings.TrimSpace(value)
}
}
service := props["Service"]
if props["Remote"] != "yes" || props["Name"] == "" || props["RemoteHost"] == "" || props["State"] != "active" || (service != "sshd" && service != "ssh") {
return sshSession{}, false
}
pid, _ := strconv.ParseInt(props["Leader"], 10, 32)
return sshSession{
Username: props["Name"],
Host: props["RemoteHost"],
Terminal: props["TTY"],
PID: int32(pid),
LoginTime: parseLoginctlTimestamp(props["Timestamp"]),
}, true
}
func parseLoginctlTimestamp(value string) string {
fields := strings.Fields(value)
for i := 0; i < len(fields)-1; i++ {
if strings.Count(fields[i], "-") != 2 || strings.Count(fields[i+1], ":") != 2 {
continue
}
candidate := fields[i] + " " + fields[i+1]
t, err := time.ParseInLocation("2006-01-02 15:04:05", candidate, time.Local)
if err != nil {
return candidate
}
return t.Format("2006-1-2 15:04:05")
}
return value
}
func getNetConnections(config NetConfig) (res []byte, err error) {
result := make([]ProcessConnect, 0, 1024)
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
connections, err := net.ConnectionsMaxWithContext(ctx, "all", 32768)
if err != nil {
res, _ = json.Marshal(result)
return
}
pidConnectionsMap := make(map[int32][]net.ConnectionStat, 256)
pidNameMap := make(map[int32]string, 256)
for _, conn := range connections {
if conn.Family != 2 && conn.Family != 10 {
continue
}
if conn.Pid == 0 {
continue
}
if config.ProcessID > 0 && conn.Pid != config.ProcessID {
continue
}
if config.Port > 0 && conn.Laddr.Port != config.Port && conn.Raddr.Port != config.Port {
continue
}
if _, exists := pidNameMap[conn.Pid]; !exists {
pName, _ := getProcessNameWithContext(ctx, conn.Pid)
if pName == "" {
pName = "<UNKNOWN>"
}
pidNameMap[conn.Pid] = pName
}
pidConnectionsMap[conn.Pid] = append(pidConnectionsMap[conn.Pid], conn)
}
for pid, connections := range pidConnectionsMap {
pName := pidNameMap[pid]
if config.ProcessName != "" && !strings.Contains(pName, config.ProcessName) {
continue
}
for _, conn := range connections {
result = append(result, ProcessConnect{
Type: getConnectionType(conn.Type, conn.Family),
Status: conn.Status,
Laddr: conn.Laddr,
Raddr: conn.Raddr,
PID: conn.Pid,
Name: pName,
})
}
}
res, err = json.Marshal(result)
return
}
func getProcessNameWithContext(ctx context.Context, pid int32) (string, error) {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err == nil && len(data) > 0 {
return strings.TrimSpace(string(data)), nil
}
p, err := process.NewProcessWithContext(ctx, pid)
if err != nil {
return "", err
}
return p.Name()
}
func getConnectionType(connType uint32, family uint32) string {
switch {
case connType == 1 && family == 2:
return "tcp"
case connType == 1 && family == 10:
return "tcp6"
case connType == 2 && family == 2:
return "udp"
case connType == 2 && family == 10:
return "udp6"
default:
return "unknown"
}
}