节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
25.76 KiB
Stardust
#if !NET40
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using NewLife;
using NewLife.Agent;
using NewLife.Agent.WebPanel;
using NewLife.Data;
using NewLife.Http;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Serialization;
using Stardust;
using Stardust.Managers;
using Stardust.Models;

namespace StarAgent.WebPanel;

/// <summary>StarAgent Web管理面板 API 控制器</summary>
/// <remarks>
/// 提供 StarAgent 专有 API 接口,包括子服务管理、星尘配置、本机信息等功能。
/// 路由格式:/api/star/{MethodName},由 HttpServer.ControllerHandler 自动映射。
/// 需要 Bearer Token 鉴权。
/// </remarks>
public class StarApi : IHttpController
{
    #region 属性
    /// <summary>当前Http上下文。由 ControllerHandler 自动注入</summary>
    public IHttpContext? Context { get; set; }

    /// <summary>获取服务管理器</summary>
    private ServiceManager? GetManager()
    {
        var svc = AgentWebPanel.Current?.Service;
        if (svc is IServiceProvider provider)
            return provider.GetService(typeof(ServiceManager)) as ServiceManager;

        return null;
    }

    /// <summary>获取 StarSetting</summary>
    private static StarSetting GetStarSetting() => StarSetting.Current;

    /// <summary>获取 StarAgentSetting</summary>
    private static StarAgentSetting GetAgentSetting() => StarAgentSetting.Current;
    #endregion

    #region 鉴权
    private Boolean CheckAuth()
    {
        var ctx = Context;
        if (ctx == null) return false;

        var auth = ctx.Request.Headers["Authorization"];
        if (auth.IsNullOrEmpty() || !auth.StartsWithIgnoreCase("Bearer ")) return false;

        var token = auth.Substring("Bearer ".Length).Trim();
        if (token.IsNullOrEmpty()) return false;

        return AgentWebPanel.Current?.ValidateToken(token) == true;
    }
    #endregion

    #region 子服务管理
    /// <summary>获取所有子服务列表</summary>
    /// <returns>服务列表,含运行状态</returns>
    public Object Services()
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        var list = manager.Services;
        var runningList = manager.RunningServices;

        var services = list?.Select(svc =>
        {
            var running = runningList?.FirstOrDefault(e => e.Name.EqualIgnoreCase(svc.Name));
            return new
            {
                svc.Name,
                svc.FileName,
                svc.Arguments,
                svc.WorkingDirectory,
                svc.Enable,
                mode = svc.Mode.ToString(),
                svc.MaxMemory,
                svc.HealthCheck,
                svc.AllowMultiple,
                svc.AutoStop,
                svc.ReloadOnChange,
                svc.Environments,
                svc.OomScoreAdjust,
                priority = svc.Priority.ToString(),
                svc.UserName,
                Running = running != null,
                ProcessId = running?.ProcessId ?? 0,
                ProcessName = running?.ProcessName ?? "",
                StartTime = running?.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
            };
        }).ToArray();

