Иконка ресурса

FREE ServerMonitor : 0.1.0 0.1.0

Нет прав для скачивания
C#:
using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Text;

using Rust;
using Facepunch;
using UnityEngine;

using Newtonsoft.Json;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;

namespace Oxide.Plugins
{
    [Info("ServerMonitor", "DeltaDinizzz", "0.1.0")]
    public class ServerMonitor : CovalencePlugin
    {
        // ##StartModule - Global Variables & Config Properties
        private static ServerMonitor _instance;
        private FPSVisor ActiveVisor;
        private int MinimalFPS = 9999;

        // --- CONFIG ---
        private const string ApiUrl = "https://rustcenter.org/api/server-monitor/ingest";

        // --- ACCESS TOKENS ---
        // Three tokens are generated, one per console access level. The one the owner registers on
        // the website becomes ActiveToken; the other two are discarded. See "Access Tokens" below.
        private const string LevelFull = "full";
        private const string LevelStandard = "standard";
        private const string LevelRestricted = "restricted";
        private const float AwaitingRegistrationRetrySeconds = 15.0f;
        private DateTime _lastAwaitingLogUtc = DateTime.MinValue;

        private string ActiveToken => Config["ActiveToken"] as string ?? string.Empty;
        private string ActiveLevel => Config["ActiveLevel"] as string ?? string.Empty;
        private bool IsActivated => !string.IsNullOrEmpty(ActiveToken);

        // --- DYNAMIC INTERVALS (Provided by the Web API) ---
        private float _updateInterval = 2.0f;
        private string _monitorMode = "adaptive"; // adaptive | always_on
        private bool _shouldCollectFull = true;

        private Timer _loopTimer;
        private bool _isSleeping = false;
        private bool _hasUnloaded = false;
        private const float MinErrorRetrySeconds = 10.0f;
        private readonly Dictionary<string, double> _lastHookSecondsByPlugin = new Dictionary<string, double>();
        private readonly Dictionary<string, double> _maxHookSinceLastTickSecondsByPlugin = new Dictionary<string, double>();
        private DateTime _lastFullHookSampleUtc = DateTime.MinValue;

        // CPU sampling (delta between ticks)
        private DateTime _lastCpuTimeUtc = DateTime.MinValue;
        private TimeSpan _lastProcessorTime = TimeSpan.Zero;
        private DateTime _lastNetworkSampleUtc = DateTime.MinValue;
        private long _lastBytesReceived = -1;
        private long _lastBytesSent = -1;
        // ##EndModule - Global Variables & Config Properties

        // ##StartModule - Oxide Hooks
        protected override void LoadDefaultConfig()
        {
            GenerateAccessTokens();
        }

        private void Init()
        {
            _instance = this;
            _hasUnloaded = false;
            MigrateTokenConfig();
            PrintTokenBanner();
        }


        private void OnServerInitialized()
        {
            // Adiciona o script para monitorar o menor FPS
            ActiveVisor = Terrain.activeTerrain.gameObject.AddComponent<FPSVisor>();

            // Kickstart no loop
            SendServerStatsTick();
        }

        private void Unload()
        {
            _hasUnloaded = true;
            if (ActiveVisor != null)
            {
                UnityEngine.Object.Destroy(ActiveVisor);
            }
            if (_loopTimer != null)
            {
                _loopTimer.Destroy();
            }
        }
        // ##EndModule - Oxide Hooks

