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 //methods available on the hub in the server
public interface IBTCPayAppHubServer 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<Dictionary<string,string>> Pair(PairRequest request);
Task<AppHandshakeResponse> Handshake(AppHandshake request); Task<AppHandshakeResponse> Handshake(AppHandshake request);

View file

@ -15,6 +15,8 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using VSSProto; using VSSProto;
namespace BTCPayServer.App.API; namespace BTCPayServer.App.API;
@ -29,20 +31,21 @@ public class VSSController : Controller, IVSSAPI
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayAppState _appState; private readonly BTCPayAppState _appState;
private readonly ILogger<VSSController> _logger;
public VSSController(ApplicationDbContextFactory dbContextFactory, public VSSController(ApplicationDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager, BTCPayAppState appState) UserManager<ApplicationUser> userManager, BTCPayAppState appState, ILogger<VSSController> logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_userManager = userManager; _userManager = userManager;
_appState = appState; _appState = appState;
_logger = logger;
} }
[HttpPost(HttpVSSAPIClient.GET_OBJECT)] [HttpPost(HttpVSSAPIClient.GET_OBJECT)]
[MediaTypeConstraint("application/octet-stream")] [MediaTypeConstraint("application/octet-stream")]
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken) public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken)
{ {
var userId = _userManager.GetUserId(User); var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext(); await using var dbContext = _dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems.SingleOrDefaultAsync(data => var store = await dbContext.AppStorageItems.SingleOrDefaultAsync(data =>
@ -74,22 +77,20 @@ public class VSSController : Controller, IVSSAPI
private bool VerifyGlobalVersion(long globalVersion) private bool VerifyGlobalVersion(long globalVersion)
{ {
var userId = _userManager.GetUserId(User); 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) if (conn.Key == null)
{ {
return false; 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)] [HttpPost(HttpVSSAPIClient.PUT_OBJECTS)]
[MediaTypeConstraint("application/octet-stream")] [MediaTypeConstraint("application/octet-stream")]
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken) public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken)
{ {
if (!VerifyGlobalVersion(request.GlobalVersion)) if (!VerifyGlobalVersion(request.GlobalVersion))
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse() return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
{ {
@ -99,7 +100,8 @@ public class VSSController : Controller, IVSSAPI
var userId = _userManager.GetUserId(User); var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext(); await using var dbContext = _dbContextFactory.CreateContext();
return await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async async =>
{
await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try try
{ {
@ -122,31 +124,32 @@ public class VSSController : Controller, IVSSAPI
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
await dbContextTransaction.CommitAsync(cancellationToken); await dbContextTransaction.CommitAsync(cancellationToken);
return new PutObjectResponse();
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Error while processing transaction");
await dbContextTransaction.RollbackAsync(cancellationToken); await dbContextTransaction.RollbackAsync(cancellationToken);
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse() return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
{ {
ErrorCode = ErrorCode.ConflictException, Message = e.Message ErrorCode = ErrorCode.ConflictException, Message = e.Message
})); }));
} }
}, cancellationToken);
return new PutObjectResponse();
} }
[HttpPost(HttpVSSAPIClient.DELETE_OBJECT)] [HttpPost(HttpVSSAPIClient.DELETE_OBJECT)]
[MediaTypeConstraint("application/octet-stream")] [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); var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext(); await using var dbContext = _dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems var store = await dbContext.AppStorageItems
.Where(data => data.Key == request.KeyValue.Key && data.UserId == userId && .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 return store == 0
? SetResult<DeleteObjectResponse>( ? SetResult<DeleteObjectResponse>(
new NotFoundObjectResult(new ErrorResponse() new NotFoundObjectResult(new ErrorResponse()
@ -157,13 +160,15 @@ public class VSSController : Controller, IVSSAPI
} }
[HttpPost(HttpVSSAPIClient.LIST_KEY_VERSIONS)] [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); var userId = _userManager.GetUserId(User);
await using var dbContext = _dbContextFactory.CreateContext(); await using var dbContext = _dbContextFactory.CreateContext();
var items = await dbContext.AppStorageItems var items = await dbContext.AppStorageItems
.Where(data => data.UserId == userId) .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}}; return new ListKeyVersionsResponse {KeyVersions = {items}};
} }
} }

View file

@ -300,7 +300,7 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
await _appState.InvoiceUpdate(identifier, lightningInvoice); 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); return await _appState.DeviceMasterSignal(Context.ConnectionId,deviceIdentifier,active);
} }

View file

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