switch to a long for device identifier

This commit is contained in:
Kukks 2024-07-30 15:56:45 +02:00
parent c0ad32eaf7
commit 7bdb136c27
No known key found for this signature in database
GPG key ID: 8E5530D9D1C93097
4 changed files with 70 additions and 55 deletions

View file

@ -27,7 +27,7 @@ public interface IBTCPayAppHubClient
//methods available on the hub in the server
public interface IBTCPayAppHubServer
{
Task<bool> DeviceMasterSignal(string deviceIdentifier, bool active);
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
Task<Dictionary<string,string>> Pair(PairRequest request);
Task<AppHandshakeResponse> Handshake(AppHandshake request);

View file

@ -15,6 +15,8 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using VSSProto;
namespace BTCPayServer.App.API;
@ -29,20 +31,21 @@ public class VSSController : Controller, IVSSAPI
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayAppState _appState;
private readonly ILogger<VSSController> _logger;
public VSSController(ApplicationDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager, BTCPayAppState appState)
UserManager<ApplicationUser> userManager, BTCPayAppState appState, ILogger<VSSController> logger)
{
_dbContextFactory = dbContextFactory;
_userManager = userManager;
_appState = appState;
_logger = logger;
}
[HttpPost(HttpVSSAPIClient.GET_OBJECT)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken)
{
var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems.SingleOrDefaultAsync(data =>
@ -74,79 +77,79 @@ public class VSSController : Controller, IVSSAPI
private bool VerifyGlobalVersion(long globalVersion)
{
var userId = _userManager.GetUserId(User);
var conn = _appState.Connections.SingleOrDefault(pair => pair.Value.Master && pair.Value.UserId == userId);
var conn = _appState.Connections.SingleOrDefault(pair => pair.Value.Master && pair.Value.UserId == userId);
if (conn.Key == null)
{
return false;
}
// This has a high collision rate, but we're not expecting something insane here since we have auth and other checks in place.
return globalVersion == conn.Value.DeviceIdentifier.GetHashCode();
return globalVersion == conn.Value.DeviceIdentifier;
}
[HttpPost(HttpVSSAPIClient.PUT_OBJECTS)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken)
{
if (!VerifyGlobalVersion(request.GlobalVersion))
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
{
ErrorCode = ErrorCode.ConflictException, Message = "Global version mismatch"
}));
var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext();
await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
return await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async async =>
{
if (request.TransactionItems.Any())
await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
var items = request.TransactionItems.Select(data => new AppStorageItemData()
if (request.TransactionItems.Any())
{
Key = data.Key, Value = data.Value.ToByteArray(), UserId = userId, Version = data.Version
});
await dbContext.AppStorageItems.AddRangeAsync(items, cancellationToken);
var items = request.TransactionItems.Select(data => new AppStorageItemData()
{
Key = data.Key, Value = data.Value.ToByteArray(), UserId = userId, Version = data.Version
});
await dbContext.AppStorageItems.AddRangeAsync(items, cancellationToken);
}
if (request.DeleteItems.Any())
{
var deleteQuery = request.DeleteItems.Aggregate(
dbContext.AppStorageItems.Where(data => data.UserId == userId),
(current, key) => current.Where(data => data.Key == key.Key && data.Version == key.Version));
await deleteQuery.ExecuteDeleteAsync(cancellationToken: cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
await dbContextTransaction.CommitAsync(cancellationToken);
return new PutObjectResponse();
}
if (request.DeleteItems.Any())
catch (Exception e)
{
var deleteQuery = request.DeleteItems.Aggregate(
dbContext.AppStorageItems.Where(data => data.UserId == userId),
(current, key) => current.Where(data => data.Key == key.Key && data.Version == key.Version));
await deleteQuery.ExecuteDeleteAsync(cancellationToken: cancellationToken);
_logger.LogError(e, "Error while processing transaction");
await dbContextTransaction.RollbackAsync(cancellationToken);
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
{
ErrorCode = ErrorCode.ConflictException, Message = e.Message
}));
}
await dbContext.SaveChangesAsync(cancellationToken);
await dbContextTransaction.CommitAsync(cancellationToken);
}
catch (Exception e)
{
await dbContextTransaction.RollbackAsync(cancellationToken);
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
{
ErrorCode = ErrorCode.ConflictException, Message = e.Message
}));
}
return new PutObjectResponse();
}, cancellationToken);
}
[HttpPost(HttpVSSAPIClient.DELETE_OBJECT)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken)
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request,
CancellationToken cancellationToken)
{
var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems
.Where(data => data.Key == request.KeyValue.Key && data.UserId == userId &&
data.Version == request.KeyValue.Version).ExecuteDeleteAsync(cancellationToken: cancellationToken);
data.Version == request.KeyValue.Version)
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
return store == 0
? SetResult<DeleteObjectResponse>(
new NotFoundObjectResult(new ErrorResponse()
@ -157,13 +160,15 @@ public class VSSController : Controller, IVSSAPI
}
[HttpPost(HttpVSSAPIClient.LIST_KEY_VERSIONS)]
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request, CancellationToken cancellationToken)
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request,
CancellationToken cancellationToken)
{
var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext();
var items = await dbContext.AppStorageItems
.Where(data => data.UserId == userId)
.Select(data => new KeyValue() {Key = data.Key, Version = data.Version}).ToListAsync(cancellationToken: cancellationToken);
.Select(data => new KeyValue() {Key = data.Key, Version = data.Version})
.ToListAsync(cancellationToken: cancellationToken);
return new ListKeyVersionsResponse {KeyVersions = {items}};
}
}