        // ##StartModule - Core Server Tick Logic
        private void SendServerStatsTick()
        {
            if (_hasUnloaded)
                return;

            // ── HEARTBEAT MODE ──
            // When sleeping in adaptive mode, send only a lightweight ping
            // so the API can tell us when to wake up. No data collection.
            // Until one of the tokens is registered on the website, only heartbeats are sent.
            if (!IsActivated || (_isSleeping && _monitorMode != "always_on"))
            {
                // Reset baselines so the first "full" tick after waking
                // doesn't retroactively cover the entire sleep period.
                _lastHookSecondsByPlugin.Clear();
                _maxHookSinceLastTickSecondsByPlugin.Clear();
                _lastFullHookSampleUtc = DateTime.MinValue;
                _lastCpuTimeUtc = DateTime.MinValue;
                _lastNetworkSampleUtc = DateTime.MinValue;
                _lastBytesReceived = -1;
                _lastBytesSent = -1;

                // Before registration the website only gets the hashes of the 3 tokens (never the
                // tokens), so it can only activate the one the owner actually registered there
                object heartbeat = IsActivated
                    ? (object)new { method = "heartbeat", token = ActiveToken }
                    : new { method = "heartbeat", tokenHashes = PendingTokenHashes() };
                string heartbeatJson = JsonConvert.SerializeObject(heartbeat);
                var hbHeaders = new Dictionary<string, string> { { "Content-Type", "application/json" } };

                webrequest.Enqueue(ApiUrl, heartbeatJson, (code, response) =>
                {
                    if (_hasUnloaded) return;
                    if (code == 200 || code == 204)
                    {
                        try
                        {
                            var jr = JsonConvert.DeserializeObject<Dictionary<string, object>>(response);
                            if (jr != null)
                            {
                                if (!IsActivated && jr.ContainsKey("activate") && jr["activate"] != null)
                                    TryActivateToken(jr["activate"].ToString());

                                if (jr.ContainsKey("updateInterval"))
                                    float.TryParse(jr["updateInterval"].ToString(), out _updateInterval);
                                if (jr.ContainsKey("monitorMode"))
                                    _monitorMode = jr["monitorMode"].ToString();
                                if (jr.ContainsKey("shouldCollectFull"))
                                {
                                    bool parsed;
                                    if (bool.TryParse(jr["shouldCollectFull"].ToString(), out parsed))
                                        _shouldCollectFull = parsed;
                                }
                                if (jr.ContainsKey("state"))
                                {
                                    string st = jr["state"].ToString();
                                    if (st == "active")
                                    {
                                        Puts($"[ServerMonitor] Dashboard aberta/detectada. Saindo do Sleep Mode! Atualizando a cada {_updateInterval}s...");
                                        _isSleeping = false;
                                    }
                                }
                                if (_monitorMode == "always_on")
                                    _isSleeping = false;
                            }
                        }
                        catch { }
                    }
                    else if (!IsActivated && (DateTime.UtcNow - _lastAwaitingLogUtc).TotalMinutes >= 5)
                    {
                        // The website answers 403 until one of the tokens is registered there
                        _lastAwaitingLogUtc = DateTime.UtcNow;
                        Puts("[ServerMonitor] Waiting for one of the 3 tokens to be registered at rustcenter.org (run 'servermonitor.tokens' in the server console to see them).");
                    }
                    if (!_hasUnloaded)
                        _loopTimer = timer.Once(IsActivated ? _updateInterval : AwaitingRegistrationRetrySeconds, SendServerStatsTick);
                }, this, RequestMethod.POST, hbHeaders);
                return;
            }

            // ── FULL DATA COLLECTION ──
            var nowUtc = DateTime.UtcNow;
            var connectedPlayers = players.Connected.ToArray();
            var pluginItems = new List<object>();
            double? sampleWindowMs = null;

            if (_lastFullHookSampleUtc != DateTime.MinValue && _lastFullHookSampleUtc < nowUtc)
                sampleWindowMs = Math.Max(0.0, (nowUtc - _lastFullHookSampleUtc).TotalMilliseconds);
            _lastFullHookSampleUtc = nowUtc;

            // Coleta plugins
            var listPlugins = plugins.PluginManager.GetPlugins().ToArray();
            var seenPluginNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            for (var i = 0; i < listPlugins.Length; i++)
            {
                var pluginName = listPlugins[i].Name;
                seenPluginNames.Add(pluginName);
                var hookSeconds = listPlugins[i].TotalHookTime.TotalSeconds;
                double previousHookSeconds;
                var hasPreviousHookSample = _lastHookSecondsByPlugin.TryGetValue(pluginName, out previousHookSeconds);
                var hookCounterReset = hasPreviousHookSample && hookSeconds < previousHookSeconds;
                var deltaHookSeconds = (!hasPreviousHookSample || hookCounterReset)
                    ? 0.0
                    : Math.Max(0.0, hookSeconds - previousHookSeconds);

                double previousMaxSinceLastTick;
                var hasPreviousMax = _maxHookSinceLastTickSecondsByPlugin.TryGetValue(pluginName, out previousMaxSinceLastTick);
                var nextMaxSinceLastTick = (!hasPreviousHookSample || hookCounterReset)
                    ? 0.0
                    : Math.Max(hasPreviousMax ? previousMaxSinceLastTick : 0.0, deltaHookSeconds);
                _maxHookSinceLastTickSecondsByPlugin[pluginName] = nextMaxSinceLastTick;
                _lastHookSecondsByPlugin[pluginName] = hookSeconds;

                pluginItems.Add(new
                {
                    name = pluginName,
                    version = listPlugins[i].Version.ToString(),
                    author = listPlugins[i].Author,
                    hash = pluginName.GetHashCode(),
                    time = deltaHookSeconds,
                    hookTimeMs = deltaHookSeconds * 1000.0,
                    hookMaxSinceLastTickMs = nextMaxSinceLastTick * 1000.0
                });

                _maxHookSinceLastTickSecondsByPlugin[pluginName] = 0.0;
            }

            PruneStaleHookSamples(seenPluginNames);

            int currentMinFps = MinimalFPS;
            MinimalFPS = 9999; // Reseta depois de ler

            // CPU, RAM e Disk (uso real do processo / disco)
            double cpuPercent = 0;
            long ramMb = 0;
            long ramTotalMb = 0;
            double diskUsedPercent = -1;
            double? averagePingMs = null;
            double? networkInKBps = null;
            double? networkOutKBps = null;

            try
            {
                ramMb = GetProcessRamMb();
                ramTotalMb = GetTotalSystemRamMb();

                // CPU: % desde o último tick (TotalProcessorTime delta / wall-clock delta)
                var process = Process.GetCurrentProcess();
                process.Refresh();
                var currentProcessorTime = process.TotalProcessorTime;
                if (_lastCpuTimeUtc != DateTime.MinValue && _lastCpuTimeUtc < nowUtc)
                {
                    var wallSeconds = (nowUtc - _lastCpuTimeUtc).TotalSeconds;
                    var cpuSeconds = (currentProcessorTime - _lastProcessorTime).TotalSeconds;
                    if (wallSeconds > 0 && cpuSeconds >= 0)
                        cpuPercent = (cpuSeconds / wallSeconds) * 100.0; // Pode passar 100 em multi-core
                }
                _lastCpuTimeUtc = nowUtc;
                _lastProcessorTime = currentProcessorTime;

                // Disk: % usado no drive onde o servidor está a correr
                try
                {
                    var baseDir = AppDomain.CurrentDomain.BaseDirectory;
                    if (!string.IsNullOrEmpty(baseDir))
                    {
                        var root = Path.GetPathRoot(baseDir);
                        if (!string.IsNullOrEmpty(root))
                        {
                            var drive = new DriveInfo(root);
                            if (drive.IsReady)
                            {
                                long total = drive.TotalSize;
                                long free = drive.AvailableFreeSpace;
                                if (total > 0)
                                    diskUsedPercent = (double)(total - free) / total * 100.0;
                            }
                        }
                    }
                }
                catch { diskUsedPercent = -1; }
            }
            catch { /* Process/Refresh pode falhar em alguns ambientes */ }

            averagePingMs = GetAveragePlayerPingMs(connectedPlayers);
            SampleNetworkIo(nowUtc, out networkInKBps, out networkOutKBps);

            // Monta o Payload principal
            var payload = new
            {
                method = "tick_server",
                serverName = server.Name,
                serverIp = server.Address + ":" + server.Port,
                token = ActiveToken,
                fps = Performance.current.frameRate,
                minfps = currentMinFps,
                ent = BaseNetworkable.serverEntities.Count,
                online = connectedPlayers.Length,
                maxPlayers = server.MaxPlayers,
                SleepPlayer = BasePlayer.sleepingPlayerList.Count,
                JoiningPlayer = ServerMgr.Instance.connectionQueue.Joining,
                QueuedPlayer = ServerMgr.Instance.connectionQueue.Queued,
                uptime = (int)UnityEngine.Time.realtimeSinceStartup,
                version = server.Version,
                map = ConVar.Server.level,
                sampleWindowMs = sampleWindowMs.HasValue ? Math.Round(sampleWindowMs.Value) : (double?)null,
                listPlugins = pluginItems,
                isSleeping = _isSleeping,
                cpu = Math.Round(cpuPercent, 2),
                ramMb = ramMb,
                ramTotalMb = ramTotalMb > 0 ? ramTotalMb : (long?)null,
                ping = averagePingMs.HasValue ? Math.Round(averagePingMs.Value, 2) : (double?)null,
                diskUsedPercent = diskUsedPercent >= 0 ? Math.Round(diskUsedPercent, 2) : (double?)null,
                networkInKBps = networkInKBps.HasValue ? Math.Round(networkInKBps.Value, 2) : (double?)null,
                networkOutKBps = networkOutKBps.HasValue ? Math.Round(networkOutKBps.Value, 2) : (double?)null
            };

            string jsonPayload = JsonConvert.SerializeObject(payload);
            
            // Cabeçalho básico
            var headers = new Dictionary<string, string>
            {
                { "Content-Type", "application/json" }
            };

            // Envia o HTTP POST via WebRequest
            webrequest.Enqueue(ApiUrl, jsonPayload, (code, response) =>
            {
                if (_hasUnloaded)
                    return;

                float nextDelay = _updateInterval;

                if (code == 200 || code == 204)
                {
                    try
                    {
                        var jsonResponse = JsonConvert.DeserializeObject<Dictionary<string, object>>(response);
                        
                        if (jsonResponse != null)
                        {
                            if (jsonResponse.ContainsKey("updateInterval"))
                            {
                                float.TryParse(jsonResponse["updateInterval"].ToString(), out _updateInterval);
                                nextDelay = _updateInterval;
                            }

                            if (jsonResponse.ContainsKey("monitorMode"))
                            {
                                _monitorMode = jsonResponse["monitorMode"].ToString();
                            }
                            if (jsonResponse.ContainsKey("shouldCollectFull"))
                            {
                                bool parsedCollectFull;
                                if (bool.TryParse(jsonResponse["shouldCollectFull"].ToString(), out parsedCollectFull))
                                {
                                    _shouldCollectFull = parsedCollectFull;
                                }
                            }

                            if (jsonResponse.ContainsKey("state"))
                            {
                                string state = jsonResponse["state"].ToString();
                                if (state == "sleep")
                                {
                                    if (!_isSleeping)
                                        Puts($"[ServerMonitor] Ninguém na dashboard. Entrando em Sleep Mode (heartbeat a cada {_updateInterval}s)...");
                                    
                                    _isSleeping = true;
                                }
                                else if (state == "active")
                                {
                                    if (_isSleeping)
                                        Puts($"[ServerMonitor] Dashboard aberta/detectada. Saindo do Sleep Mode! Atualizando a cada {_updateInterval}s...");
                                    
                                    _isSleeping = false;
                                }
                                else if (state == "degraded")
                                {
                                    _isSleeping = false;
                                    nextDelay = Math.Max(_updateInterval, MinErrorRetrySeconds);
                                }
                            }

                            if (_monitorMode == "always_on")
                            {
                                _isSleeping = false;
                            }
                            // C# Native RCON-less execution 
                            if (jsonResponse.ContainsKey("pendingCommands"))
                            {
                                var commandsList = jsonResponse["pendingCommands"] as Newtonsoft.Json.Linq.JArray;
                                if (commandsList != null)
                                {
                                    foreach (var command in commandsList)
                                    {
                                        string cmdRaw = command.ToString();
                                        if (string.IsNullOrEmpty(cmdRaw))
                                            continue;

                                        // Enforced here, not only on the website: a website problem can
                                        // never go above the access level of the registered token
                                        string blockedReason;
                                        if (!IsCommandAllowed(ActiveLevel, cmdRaw, out blockedReason))
                                        {
                                            PrintWarning($"[ServerMonitor] Blocked remote command ({LevelLabel(ActiveLevel)} access): {cmdRaw} ({blockedReason})");
                                            continue;
                                        }
                                        Puts($"[ServerMonitor] Executing remote command: {cmdRaw}");
                                        ConsoleSystem.Run(ConsoleSystem.Option.Server, cmdRaw);
                                    }
                                }
                            }
                        }
                    }
                    catch { }
                }
                else
                {
                    // Em erro transitório de rede/proxy, aplica backoff curto.
                    float retryDelay = Math.Max(_updateInterval * 2.0f, MinErrorRetrySeconds);
                    Puts($"[ServerMonitor] HTTP Error {code} - Response: {(response ?? "null")}. Retrying in {retryDelay:0.0}s...");
                    _isSleeping = false;
                    nextDelay = retryDelay;
                }

                // Agenda a próxima execução
                if (!_hasUnloaded)
                    _loopTimer = timer.Once(nextDelay, SendServerStatsTick);

            }, this, RequestMethod.POST, headers);
        }
        // ##EndModule - Core Server Tick Logic

