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

LumaTunnel

【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;
using LumaTunnel.Server.Core.Persistence;

namespace LumaTunnel.Server.Core.Security;

public sealed class DeviceRegistry(AtomicJsonStore<DeviceDatabase> store) : IDisposable
{
    private readonly ConcurrentDictionary<string, DeviceRecord> _devices = new(StringComparer.Ordinal);
    private readonly AtomicJsonStore<DeviceDatabase> _store = store;
    private readonly SemaphoreSlim _mutationGate = new(1, 1);

    public event Action<string>? DeviceRevoked;

    public IReadOnlyCollection<DeviceRecord> Devices => _devices.Values.OrderBy(static x => x.DeviceName).ToArray();

    public async Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        var database = await _store.LoadOrCreateAsync(static () => new DeviceDatabase(), cancellationToken).ConfigureAwait(false);
        foreach (var device in database.Devices)
            _devices[device.DeviceId] = device;
    }

    public async Task ReloadAsync(CancellationToken cancellationToken = default)
    {
        List<string> revoked = [];
        await _mutationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var database = await _store.LoadOrCreateAsync(static () => new DeviceDatabase(), cancellationToken).ConfigureAwait(false);
            var incoming = database.Devices.ToDictionary(static item => item.DeviceId, StringComparer.Ordinal);
            revoked.AddRange(_devices.Values.Where(current => current.Enabled && (!incoming.TryGetValue(current.DeviceId, out var replacement) || !replacement.Enabled)).Select(static item => item.DeviceId));
            _devices.Clear();
            foreach (var device in incoming.Values)
                _devices[device.DeviceId] = device;
        }
        finally
        {
            _mutationGate.Release();
        }

        foreach (var deviceId in revoked)
            DeviceRevoked?.Invoke(deviceId);
    }

    public async Task<(DeviceRecord Device, string Token)> EnrollAsync(string deviceId, string deviceName, string clientVersion, CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(deviceId);
        ArgumentException.ThrowIfNullOrWhiteSpace(deviceName);

        var token = TokenUtilities.CreateDeviceToken();
        var record = new DeviceRecord
        {
            DeviceId = deviceId.Trim(),
            DeviceName = deviceName.Trim(),
            ClientVersion = clientVersion.Trim(),
            TokenHash = TokenUtilities.Hash(token)
        };

        await _mutationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            _devices[record.DeviceId] = record;
            await SaveCoreAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _mutationGate.Release();
        }

        return (record, token);
    }

    public bool Validate(string deviceId, string token, out DeviceRecord? record)
    {
        if (_devices.TryGetValue(deviceId, out var candidate)
            && candidate.Enabled
            && TokenUtilities.FixedTimeHashEquals(token, candidate.TokenHash))
        {
            record = candidate;
            return true;
        }

        record = null;
        return false;
    }

    public async Task<bool> RevokeAsync(string deviceId, CancellationToken cancellationToken = default)
    {
        await _mutationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (!_devices.TryGetValue(deviceId, out var existing))
                return false;
            _devices[deviceId] = existing with { Enabled = false };
            await SaveCoreAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _mutationGate.Release();
        }

        DeviceRevoked?.Invoke(deviceId);
        return true;
    }

    public async Task<bool> RenameAsync(string deviceId, string name, CancellationToken cancellationToken = default)
    {
        await _mutationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (!_devices.TryGetValue(deviceId, out var existing))
                return false;
            _devices[deviceId] = existing with { DeviceName = name.Trim() };
            await SaveCoreAsync(cancellationToken).ConfigureAwait(false);
            return true;
        }
        finally
        {
            _mutationGate.Release();
        }
    }

    public async Task AddTrafficAsync(string deviceId, long uploadBytes, long downloadBytes, CancellationToken cancellationToken = default)
    {
        if (!_devices.TryGetValue(deviceId, out var existing))
            return;

        _devices[deviceId] = existing with
        {
            LastSeenAtUtc = DateTimeOffset.UtcNow,
            UploadBytes = existing.UploadBytes + Math.Max(0, uploadBytes),
            DownloadBytes = existing.DownloadBytes + Math.Max(0, downloadBytes)
        };

        // Traffic persistence is intentionally batched by the runtime flush timer.
        await Task.CompletedTask.ConfigureAwait(false);
    }

    public async Task SaveAsync(CancellationToken cancellationToken = default)
    {
        await _mutationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await SaveCoreAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _mutationGate.Release();
        }
    }

    private Task SaveCoreAsync(CancellationToken cancellationToken) =>
        _store.SaveAsync(new DeviceDatabase { Devices = _devices.Values.OrderBy(static x => x.DeviceId).ToList() }, cancellationToken);

    public void Dispose()
    {
        _mutationGate.Dispose();
        _store.Dispose();
        GC.SuppressFinalize(this);
    }
}