        return new
        {
            code = 0,
            data = new
            {
                services,
                total = services?.Length ?? 0,
                running = runningList?.Length ?? 0
            }
        };
    }

    /// <summary>启动子服务</summary>
    /// <param name="serviceName">服务名称</param>
    /// <returns>操作结果</returns>
    public Object StartService(String serviceName)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        if (serviceName.IsNullOrEmpty()) return new { code = 400, message = L.T("Service name cannot be empty.") };

        try
        {
            var result = manager.Start(serviceName);
            return new
            {
                code = result ? 0 : 1,
                message = result ? L.T("Service started successfully.") : L.T("Service failed to start or does not exist."),
                serviceName
            };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while starting service: {0}", ex.Message), serviceName };
        }
    }

    /// <summary>停止子服务</summary>
    /// <param name="serviceName">服务名称</param>
    /// <returns>操作结果</returns>
    public Object StopService(String serviceName)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        if (serviceName.IsNullOrEmpty()) return new { code = 400, message = L.T("Service name cannot be empty.") };

        try
        {
            var result = manager.Stop(serviceName, L.TLog("Stopped from the web panel"));
            return new
            {
                code = result ? 0 : 1,
                message = result ? L.T("Service stopped successfully.") : L.T("Service failed to stop or does not exist."),
                serviceName
            };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while stopping service: {0}", ex.Message), serviceName };
        }
    }

    /// <summary>重启子服务</summary>
    /// <param name="serviceName">服务名称</param>
    /// <returns>操作结果</returns>
    public Object RestartService(String serviceName)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        if (serviceName.IsNullOrEmpty()) return new { code = 400, message = L.T("Service name cannot be empty.") };

        try
        {
            // 停止服务
            var running = manager.RunningServices?.Any(e => e.Name.EqualIgnoreCase(serviceName)) == true;
            if (running)
            {
                var stopResult = manager.Stop(serviceName, L.TLog("Restarted from the web panel"));
                if (!stopResult) return new { code = 1, message = L.T("Failed to stop service."), serviceName };
                Thread.Sleep(1000);
            }

            // 启动服务
            var startResult = manager.Start(serviceName);
            return new
            {
                code = startResult ? 0 : 1,
                message = startResult ? L.T("Service restarted successfully.") : L.T("Failed to start service."),
                serviceName
            };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while restarting service: {0}", ex.Message), serviceName };
        }
    }

    /// <summary>新增或更新子服务</summary>
    /// <param name="info">服务信息</param>
    /// <returns>操作结果</returns>
    public Object AddService(ServiceInfo info)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        if (info == null || info.Name.IsNullOrEmpty())
            return new { code = 400, message = L.T("Service name cannot be empty.") };

        try
        {
            manager.Add(info);

            // 持久化到配置
            var set = StarAgentSetting.Current;
            set.Services = manager.Services;
            set.Save();

            return new { code = 0, message = L.T("Service [{0}] added or updated.", info.Name), serviceName = info.Name };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while adding service: {0}", ex.Message) };
        }
    }

    /// <summary>删除子服务</summary>
    /// <param name="serviceName">服务名称</param>
    /// <returns>操作结果</returns>
    public Object RemoveService(String serviceName)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var manager = GetManager();
        if (manager == null) return new { code = 500, message = L.T("Service manager not available") };

        if (serviceName.IsNullOrEmpty()) return new { code = 400, message = L.T("Service name cannot be empty.") };

        try
        {
            // 先停止服务
            if (manager.RunningServices?.Any(e => e.Name.EqualIgnoreCase(serviceName)) == true)
                manager.Stop(serviceName, L.TLog("Removed from the web panel"));

            manager.Remove(serviceName);

            // 持久化到配置
            var set = StarAgentSetting.Current;
            set.Services = manager.Services;
            set.Save();

            return new { code = 0, message = L.T("Service [{0}] removed.", serviceName), serviceName };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while removing service: {0}", ex.Message), serviceName };
        }
    }
    #endregion

    #region 星尘配置
    /// <summary>获取星尘配置</summary>
    /// <returns>StarSetting + StarAgentSetting 所有配置项</returns>
    public Object GetStarConfig()
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        var starSet = GetStarSetting();
        var agentSet = GetAgentSetting();

        var items = new List<Object>();

        // StarSetting 配置项
        items.Add(new { group = L.T("StarServer connection"), items = new Object[]
        {
            new { name = "Server", displayName = L.T("Server address"), value = (Object)(starSet.Server ?? ""), type = "String", description = L.T("Stardust server address, e.g. http://star.newlifex.com:6600") },
            new { name = "AppKey", displayName = L.T("Application ID"), value = (Object)(starSet.AppKey ?? ""), type = "String", description = L.T("Application ID registered with the Stardust registry") },
            new { name = "Secret", displayName = L.T("Application secret"), value = (Object)(starSet.Secret ?? ""), type = "Password", description = L.T("Application secret registered with the Stardust registry") },
        }});

        items.Add(new { group = L.T("StarAgent local settings"), items = new Object[]
        {
            new { name = "LocalPort", displayName = L.T("Local port (UDP)"), value = (Object)agentSet.LocalPort, type = "Int32", description = L.T("Local API port (UDP), default 5500. Shares the port with the web panel: UDP for local RPC, TCP for the web panel") },
            new { name = "WebBind", displayName = L.T("Panel bind address"), value = (Object)(agentSet.WebBind ?? ""), type = "String", description = L.T("Web panel listen address, default 127.0.0.1 for local access only. Set to * for all interfaces or a specific IP. Requires a StarAgent restart") },
            new { name = "Code", displayName = L.T("Node code"), value = (Object)(agentSet.Code ?? ""), type = "String", description = L.T("Unique code of the current node") },
            new { name = "Secret_Agent", displayName = L.T("Node secret"), value = (Object)(agentSet.Secret ?? ""), type = "Password", description = L.T("Secret used for node communication") },
            new { name = "Project", displayName = L.T("Project"), value = (Object)(agentSet.Project ?? ""), type = "String", description = L.T("Project the new node joins by default") },
            new { name = "Channel", displayName = L.T("Update channel"), value = (Object)agentSet.Channel, type = "String", description = L.T("Update channel: Release/Debug") },
            new { name = "Delay", displayName = L.T("Delay (ms)"), value = (Object)agentSet.Delay, type = "Int32", description = L.T("Delay before restarting the process or service, default 3000 ms") },
            new { name = "SyncTime", displayName = L.T("Sync interval (s)"), value = (Object)agentSet.SyncTime, type = "Int32", description = L.T("Periodically sync server time to local, 0 to disable") },
            new { name = "StartupHook", displayName = L.T("Startup hook"), value = (Object)agentSet.StartupHook, type = "Boolean", description = L.T("Inject the Stardust monitoring hook into .NET applications when launching the target process") },
            new { name = "UseAutorun", displayName = L.T("Windows autorun"), value = (Object)agentSet.UseAutorun, type = "Boolean", description = L.T("Autorun requires a signed-in desktop; false by default uses the system service") },
        }});

        return new { code = 0, data = new { groups = items } };
    }

    /// <summary>更新星尘配置</summary>
    /// <param name="updates">配置更新键值对</param>
    /// <returns>操作结果</returns>
    public Object UpdateStarConfig(Dictionary<String, Object> updates)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        if (updates == null || updates.Count == 0)
            return new { code = 400, message = L.T("No configuration items to update.") };

        try
        {
            var starSet = GetStarSetting();
            var agentSet = GetAgentSetting();
            var changedStar = false;
            var changedAgent = false;

            // 安全白名单:只允许更新以下字段
            var starFields = new[] { "Server", "AppKey", "Secret" };
            var agentFields = new[] { "LocalPort", "WebBind", "Code", "Secret", "Project", "Channel", "Delay", "SyncTime", "StartupHook", "UseAutorun" };

            foreach (var kv in updates)
            {
                var name = kv.Key;
                var value = kv.Value;

                if (starFields.Contains(name))
                {
                    switch (name)
                    {
                        case "Server": starSet.Server = value?.ToString() ?? ""; break;
                        case "AppKey": starSet.AppKey = value?.ToString(); break;
                        case "Secret": starSet.Secret = value?.ToString(); break;
                    }
                    changedStar = true;
                }
                else if (agentFields.Contains(name))
                {
                    switch (name)
                    {
                        case "LocalPort": agentSet.LocalPort = Convert.ToInt32(value ?? 5500); break;
                        case "WebBind": agentSet.WebBind = value?.ToString() ?? "127.0.0.1"; break;
                        case "Code": agentSet.Code = value?.ToString() ?? ""; break;
                        case "Secret": agentSet.Secret = value?.ToString(); break;
                        case "Project": agentSet.Project = value?.ToString(); break;
                        case "Channel": agentSet.Channel = value?.ToString() ?? "Release"; break;
                        case "Delay": agentSet.Delay = Convert.ToInt32(value ?? 3000); break;
                        case "SyncTime": agentSet.SyncTime = Convert.ToInt32(value ?? 0); break;
                        case "StartupHook": agentSet.StartupHook = Convert.ToBoolean(value ?? false); break;
                        case "UseAutorun": agentSet.UseAutorun = Convert.ToBoolean(value ?? false); break;
                    }
                    changedAgent = true;
                }
            }

            if (changedStar) starSet.Save();
            if (changedAgent) agentSet.Save();

            var saved = new List<String>();
            if (changedStar) saved.Add("StarSetting");
            if (changedAgent) saved.Add("StarAgentSetting");

            return new { code = 0, message = L.T("Configuration saved to: {0}", saved.Join(", ")) };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while saving configuration: {0}", ex.Message) };
        }
    }
    #endregion

    #region 本机信息
    /// <summary>获取本机详细信息</summary>
    /// <returns>丰富的本机信息</returns>
    public Object Machine()
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        try
        {
            var mi = MachineInfo.GetCurrent();
            mi.Refresh();

            // 系统概览
            var osInfo = new
            {
                os = Environment.OSVersion.ToString(),
                platform = Runtime.Windows ? "Windows" : Runtime.Linux ? "Linux" : Runtime.OSX ? "macOS" : "Other",
                hostName = Environment.MachineName,
                userName = Environment.UserName,
                processorCount = Environment.ProcessorCount,
                tickCount = Runtime.TickCount64 / 1000,
                hostUptime = FormatUptime(Runtime.TickCount64 / 1000),
#if NET45 || NET461
                runtime = Environment.Version + "",
                processArch = IntPtr.Size == 8 ? "X64" : "X86",
                systemArch = IntPtr.Size == 8 ? "X64" : "X86",
#else
                runtime = RuntimeInformation.FrameworkDescription,
                processArch = RuntimeInformation.ProcessArchitecture.ToString(),
                systemArch = RuntimeInformation.OSArchitecture.ToString(),
#endif
            };

            // CPU
            var cpuInfo = new
            {
                cpuName = mi.Processor ?? "",
                cpuCount = Environment.ProcessorCount,
                cpuRate = mi.CpuRate,
                cpuRatePercent = (mi.CpuRate * 100).ToString("F1") + "%",
            };

            // 内存信息
            var totalMemory = (Int64)mi.Memory;
            var availableMemory = (Int64)mi.AvailableMemory;
            var usedMemory = totalMemory - availableMemory;
            var memRate = totalMemory > 0 ? (Double)usedMemory / totalMemory : 0;
            var memoryInfo = new
            {
                totalMemory = FormatBytes(totalMemory),
                usedMemory = FormatBytes(usedMemory),
                availableMemory = FormatBytes(availableMemory),
                memoryRate = memRate,
                memoryRatePercent = (memRate * 100).ToString("F1") + "%",
                totalMB = totalMemory / 1024 / 1024,
                usedMB = usedMemory / 1024 / 1024,
                availableMB = availableMemory / 1024 / 1024,
            };

            // 磁盘信息
            var drives = DriveInfo.GetDrives()
                .Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
                .Select(d =>
                {
                    var total = d.TotalSize;
                    var free = d.TotalFreeSpace;
                    var used = total - free;
                    var rate = total > 0 ? (Double)used / total : 0;
                    return new
                    {
                        name = d.Name,
                        label = d.VolumeLabel ?? "",
                        format = d.DriveFormat,
                        totalSize = FormatBytes(total),
                        usedSize = FormatBytes(used),
                        freeSize = FormatBytes(free),
                        usedPercent = (rate * 100).ToString("F1") + "%",
                        totalMB = total / 1024 / 1024,
                        usedMB = used / 1024 / 1024,
                        freeMB = free / 1024 / 1024,
                    };
                }).ToArray();

            // 网络信息
            var _Excludes = new[] { "Loopback", "VMware", "VBox", "Virtual", "Teredo", "Tunnel", "VPN", "VNIC", "IEEE", "Filter", "Npcap", "QoS", "Miniport", "Kernel Debug" };
            var nics = NetworkInterface.GetAllNetworkInterfaces()
                .Where(n => n.OperationalStatus == OperationalStatus.Up &&
                       n.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
                       n.NetworkInterfaceType != NetworkInterfaceType.Tunnel &&
                       !_Excludes.Any(e => n.Description.Contains(e)))
                .Select(n =>
                {
                    var props = n.GetIPProperties();
                    var ip = props.UnicastAddresses
                        .FirstOrDefault(u => u.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                        ?.Address.ToString() ?? "";
                    var stats = n.GetIPv4Statistics();
                    return new
                    {
                        name = n.Name,
                        description = n.Description,
                        ip,
                        mac = n.GetPhysicalAddress()?.ToString() ?? "",
                        type = n.NetworkInterfaceType.ToString(),
                        speed = FormatSpeed(n.Speed),
                        operationalStatus = n.OperationalStatus.ToString(),
                        bytesReceived = FormatBytes(stats.BytesReceived),
                        bytesSent = FormatBytes(stats.BytesSent),
                    };
                }).ToArray();

            // 进程 Top
            var processes = Process.GetProcesses()
                .Where(p =>
                {
                    try { return p.TotalProcessorTime.TotalMilliseconds > 0 || p.WorkingSet64 > 0; }
                    catch { return false; }
                })
                .OrderByDescending(p =>
                {
                    try { return p.WorkingSet64; }
                    catch { return 0L; }
                })
                .Take(15)
                .Select(p =>
                {
                    try
                    {
                        return new
                        {
                            name = p.ProcessName,
                            pid = p.Id,
                            memoryMB = (p.WorkingSet64 / 1024 / 1024).ToString("F0"),
                            cpuTime = p.TotalProcessorTime.TotalSeconds.ToString("F1") + "s",
                            threadCount = p.Threads.Count,
                            handleCount = p.HandleCount,
                            startTime = p.StartTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"),
                        };
                    }
                    catch { return null; }
                })
                .Where(p => p != null)
                .ToArray();

            return new
            {
                code = 0,
                data = new
                {
                    os = osInfo,
                    cpu = cpuInfo,
                    memory = memoryInfo,
                    drives,
                    nics,
                    processes,
                }
            };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while getting machine information: {0}", ex.Message) };
        }
    }

    /// <summary>获取 Top 进程列表</summary>
    /// <param name="sort">排序方式:memory/cpu</param>
    /// <param name="count">返回数量,默认 10</param>
    /// <returns>进程列表</returns>
    public Object GetProcessList(String sort = "memory", Int32 count = 10)
    {
        if (!CheckAuth()) return new { code = 401, message = L.T("Unauthorized") };

        try
        {
            var query = Process.GetProcesses()
                .Where(p =>
                {
                    try { return p.TotalProcessorTime.TotalMilliseconds > 0 || p.WorkingSet64 > 0; }
                    catch { return false; }
                });

            if (sort.EqualIgnoreCase("cpu"))
                query = query.OrderByDescending(p =>
                {
                    try { return p.TotalProcessorTime.TotalSeconds; }
                    catch { return 0.0; }
                });
            else
                query = query.OrderByDescending(p =>
                {
                    try { return p.WorkingSet64; }
                    catch { return 0L; }
                });

            var processes = query.Take(count).Select(p =>
            {
                try
                {
                    return new
                    {
                        name = p.ProcessName,
                        pid = p.Id,
                        memoryMB = (p.WorkingSet64 / 1024 / 1024).ToString("F0"),
                        cpuSeconds = p.TotalProcessorTime.TotalSeconds.ToString("F1"),
                        threadCount = p.Threads.Count,
                        handleCount = p.HandleCount,
                        startTime = p.StartTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"),
                    };
                }
                catch { return null; }
            }).Where(p => p != null).ToArray();

            return new { code = 0, data = new { processes, total = processes.Length } };
        }
        catch (Exception ex)
        {
            return new { code = 1, message = L.T("Error while getting process list: {0}", ex.Message) };
        }
    }
    #endregion

    #region 辅助方法
    private static String FormatBytes(Int64 bytes)
    {
        if (bytes < 1024) return $"{bytes} B";
        if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
        if (bytes < 1024 * 1024 * 1024) return $"{bytes / 1024.0 / 1024.0:F1} MB";
        return $"{bytes / 1024.0 / 1024.0 / 1024.0:F2} GB";
    }

    private static String FormatSpeed(Int64 bps)
    {
        if (bps < 1000) return $"{bps} bps";
        if (bps < 1000_000) return $"{bps / 1000.0:F1} Kbps";
        if (bps < 1000_000_000) return $"{bps / 1000_000.0:F1} Mbps";
        return $"{bps / 1000_000_000.0:F2} Gbps";
    }

    private static String FormatUptime(Int64 seconds)
    {
        var ts = TimeSpan.FromSeconds(seconds);
        if (ts.TotalDays >= 1) return L.T("{0}d {1}h {2}m", (Int32)ts.TotalDays, ts.Hours, ts.Minutes);
        if (ts.TotalHours >= 1) return L.T("{0}h {1}m", (Int32)ts.TotalHours, ts.Minutes);
        return L.T("{0}m {1}s", ts.Minutes, ts.Seconds);
    }

    #endregion
}
#endif