        private long GetProcessRamMb()
        {
            try
            {
                var process = Process.GetCurrentProcess();
                process.Refresh();
                long workingSetBytes = process.WorkingSet64;
                if (workingSetBytes > 0)
                    return workingSetBytes / (1024L * 1024L);
            }
            catch { }

            long procStatusKb;
            if (TryReadProcFileValueKb("/proc/self/status", "VmRSS:", out procStatusKb) && procStatusKb > 0)
                return procStatusKb / 1024L;

            try
            {
                var process = Process.GetCurrentProcess();
                string processStatusPath = $"/proc/{process.Id}/status";
                if (TryReadProcFileValueKb(processStatusPath, "VmRSS:", out procStatusKb) && procStatusKb > 0)
                    return procStatusKb / 1024L;
            }
            catch { }

            long residentPages;
            if (TryReadProcStatmResidentPages(out residentPages) && residentPages > 0)
            {
                long pageSizeBytes = Environment.SystemPageSize > 0 ? Environment.SystemPageSize : 4096;
                long residentBytes = residentPages * pageSizeBytes;
                if (residentBytes > 0)
                    return residentBytes / (1024L * 1024L);
            }

            try
            {
                long managedBytes = System.GC.GetTotalMemory(false);
                if (managedBytes > 0)
                    return managedBytes / (1024L * 1024L);
            }
            catch { }

            return 0;
        }