View file

@ -300,7 +300,7 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
await _appState.InvoiceUpdate(identifier, lightningInvoice);
}
public async Task<bool> DeviceMasterSignal(string deviceIdentifier, bool active)
public async Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active)
{
return await _appState.DeviceMasterSignal(Context.ConnectionId,deviceIdentifier,active);
}

View file

@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers;
public record ConnectedInstance(
string UserId,
string DeviceIdentifier,
long? DeviceIdentifier,
bool Master,
// string ProvidedAccessKey,
HashSet<string> Groups);
@ -45,7 +45,7 @@ public class BTCPayAppState : IHostedService
private DerivationSchemeParser _derivationSchemeParser;
public ConcurrentDictionary<string, ConnectedInstance> Connections { get; set; }
public ConcurrentDictionary<string, ConnectedInstance> Connections { get; set; } = new();
private CancellationTokenSource? _cts;
@ -266,24 +266,29 @@ public class BTCPayAppState : IHostedService
}
public async Task<bool> DeviceMasterSignal(string contextConnectionId, string deviceIdentifier, bool active)
public async Task<bool> DeviceMasterSignal(string contextConnectionId, long deviceIdentifier, bool active)
{
if (!Connections.TryGetValue(contextConnectionId, out var connectedInstance))
{
_logger.LogWarning("DeviceMasterSignal called on non existing connection");
return false;
}
if(!string.IsNullOrEmpty(connectedInstance.DeviceIdentifier) && connectedInstance.DeviceIdentifier != deviceIdentifier)
if(connectedInstance.DeviceIdentifier != null && connectedInstance.DeviceIdentifier != deviceIdentifier)
{
_logger.LogWarning("DeviceMasterSignal called with different device identifier");
return false;
}
if(string.IsNullOrEmpty(connectedInstance.DeviceIdentifier))
if(connectedInstance.DeviceIdentifier == null)
{
Connections[contextConnectionId] = connectedInstance with {DeviceIdentifier = deviceIdentifier};
_logger.LogInformation("DeviceMasterSignal called with device identifier {deviceIdentifier}", deviceIdentifier);
connectedInstance = connectedInstance with {DeviceIdentifier = deviceIdentifier};
Connections[contextConnectionId] = connectedInstance;
}
if(connectedInstance.Master == active)
{
_logger.LogInformation("DeviceMasterSignal called with same active state");
return true;
}
else if (active)
@ -291,17 +296,22 @@ public class BTCPayAppState : IHostedService
//check if there is any other master connection with the same user id
if (Connections.Values.Any(c => c.UserId == connectedInstance.UserId && c.Master))
{
_logger.LogWarning("DeviceMasterSignal called with active state but there is already a master connection");
return false;
}
else
{
Connections[contextConnectionId] = connectedInstance with {Master = true};
_logger.LogInformation("DeviceMasterSignal called with active state");
connectedInstance = connectedInstance with {Master = true};
Connections[contextConnectionId] = connectedInstance;
return true;
}
}
else
{
Connections[contextConnectionId] = connectedInstance with {Master = false};
_logger.LogInformation("DeviceMasterSignal called with inactive state");
connectedInstance = connectedInstance with {Master = false};
Connections[contextConnectionId] = connectedInstance;
return true;
}
@ -319,7 +329,7 @@ public class BTCPayAppState : IHostedService
public async Task Connected(string contextConnectionId, string userId)
{
Connections.TryAdd(contextConnectionId, new ConnectedInstance(userId, string.Empty, false, new HashSet<string>()));
Connections.TryAdd(contextConnectionId, new ConnectedInstance(userId, null, false, new HashSet<string>()));
if (_nodeInfo.Length > 0)
await _hubContext.Clients.Client(contextConnectionId).NotifyServerNode(_nodeInfo);