Files
terminal/server/api/portal_api.go
T

560 lines
13 KiB
Go

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
}