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 LumaTunnel.Server.Core.Persistence;

namespace LumaTunnel.Server.Core.Security;

public sealed class PairingCodeService(AtomicJsonStore<PairingCodeDatabase> store) : IDisposable
{
    private readonly AtomicJsonStore<PairingCodeDatabase> _store = store;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private PairingCodeDatabase _database = new();

    public async Task InitializeAsync(CancellationToken cancellationToken = default) =>
        _database = await _store.LoadOrCreateAsync(static () => new PairingCodeDatabase(), cancellationToken).ConfigureAwait(false);

    public async Task<(string Code, DateTimeOffset ExpiresAtUtc)> CreateAsync(TimeSpan lifetime, CancellationToken cancellationToken = default)
    {
        if (lifetime <= TimeSpan.Zero || lifetime > TimeSpan.FromDays(1))
            throw new ArgumentOutOfRangeException(nameof(lifetime), "Pairing code lifetime must be between zero and one day.");

        var code = TokenUtilities.CreatePairingCode();
        var expires = DateTimeOffset.UtcNow.Add(lifetime);
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            _database = await _store.LoadOrCreateAsync(static () => new PairingCodeDatabase(), cancellationToken).ConfigureAwait(false);
            _database.Codes.RemoveAll(static x => x.Used || x.ExpiresAtUtc <= DateTimeOffset.UtcNow);
            _database.Codes.Add(new PairingCodeRecord { CodeHash = TokenUtilities.Hash(code), ExpiresAtUtc = expires });
            await _store.SaveAsync(_database, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
        return (code, expires);
    }

    public async Task<bool> ConsumeAsync(string code, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            // Pairing codes are normally created by a separate elevated CLI process.
            _database = await _store.LoadOrCreateAsync(static () => new PairingCodeDatabase(), cancellationToken).ConfigureAwait(false);
            var now = DateTimeOffset.UtcNow;
            var match = _database.Codes.FirstOrDefault(x => !x.Used && x.ExpiresAtUtc > now && TokenUtilities.FixedTimeHashEquals(code, x.CodeHash));
            if (match is null)
                return false;

            var index = _database.Codes.IndexOf(match);
            _database.Codes[index] = match with { Used = true };
            await _store.SaveAsync(_database, cancellationToken).ConfigureAwait(false);
            return true;
        }
        finally
        {
            _gate.Release();
        }
    }

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