        private long GetTotalSystemRamMb()
        {
            long memTotalKb;
            if (TryReadProcFileValueKb("/proc/meminfo", "MemTotal:", out memTotalKb) && memTotalKb > 0)
                return memTotalKb / 1024L;

            try
            {
                var unitySystemMemoryMb = SystemInfo.systemMemorySize;
                if (unitySystemMemoryMb > 0)
                    return unitySystemMemoryMb;
            }
            catch { }

            return 0;
        }

        private bool TryReadProcFileValueKb(string path, string key, out long valueKb)
        {
            valueKb = 0;

            try
            {
                if (!File.Exists(path))
                    return false;

                var lines = File.ReadAllLines(path);
                for (int i = 0; i < lines.Length; i++)
                {
                    var line = lines[i];
                    if (!line.StartsWith(key, StringComparison.OrdinalIgnoreCase))
                        continue;

                    var digits = new string(line.Where(char.IsDigit).ToArray());
                    long parsed;
                    if (long.TryParse(digits, out parsed) && parsed >= 0)
                    {
                        valueKb = parsed;
                        return true;
                    }
                }
            }
            catch { }

            return false;
        }

        private bool TryReadProcStatmResidentPages(out long residentPages)
        {
            residentPages = 0;

            try
            {
                const string statmPath = "/proc/self/statm";
                if (!File.Exists(statmPath))
                    return false;

                var content = File.ReadAllText(statmPath).Trim();
                if (string.IsNullOrEmpty(content))
                    return false;

                var parts = content.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length < 2)
                    return false;

                long parsedPages;
                if (long.TryParse(parts[1], out parsedPages) && parsedPages > 0)
                {
                    residentPages = parsedPages;
                    return true;
                }
            }
            catch { }

