package api import ( "context" "strconv" "strings" "next-terminal/server/common/maps" "next-terminal/server/common/nt" "next-terminal/server/global/session" "next-terminal/server/log" "next-terminal/server/model" "next-terminal/server/repository" "next-terminal/server/service" "github.com/labstack/echo/v4" "golang.org/x/crypto/ssh" ) type PortalApi struct{} func (api PortalApi) AssetsEndpoint(c echo.Context) error { assetType := c.QueryParam("type") ctx := context.TODO() var items []model.Asset var err error if assetType != "" { items, err = repository.AssetRepository.FindByProtocol(ctx, assetType) } else { items, err = repository.AssetRepository.FindAll(ctx) } if err != nil { return err } result := make([]maps.Map, len(items)) for i, a := range items { result[i] = maps.Map{ "id": a.ID, "logo": "", "name": a.Name, "address": a.IP + ":" + strconv.Itoa(a.Port), "protocol": a.Protocol, "tags": []string{}, "status": "unknown", "type": "asset", "groupId": "", "users": []string{}, } } return Success(c, result) } func (api PortalApi) DatabaseAssetsEndpoint(c echo.Context) error { return Success(c, []interface{}{}) } func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error { protocol := c.QueryParam("protocol") keyword := c.QueryParam("keyword") ctx := context.TODO() items, _ := repository.AssetRepository.FindByProtocol(ctx, protocol) groups, _ := repository.AssetGroupRepository.FindAll(ctx) tree := make([]maps.Map, 0) for _, g := range groups { node := maps.Map{ "key": "group_" + g.ID, "title": g.Name, "isLeaf": false, "children": []interface{}{}, } tree = append(tree, node) } for _, a := range items { if keyword != "" && !containsKeyword(a.Name, keyword) { continue } node := maps.Map{ "key": "asset_" + a.ID, "title": a.Name, "isLeaf": true, "extra": maps.Map{ "protocol": a.Protocol, "logo": "", "status": "unknown", "network": a.IP, "wolEnabled": false, }, } tree = append(tree, node) } return Success(c, tree) } func containsKeyword(name, keyword string) bool { if keyword == "" { return true } for _, ch := range name { for _, kh := range keyword { if ch == kh { return true } } } return false } func (api PortalApi) WebsitesTreeEndpoint(c echo.Context) error { keyword := c.QueryParam("keyword") items, _ := repository.WebsiteRepository.FindAll(context.TODO()) tree := make([]maps.Map, 0) for _, w := range items { if keyword != "" && !containsKeyword(w.Name, keyword) { continue } node := maps.Map{ "key": "website_" + w.ID, "title": w.Name, "isLeaf": true, "extra": maps.Map{ "protocol": "website", "logo": "", "status": w.Status, "network": w.Domain, }, } tree = append(tree, node) } return Success(c, tree) } func (api PortalApi) AssetsGroupTreeEndpoint(c echo.Context) error { groups, _ := repository.AssetGroupRepository.FindAll(context.TODO()) tree := make([]maps.Map, 0) for _, g := range groups { node := maps.Map{ "key": "group_" + g.ID, "title": g.Name, "isLeaf": false, "children": []interface{}{}, } tree = append(tree, node) } return Success(c, tree) } func (api PortalApi) WebsitesGroupTreeEndpoint(c echo.Context) error { return Success(c, []interface{}{}) } func (api PortalApi) AccessRequireMfaEndpoint(c echo.Context) error { return Success(c, maps.Map{"required": false}) } func (api PortalApi) CreateSessionEndpoint(c echo.Context) error { var req map[string]interface{} if err := c.Bind(&req); err != nil { return err } assetId, _ := req["assetId"].(string) account, _ := GetCurrentAccount(c) clientIP := service.PropertyService.GetClientIP(c) s, err := service.SessionService.Create(clientIP, assetId, nt.Native, account) if err != nil { return err } asset, _ := repository.AssetRepository.FindById(context.TODO(), assetId) assetName := asset.Name if assetName == "" { assetName = assetId } return Success(c, maps.Map{ "id": s.ID, "protocol": s.Protocol, "assetName": assetName, "strategy": maps.Map{ "upload": s.Upload == "1", "download": s.Download == "1", "delete": s.Delete == "1", "rename": s.Rename == "1", "edit": s.Edit == "1", "copy": s.Copy == "1", "paste": s.Paste == "1", "fileSystem": s.FileSystem == "1", }, "url": "", "watermark": maps.Map{}, "readonly": false, "idle": 3600, "fileSystem": s.FileSystem == "1", "width": 800, "height": 600, }) } func (api PortalApi) GetSessionEndpoint(c echo.Context) error { id := c.Param("id") return Success(c, maps.Map{ "id": id, "protocol": "ssh", "assetName": id, "strategy": maps.Map{}, "url": "", "watermark": maps.Map{}, "readonly": false, "idle": 3600, "fileSystem": false, "width": 800, "height": 600, }) } func (api PortalApi) GetShareEndpoint(c echo.Context) error { return Success(c, maps.Map{ "enabled": false, "passcode": "", "url": "", }) } func (api PortalApi) CreateShareEndpoint(c echo.Context) error { return Success(c, maps.Map{ "enabled": true, "passcode": "", "url": "", }) } func (api PortalApi) CancelShareEndpoint(c echo.Context) error { return Success(c, nil) } func (api PortalApi) AccessWebsiteEndpoint(c echo.Context) error { websiteId := c.QueryParam("websiteId") item, _ := repository.WebsiteRepository.FindById(context.TODO(), websiteId) url := "" if item.TargetUrl != "" { url = item.TargetUrl } else { url = "http://" + item.Domain } return Success(c, maps.Map{"url": url}) } func (api PortalApi) AllowWebsiteIpEndpoint(c echo.Context) error { return Success(c, nil) } func (api PortalApi) WakeOnLanEndpoint(c echo.Context) error { assetId := c.Param("id") _ = assetId return Success(c, maps.Map{ "delay": 5, }) } func (api PortalApi) PingAssetEndpoint(c echo.Context) error { assetId := c.Param("id") item, _ := repository.AssetRepository.FindById(context.TODO(), assetId) return Success(c, maps.Map{ "name": item.Name, "active": true, "usedTime": 10, "usedTimeStr": "10ms", }) } func (api PortalApi) TerminalStatsEndpoint(c echo.Context) error { sessionId := c.Param("id") sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Success(c, maps.Map{ "info": maps.Map{ "id": "", "name": "", "version": "", "arch": "", "uptime": 0, "hostname": "", "upDays": 0, }, "load": maps.Map{ "load1": "0.00", "load5": "0.00", "load15": "0.00", "runningProcess": "0", "totalProcess": "0", }, "memory": maps.Map{ "memTotal": 0, "memAvailable": 0, "memFree": 0, "memBuffers": 0, "memCached": 0, "memUsed": 0, "swapTotal": 0, "swapFree": 0, }, "fileSystems": []interface{}{}, "network": []interface{}{}, "cpu": []interface{}{}, }) } stats, err := getSystemStats(sess.NextTerminal.SshClient) if err != nil { log.Warn("Failed to get system stats", log.NamedError("error", err)) return Success(c, maps.Map{ "info": maps.Map{ "id": "", "name": "", "version": "", "arch": "", "uptime": 0, "hostname": "", "upDays": 0, }, "load": maps.Map{ "load1": "0.00", "load5": "0.00", "load15": "0.00", "runningProcess": "0", "totalProcess": "0", }, "memory": maps.Map{ "memTotal": 0, "memAvailable": 0, "memFree": 0, "memBuffers": 0, "memCached": 0, "memUsed": 0, "swapTotal": 0, "swapFree": 0, }, "fileSystems": []interface{}{}, "network": []interface{}{}, "cpu": []interface{}{}, }) } return Success(c, stats) } func getSystemStats(client *ssh.Client) (maps.Map, error) { result := maps.Map{} // Get hostname hostname, _ := sshExec(client, "hostname") // Get OS info osId, _ := sshExec(client, "cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'") osName, _ := sshExec(client, "cat /etc/os-release 2>/dev/null | grep '^NAME=' | cut -d'=' -f2 | tr -d '\"'") osVersion, _ := sshExec(client, "cat /etc/os-release 2>/dev/null | grep '^VERSION_ID=' | cut -d'=' -f2 | tr -d '\"'") // Get arch arch, _ := sshExec(client, "uname -m") // Get uptime uptimeStr, _ := sshExec(client, "cat /proc/uptime | awk '{print $1}'") uptimeFloat, _ := strconv.ParseFloat(strings.TrimSpace(uptimeStr), 64) uptime := int64(uptimeFloat) upDays := uptime / 86400 // Get load average loadStr, _ := sshExec(client, "cat /proc/loadavg") loadParts := strings.Fields(loadStr) load1, load5, load15 := "0.00", "0.00", "0.00" if len(loadParts) >= 3 { load1 = loadParts[0] load5 = loadParts[1] load15 = loadParts[2] } // Get process count processStr, _ := sshExec(client, "ps aux | wc -l") totalProcess := strings.TrimSpace(processStr) // Get memory info memInfo, _ := sshExec(client, "cat /proc/meminfo") memTotal, memFree, memAvailable, memBuffers, memCached, swapTotal, swapFree := parseMemInfo(memInfo) memUsed := memTotal - memAvailable // Get CPU info cpuInfo, _ := sshExec(client, "cat /proc/stat | head -1") cpuStats := parseCpuStat(cpuInfo) // Get filesystem info dfOutput, _ := sshExec(client, "df -B1 | tail -n +2") fileSystems := parseDfOutput(dfOutput) // Get network info netDev, _ := sshExec(client, "cat /proc/net/dev | tail -n +3") network := parseNetDev(netDev) result["info"] = maps.Map{ "id": strings.TrimSpace(osId), "name": strings.TrimSpace(osName), "version": strings.TrimSpace(osVersion), "arch": strings.TrimSpace(arch), "uptime": uptime, "hostname": strings.TrimSpace(hostname), "upDays": upDays, } result["load"] = maps.Map{ "load1": load1, "load5": load5, "load15": load15, "runningProcess": totalProcess, "totalProcess": totalProcess, } result["memory"] = maps.Map{ "memTotal": memTotal * 1024, "memAvailable": memAvailable * 1024, "memFree": memFree * 1024, "memBuffers": memBuffers * 1024, "memCached": memCached * 1024, "memUsed": memUsed * 1024, "swapTotal": swapTotal * 1024, "swapFree": swapFree * 1024, } result["cpu"] = cpuStats result["fileSystems"] = fileSystems result["network"] = network return result, nil } func sshExec(client *ssh.Client, cmd string) (string, error) { session, err := client.NewSession() if err != nil { return "", err } defer session.Close() output, err := session.CombinedOutput(cmd) if err != nil { return "", err } return string(output), nil } func parseMemInfo(info string) (total, free, available, buffers, cached, swapTotal, swapFree int64) { lines := strings.Split(info, "\n") for _, line := range lines { parts := strings.Fields(line) if len(parts) < 2 { continue } key := strings.TrimSuffix(parts[0], ":") value, _ := strconv.ParseInt(parts[1], 10, 64) switch key { case "MemTotal": total = value case "MemFree": free = value case "MemAvailable": available = value case "Buffers": buffers = value case "Cached": cached = value case "SwapTotal": swapTotal = value case "SwapFree": swapFree = value } } return } func parseCpuStat(stat string) []interface{} { parts := strings.Fields(stat) if len(parts) < 5 { return []interface{}{maps.Map{"user": 0.0, "nice": 0.0, "system": 0.0}} } user, _ := strconv.ParseFloat(parts[1], 64) nice, _ := strconv.ParseFloat(parts[2], 64) system, _ := strconv.ParseFloat(parts[3], 64) return []interface{}{ maps.Map{ "user": user, "nice": nice, "system": system, }, } } func parseDfOutput(output string) []interface{} { var result []interface{} lines := strings.Split(output, "\n") for _, line := range lines { if strings.TrimSpace(line) == "" { continue } parts := strings.Fields(line) if len(parts) < 6 { continue } total, _ := strconv.ParseInt(parts[1], 10, 64) used, _ := strconv.ParseInt(parts[2], 10, 64) free, _ := strconv.ParseInt(parts[3], 10, 64) var percent float64 = 0 if total > 0 { percent = float64(used) / float64(total) } result = append(result, maps.Map{ "mountPoint": parts[5], "total": total, "used": used, "free": free, "percent": percent, }) } return result } func parseNetDev(output string) []interface{} { var result []interface{} lines := strings.Split(output, "\n") for _, line := range lines { if strings.TrimSpace(line) == "" { continue } parts := strings.Fields(line) if len(parts) < 10 { continue } iface := strings.TrimSuffix(parts[0], ":") if strings.HasPrefix(iface, "lo") { continue } rx, _ := strconv.ParseInt(parts[1], 10, 64) tx, _ := strconv.ParseInt(parts[9], 10, 64) result = append(result, maps.Map{ "iface": iface, "rx": rx, "tx": tx, "rxSec": 0, "txSec": 0, }) } return result }