XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.ServerInteractive

[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using XFEExtension.NetCore.CyberComm;
using XFEExtension.NetCore.ServerInteractive.Exceptions;

namespace XFEExtension.NetCore.ServerInteractive.Utilities.Server;

/// <summary>
/// 承载共享监听端点的服务器核心,并按 scheme、host、port 和主入口点分发请求。
/// </summary>
internal sealed class XFEServerCoreHost
{
    private readonly IReadOnlyList<XFEServerCore> _serverCores;
    private readonly Dictionary<XFEServerCore, HashSet<BindingEndpoint>> _bindingEndpoints;
    private readonly SemaphoreSlim _startGate = new(1, 1);

    public string ServerCoreNames => string.Join(", ", _serverCores.Select(core => core.ServerCoreName));
    public IReadOnlyList<string> BindingIPAddressList { get; }
    public CyberCommServer CyberCommServer { get; }

    private XFEServerCoreHost(IReadOnlyList<XFEServerCore> serverCores)
    {
        if (serverCores.Count == 0)
            throw new ArgumentException("核心服务器主机至少需要包含一个核心服务器", nameof(serverCores));

        _serverCores = serverCores;
        foreach (var serverCore in serverCores)
            serverCore.MainEntryPoint = XFEServerCore.NormalizeMainEntryPoint(serverCore.MainEntryPoint);
        _bindingEndpoints = serverCores.ToDictionary(
            core => core,
            core => core.BindingIPAddressList.Select(BindingEndpoint.Parse).ToHashSet());
        ValidateServerCoreIdentities();

        BindingIPAddressList = serverCores
            .SelectMany(core => core.BindingIPAddressList)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        if (serverCores.Any(core => core.TransportLimits != serverCores[0].TransportLimits || core.TlsOptions != serverCores[0].TlsOptions))
            throw new XFEServerBuilderException("共享同一监听器的核心服务器必须使用相同的 CyberComm 限制和 TLS 配置");
        CyberCommServer = new CyberCommServer(new CyberCommServerOptions
        {
            ListenUrls = BindingIPAddressList,
            Limits = serverCores[0].TransportLimits,
            Tls = serverCores[0].TlsOptions,
            AssembleWebSocketMessages = true,
            ReadHttpRequestBody = true
        })
        {
            HttpRequestHandler = ProcessHttpRequestAsync,
            StartedHandler = CyberCommServerStartedAsync,
            WebSocketConnectedHandler = CyberCommServerClientConnectedAsync,
            WebSocketMessageHandler = CyberCommServerMessageReceivedAsync,
            WebSocketClosedHandler = CyberCommServerConnectionClosedAsync
        };

        foreach (var serverCore in serverCores)
        {
            serverCore.CyberCommServer = CyberCommServer;
            serverCore.ServerCoreHost = this;
        }
    }

    public static IReadOnlyList<XFEServerCoreHost> Configure(IEnumerable<XFEServerCore> serverCores)
    {
        var coreList = serverCores.Distinct().ToList();
        if (coreList.Count == 0) return [];
        if (coreList.All(core => core.ServerCoreHost is not null))
            return coreList.Select(core => core.ServerCoreHost!).Distinct().ToArray();
        if (coreList.Any(core => core.ServerCoreHost is not null))
            throw new InvalidOperationException("不能将已经分配监听主机的核心服务器与未分配的核心服务器混合配置");

        // 同 scheme + port 使用同一个底层 Socket;host 和基础路径在请求分发时继续区分。
        var listenerKeys = coreList.ToDictionary(
            core => core,
            core => core.BindingIPAddressList.Select(BindingEndpoint.Parse).Select(endpoint => endpoint.ListenerKey).ToHashSet());
        var remaining = new List<XFEServerCore>(coreList);
        var hosts = new List<XFEServerCoreHost>();
        while (remaining.Count > 0)
        {
            var first = remaining[0];
            remaining.RemoveAt(0);
            var group = new List<XFEServerCore> { first };
            var groupKeys = new HashSet<ListenerKey>(listenerKeys[first]);
            var expanded = true;
            while (expanded)
            {
                expanded = false;
                foreach (var candidate in remaining.ToArray())
                {
                    if (!groupKeys.Overlaps(listenerKeys[candidate])) continue;
                    remaining.Remove(candidate);
                    group.Add(candidate);
                    groupKeys.UnionWith(listenerKeys[candidate]);
                    expanded = true;
                }
            }
            hosts.Add(new XFEServerCoreHost(group));
        }
        return hosts;
    }

    public async Task StartServerCore(CancellationToken cancellationToken = default)
    {
        await _startGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await CyberCommServer.StartAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _startGate.Release();
        }
    }

    public Task RunAsync(CancellationToken cancellationToken = default) => CyberCommServer.RunAsync(cancellationToken);
    public async Task StopAsync(CancellationToken cancellationToken = default)
    {
        await CyberCommServer.StopAsync(cancellationToken).ConfigureAwait(false);
        var disposals = _serverCores
            .SelectMany(core => core.ServerCoreServiceList)
            .OfType<IAsyncDisposable>()
            .Distinct()
            .Select(service => service.DisposeAsync().AsTask());
        await Task.WhenAll(disposals).ConfigureAwait(false);
    }

    private async ValueTask ProcessHttpRequestAsync(CyberCommHttpRequestContext context, CancellationToken cancellationToken)
    {
        var args = CyberCommRequestEventArgs.FromContext(context);
        var serverCore = FindServerCore(context.RequestUri, context.LocalPort);
        if (serverCore is null)
        {
            var entryPoint = XFEServerCore.GetRequestSegments(context.RequestUri).FirstOrDefault() ?? "<空>";
            await args.ReplyAndClose(
                $"未找到与 {context.RequestUri.Scheme}://{context.RequestUri.Authority} 和主入口点 '{entryPoint}' 匹配的核心服务器",
                HttpStatusCode.NotFound).ConfigureAwait(false);
            return;
        }

        try
        {
            serverCore.NotifyRequestReceived(this, args);
            await serverCore.ProcessRequest(args, cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ERROR]分发核心服务器请求时发生异常:{ex.Message}");
            if (!context.Response.IsCompleted)
                await args.ReplyAndClose("服务器内部异常", HttpStatusCode.InternalServerError).ConfigureAwait(false);
        }
    }

    private void ValidateServerCoreIdentities()
    {
        for (var leftIndex = 0; leftIndex < _serverCores.Count; leftIndex++)
        {
            var left = _serverCores[leftIndex];
            for (var rightIndex = leftIndex + 1; rightIndex < _serverCores.Count; rightIndex++)
            {
                var right = _serverCores[rightIndex];
                if (!left.MainEntryPoint.Equals(right.MainEntryPoint, StringComparison.Ordinal)) continue;
                if (!_bindingEndpoints[left].Any(leftEndpoint =>
                        _bindingEndpoints[right].Any(rightEndpoint => leftEndpoint.Overlaps(rightEndpoint)))) continue;
                var entryPoint = left.MainEntryPoint.Length == 0 ? "<空>" : left.MainEntryPoint;
                throw new XFEServerBuilderException(
                    $"主入口点 '{entryPoint}' 的监听地址在核心服务器 '{left.ServerCoreName}' 与 '{right.ServerCoreName}' 之间重叠");
            }
        }
    }

    private XFEServerCore? FindServerCore(Uri requestUrl, int localPort) => _serverCores
        .Where(core => _bindingEndpoints[core].Any(endpoint => endpoint.Matches(requestUrl, localPort)))
        .Select(core => (Core: core, Matches: core.TryGetRoute(requestUrl, out _)))
        .Where(item => item.Matches)
        .OrderByDescending(item => item.Core.MainEntryPointSegmentCount)
        .Select(item => item.Core)
        .FirstOrDefault();

    private ValueTask CyberCommServerStartedAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        foreach (var serverCore in _serverCores) serverCore.NotifyServerStarted(CyberCommServer, EventArgs.Empty);
        return ValueTask.CompletedTask;
    }

    private ValueTask CyberCommServerClientConnectedAsync(CyberCommServerEventArgs e, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (e.RequestUrl is not null) FindServerCore(e.RequestUrl, e.LocalPort)?.NotifyClientConnected(CyberCommServer, e);
        return ValueTask.CompletedTask;
    }

    private ValueTask CyberCommServerMessageReceivedAsync(CyberCommServerEventArgs e, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (e.RequestUrl is not null) FindServerCore(e.RequestUrl, e.LocalPort)?.NotifyMessageReceived(CyberCommServer, e);
        return ValueTask.CompletedTask;
    }

    private ValueTask CyberCommServerConnectionClosedAsync(CyberCommServerEventArgs e, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (e.RequestUrl is not null) FindServerCore(e.RequestUrl, e.LocalPort)?.NotifyConnectionClosed(CyberCommServer, e);
        return ValueTask.CompletedTask;
    }

    private readonly record struct ListenerKey(string Scheme, int Port);

    private readonly record struct BindingEndpoint(string Scheme, string Host, int Port)
    {
        public ListenerKey ListenerKey => new(Scheme, Port);

        public static BindingEndpoint Parse(string value)
        {
            ArgumentException.ThrowIfNullOrWhiteSpace(value);
            var normalized = value
                .Replace("://+", "://*", StringComparison.OrdinalIgnoreCase)
                .Replace("://*", "://wildcard.invalid", StringComparison.OrdinalIgnoreCase);
            if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
                throw new XFEServerBuilderException($"无效的核心服务器绑定地址:'{value}'");
            var host = uri.Host.Equals("wildcard.invalid", StringComparison.OrdinalIgnoreCase) ||
                uri.Host is "0.0.0.0" or "::" or "[::]"
                ? "*"
                : uri.IdnHost;
            return new(uri.Scheme.ToLowerInvariant(), host.ToLowerInvariant(), uri.Port);
        }

        public bool Matches(Uri requestUri, int localPort) =>
            Scheme.Equals(requestUri.Scheme, StringComparison.OrdinalIgnoreCase) &&
            Port == localPort &&
            (Host == "*" || Host.Equals(requestUri.IdnHost, StringComparison.OrdinalIgnoreCase));

        public bool Overlaps(BindingEndpoint other) =>
            Scheme.Equals(other.Scheme, StringComparison.OrdinalIgnoreCase) &&
            Port == other.Port &&
            (Host == "*" || other.Host == "*" || Host.Equals(other.Host, StringComparison.OrdinalIgnoreCase));
    }
}