            return false;
        }

        private double? GetAveragePlayerPingMs(IPlayer[] connectedPlayers)
        {
            try
            {
                if (connectedPlayers == null || connectedPlayers.Length == 0)
                    return null;

                double totalPing = 0;
                int samples = 0;
                for (int i = 0; i < connectedPlayers.Length; i++)
                {
                    var ping = connectedPlayers[i].Ping;
                    if (ping < 0)
                        continue;

                    totalPing += ping;
                    samples++;
                }

                if (samples == 0)
                    return null;

                return totalPing / samples;
            }
            catch { }

            return null;
        }

        private void SampleNetworkIo(DateTime nowUtc, out double? networkInKBps, out double? networkOutKBps)
        {
            networkInKBps = null;
            networkOutKBps = null;

            long bytesReceived;
            long bytesSent;
            if (!TryGetNetworkTotals(out bytesReceived, out bytesSent))
                return;

            if (_lastNetworkSampleUtc == DateTime.MinValue || _lastBytesReceived < 0 || _lastBytesSent < 0)
            {
                networkInKBps = 0;
                networkOutKBps = 0;
            }
            else if (_lastNetworkSampleUtc < nowUtc)
            {
                var wallSeconds = (nowUtc - _lastNetworkSampleUtc).TotalSeconds;
                var deltaReceived = bytesReceived - _lastBytesReceived;
                var deltaSent = bytesSent - _lastBytesSent;
                if (wallSeconds > 0 && deltaReceived >= 0 && deltaSent >= 0)
                {
                    networkInKBps = (deltaReceived / 1024.0) / wallSeconds;
                    networkOutKBps = (deltaSent / 1024.0) / wallSeconds;
                }
            }

            _lastNetworkSampleUtc = nowUtc;
            _lastBytesReceived = bytesReceived;
            _lastBytesSent = bytesSent;
        }

