LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
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);
}
}
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);
}
}