        private bool TryGetNetworkTotals(out long bytesReceived, out long bytesSent)
        {
            bytesReceived = 0;
            bytesSent = 0;

            if (TryGetNetworkTotalsDotNet(out bytesReceived, out bytesSent))
                return true;

            if (TryGetNetworkTotalsProc(out bytesReceived, out bytesSent))
                return true;

            return TryGetNetworkTotalsNetstat(out bytesReceived, out bytesSent);
        }

        private bool TryGetNetworkTotalsDotNet(out long bytesReceived, out long bytesSent)
        {
            bytesReceived = 0;
            bytesSent = 0;

            try
            {
                var interfaces = NetworkInterface.GetAllNetworkInterfaces();
                for (int i = 0; i < interfaces.Length; i++)
                {
                    var networkInterface = interfaces[i];
                    if (networkInterface == null)
                        continue;
                    if (networkInterface.OperationalStatus != OperationalStatus.Up)
                        continue;
                    if (networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
                        continue;

                    var stats = networkInterface.GetIPStatistics();
                    if (stats == null)
                        continue;

                    bytesReceived += stats.BytesReceived;
                    bytesSent += stats.BytesSent;
                }

                return true;
            }
            catch { }

            return false;
        }

        private bool TryGetNetworkTotalsNetstat(out long bytesReceived, out long bytesSent)
        {
            bytesReceived = 0;
            bytesSent = 0;

            try
            {
                var startInfo = new ProcessStartInfo
                {
                    FileName = "netstat",
                    Arguments = "-e",
                    CreateNoWindow = true,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                };

                using (var process = Process.Start(startInfo))
                {
                    if (process == null)
                        return false;

                    var output = process.StandardOutput.ReadToEnd();
                    process.WaitForExit(2000);
                    if (string.IsNullOrWhiteSpace(output))
                        return false;

                    var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
                    for (int i = 0; i < lines.Length; i++)
                    {
                        var line = lines[i];
                        var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
                        if (parts.Length < 2)
                            continue;

                        long received;
                        long sent;
                        if (!long.TryParse(parts[parts.Length - 2], out received) || !long.TryParse(parts[parts.Length - 1], out sent))
                            continue;

                        if (received < 0 || sent < 0)
                            continue;

                        bytesReceived = received;
                        bytesSent = sent;
                        return true;
                    }
                }
            }
            catch { }

            return false;
        }

        private bool TryGetNetworkTotalsProc(out long bytesReceived, out long bytesSent)
        {
            bytesReceived = 0;
            bytesSent = 0;
            bool parsedAnyInterface = false;

            try
            {
                const string procNetDevPath = "/proc/net/dev";
                if (!File.Exists(procNetDevPath))
                    return false;

                var lines = File.ReadAllLines(procNetDevPath);
                for (int i = 2; i < lines.Length; i++)
                {
                    var line = lines[i];
                    if (string.IsNullOrWhiteSpace(line) || line.IndexOf(':') < 0)
                        continue;

                    var split = line.Split(new[] { ':' }, 2);
                    if (split.Length != 2)
                        continue;

                    var interfaceName = split[0].Trim();
                    if (interfaceName == "lo")
                        continue;

                    var values = split[1].Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
                    if (values.Length < 16)
                        continue;

                    long received;
                    long sent;
                    if (!long.TryParse(values[0], out received) || !long.TryParse(values[8], out sent))
                        continue;

                    parsedAnyInterface = true;
                    bytesReceived += received;
                    bytesSent += sent;
                }

                return parsedAnyInterface;
            }
            catch { }

            return false;
        }

        private void PruneStaleHookSamples(HashSet<string> seenPluginNames)
        {
            if (seenPluginNames == null || seenPluginNames.Count == 0)
            {
                _lastHookSecondsByPlugin.Clear();
                _maxHookSinceLastTickSecondsByPlugin.Clear();
                return;
            }

            var lastKeys = _lastHookSecondsByPlugin.Keys.ToArray();
            for (int i = 0; i < lastKeys.Length; i++)
            {
                if (!seenPluginNames.Contains(lastKeys[i]))
                    _lastHookSecondsByPlugin.Remove(lastKeys[i]);
            }

            var maxKeys = _maxHookSinceLastTickSecondsByPlugin.Keys.ToArray();
            for (int i = 0; i < maxKeys.Length; i++)
            {
                if (!seenPluginNames.Contains(maxKeys[i]))
                    _maxHookSinceLastTickSecondsByPlugin.Remove(maxKeys[i]);
            }
        }

        // ##StartModule - Access Tokens
        // Three tokens, one per console access level. The owner registers ONE at rustcenter.org; the
        // website then answers "activate" with that token (it never learns the other two, which are
        // sent only as SHA-256 hashes), and this plugin keeps it and discards the others. Remote
        // commands are checked against the level here, so the website can't go above it.
        private void GenerateAccessTokens()
        {
            Config["TokenFull"] = "SM-FULL-" + Guid.NewGuid().ToString("N");
            Config["TokenStandard"] = "SM-STD-" + Guid.NewGuid().ToString("N");
            Config["TokenRestricted"] = "SM-LTD-" + Guid.NewGuid().ToString("N");
            Config["ActiveToken"] = string.Empty;
            Config["ActiveLevel"] = string.Empty;
            SaveConfig();
        }

        private void MigrateTokenConfig()
        {
            // Plugin <= 0.0.4 had a single token, which always had full access: keep that
            // registration working as the active FULL token (servermonitor.newtokens changes it)
            var legacyToken = Config["ServerToken"] as string;
            if (!IsActivated && !string.IsNullOrEmpty(legacyToken))
            {
                Config["ActiveToken"] = legacyToken;
                Config["ActiveLevel"] = LevelFull;
                Config["ServerToken"] = string.Empty;
                SaveConfig();
                return;
            }

            bool hasPendingTokens = !string.IsNullOrEmpty(Config["TokenFull"] as string)
                && !string.IsNullOrEmpty(Config["TokenStandard"] as string)
                && !string.IsNullOrEmpty(Config["TokenRestricted"] as string);
            if (!IsActivated && !hasPendingTokens)
                GenerateAccessTokens();
        }

        private void PrintTokenBanner()
        {
            Puts("======================================================");
            Puts($" RustCenter Server Monitor {Version}");
            if (IsActivated)
            {
                Puts($" Linked to rustcenter.org with {LevelLabel(ActiveLevel)} console access (token {MaskToken(ActiveToken)}).");
                Puts(" Run 'servermonitor.tokens' in the server console for details.");
            }
            else
            {
                Puts(" Not linked yet. Register ONE of these tokens at rustcenter.org (Server Monitor):");
                Puts($"   FULL        {Config["TokenFull"]}   everything");
                Puts($"   STANDARD    {Config["TokenStandard"]}   everything except ownerid/moderatorid, permission changes and quit");
                Puts($"   RESTRICTED  {Config["TokenRestricted"]}   only say, server.save and read-only commands");
                Puts(" The token you register sets what the website console can do; the other two are discarded.");
            }
            Puts("======================================================");
        }

        private string[] PendingTokenHashes()
        {
            return new[] { "TokenFull", "TokenStandard", "TokenRestricted" }
                .Select(key => Config[key] as string)
                .Where(token => !string.IsNullOrEmpty(token))
                .Select(Sha256Hex)
                .ToArray();
        }

        private void TryActivateToken(string token)
        {
            string level = null;
            if (!string.IsNullOrEmpty(token))
            {
                if (token == Config["TokenFull"] as string) level = LevelFull;
                else if (token == Config["TokenStandard"] as string) level = LevelStandard;
                else if (token == Config["TokenRestricted"] as string) level = LevelRestricted;
            }
            if (level == null)
            {
                PrintWarning("[ServerMonitor] The website asked to activate a token this server did not generate. Ignored.");
                return;
            }

            Config["ActiveToken"] = token;
            Config["ActiveLevel"] = level;
            Config["TokenFull"] = string.Empty;
            Config["TokenStandard"] = string.Empty;
            Config["TokenRestricted"] = string.Empty;
            SaveConfig();
            Puts($"[ServerMonitor] Linked to rustcenter.org with {LevelLabel(level)} console access. The other 2 tokens were discarded.");
        }

        [Command("servermonitor.tokens")]
        private void CmdTokens(IPlayer player, string command, string[] args)
        {
            if (!player.IsServer)
            {
                player.Reply("This command can only be run from the server console.");
                return;
            }
            if (IsActivated)
            {
                Puts($"[ServerMonitor] Active token ({LevelLabel(ActiveLevel)} access): {ActiveToken}");
                Puts("[ServerMonitor] To use another access level, run 'servermonitor.newtokens' and register one of the new tokens on the website.");
            }
            else
            {
                PrintTokenBanner();
            }
        }

        [Command("servermonitor.newtokens")]
        private void CmdNewTokens(IPlayer player, string command, string[] args)
        {
            if (!player.IsServer)
            {
                player.Reply("This command can only be run from the server console.");
                return;
            }
            GenerateAccessTokens();
            Puts("[ServerMonitor] New tokens generated. The website entry linked to the old token stops receiving data; register one of the new tokens there.");
            PrintTokenBanner();
        }

        private static string LevelLabel(string level)
        {
            if (level == LevelFull) return "FULL";
            if (level == LevelStandard) return "STANDARD";
            if (level == LevelRestricted) return "RESTRICTED";
            return "NO";
        }

        private static string MaskToken(string token)
        {
            int dash = token.LastIndexOf('-');
            return dash >= 0 && token.Length > dash + 6 ? token.Substring(0, dash + 7) + "…" : "…";
        }

        private static string Sha256Hex(string value)
        {
            using (var sha = SHA256.Create())
            {
                var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(value));
                var hex = new StringBuilder(bytes.Length * 2);
                foreach (var b in bytes)
                    hex.Append(b.ToString("x2"));
                return hex.ToString();
            }
        }

        // Same rules as the website (lib/server-monitor/commandPolicy.ts)
        private static readonly HashSet<string> StandardBlockedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "ownerid", "moderatorid", "removeowner", "removemoderator", "quit"
        };
        private static readonly HashSet<string> PermissionPrefixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "c", "carbon", "o", "oxide", "umod"
        };
        private static readonly HashSet<string> PermissionVerbs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "grant", "revoke", "group", "usergroup"
        };
        private static readonly HashSet<string> RestrictedAllowedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "say", "server.save",
            "status", "players", "playerlist", "serverinfo", "fps", "version",
            "banlist", "banlistex", "listid",
            "c.plugins", "o.plugins", "oxide.plugins", "c.version", "o.version", "oxide.version"
        };

        /// <summary>Command name as the console resolves it: first word, lowercase, no quotes, no "global.".</summary>
        private static string CommandName(string raw)
        {
            var trimmed = raw.Trim();
            int end = 0;
            while (end < trimmed.Length && !char.IsWhiteSpace(trimmed[end]))
                end++;
            var name = trimmed.Substring(0, end).Trim('"', '\'').ToLowerInvariant();
            return name.StartsWith("global.") ? name.Substring("global.".Length) : name;
        }

        private static bool IsPermissionCommand(string name)
        {
            int dot = name.IndexOf('.');
            return dot > 0
                && PermissionPrefixes.Contains(name.Substring(0, dot))
                && PermissionVerbs.Contains(name.Substring(dot + 1));
        }

        private static bool IsCommandAllowed(string level, string raw, out string reason)
        {
            reason = null;
            if (level == LevelFull)
                return true;

            // One command per line: ";" or control characters could smuggle a second one in
            if (raw.IndexOf(';') >= 0 || raw.Any(char.IsControl))
            {
                reason = "one command at a time, without ';' or line breaks";
                return false;
            }

            var name = CommandName(raw);
            if (level == LevelStandard)
            {
                if (StandardBlockedCommands.Contains(name) || IsPermissionCommand(name))
                {
                    reason = "needs the FULL access token";
                    return false;
                }
                return true;
            }
            if (level == LevelRestricted)
            {
                if (RestrictedAllowedCommands.Contains(name))
                    return true;
                reason = "RESTRICTED access only allows say, server.save and read-only commands";
                return false;
            }

            reason = "no access level (server not linked)";
            return false;
        }
        // ##EndModule - Access Tokens

        // ##StartModule - FPSVisor Component
        public class FPSVisor : MonoBehaviour
        {
            private void Update()
            {
                if (_instance.MinimalFPS > (int)global::Performance.current.frameRate)
                {
                    _instance.MinimalFPS = (int)global::Performance.current.frameRate;
                }
            }
        }
        // ##EndModule - FPSVisor Component
    }
}

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху