Syncshells (#11)

* some groups stuff

* further groups rework

* fixes for pause changes

* adjsut timeout interval

* fixes and namespace change to file scoped

* more fixes

* further implement groups

* fix change group ownership

* add some more stuff for groups

* more fixes and additions

* some fixes based on analyzers, add shard info to ui

* add discord command, cleanup

* fix regex

* add group migration and deletion on user deletion

* add api method for client to check health of connection

* adjust regex for vanity

* fixes for server and bot

* fixes some string comparison in linq queries

* fixes group leave and sets alias to null

* fix syntax in changeownership

* add better logging, fixes for group leaving

* fixes for group leave

Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
rootdarkarchon
2022-10-04 14:13:43 +02:00
committed by GitHub
parent d866223069
commit bff21ead95
57 changed files with 3761 additions and 1239 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "6.0.6",
"version": "6.0.9",
"commands": [
"dotnet-ef"
]

View File

@@ -1,14 +0,0 @@
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
namespace MareSynchronosServer.Hubs
{
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext context)
{
return context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
}
}
}

View File

@@ -8,165 +8,164 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Hubs
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
public partial class MareHub
private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin;
private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin;
private List<string> OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList();
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminChangeModeratorStatus)]
public async Task ChangeModeratorStatus(string uid, bool isModerator)
{
private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin;
if (!IsAdmin) return;
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin;
if (user == null) return;
private List<string> OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList();
user.IsModerator = isModerator;
_dbContext.Update(user);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminChangeModeratorStatus)]
public async Task ChangeModeratorStatus(string uid, bool isModerator)
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminDeleteBannedUser)]
public async Task DeleteBannedUser(BannedUserDto dto)
{
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
var existingUser =
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
if (existingUser == null)
{
if (!IsAdmin) return;
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
if (user == null) return;
user.IsModerator = isModerator;
_dbContext.Update(user);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
return;
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminDeleteBannedUser)]
public async Task DeleteBannedUser(BannedUserDto dto)
_dbContext.Remove(existingUser);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminDeleteForbiddenFile)]
public async Task DeleteForbiddenFile(ForbiddenFileDto dto)
{
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
var existingFile =
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
if (existingFile == null)
{
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
var existingUser =
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
if (existingUser == null)
{
return;
}
_dbContext.Remove(existingUser);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false);
return;
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminDeleteForbiddenFile)]
public async Task DeleteForbiddenFile(ForbiddenFileDto dto)
_dbContext.Remove(existingFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetBannedUsers)]
public async Task<List<BannedUserDto>> GetBannedUsers()
{
if (!IsModerator) return null;
return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto()
{
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
CharacterHash = b.CharacterIdentification,
Reason = b.Reason
}).ToListAsync().ConfigureAwait(false);
}
var existingFile =
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
if (existingFile == null)
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetForbiddenFiles)]
public async Task<List<ForbiddenFileDto>> GetForbiddenFiles()
{
if (!IsModerator) return null;
return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto()
{
Hash = b.Hash,
ForbiddenBy = b.ForbiddenBy
}).ToListAsync().ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetOnlineUsers)]
public async Task<List<OnlineUserDto>> AdminGetOnlineUsers()
{
if (!IsModerator) return null;
var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false);
return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID).Result)).Select(async b => new OnlineUserDto
{
CharacterNameHash = await _clientIdentService.GetCharacterIdentForUid(b.UID).ConfigureAwait(false),
UID = b.UID,
IsModerator = b.IsModerator,
IsAdmin = b.IsAdmin
}).Select(c => c.Result).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminUpdateOrAddBannedUser)]
public async Task UpdateOrAddBannedUser(BannedUserDto dto)
{
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
var existingUser =
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
if (existingUser != null)
{
existingUser.Reason = dto.Reason;
_dbContext.Update(existingUser);
}
else
{
await _dbContext.BannedUsers.AddAsync(new Banned
{
return;
}
_dbContext.Remove(existingFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false);
CharacterIdentification = dto.CharacterHash,
Reason = dto.Reason
}).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetBannedUsers)]
public async Task<List<BannedUserDto>> GetBannedUsers()
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false);
var bannedUser = await _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash).ConfigureAwait(false);
if (!string.IsNullOrEmpty(bannedUser))
{
if (!IsModerator) return null;
return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto()
{
CharacterHash = b.CharacterIdentification,
Reason = b.Reason
}).ToListAsync().ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetForbiddenFiles)]
public async Task<List<ForbiddenFileDto>> GetForbiddenFiles()
{
if (!IsModerator) return null;
return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto()
{
Hash = b.Hash,
ForbiddenBy = b.ForbiddenBy
}).ToListAsync().ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeAdminGetOnlineUsers)]
public async Task<List<OnlineUserDto>> AdminGetOnlineUsers()
{
if (!IsModerator) return null;
var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false);
return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID))).Select(b => new OnlineUserDto
{
CharacterNameHash = _clientIdentService.GetCharacterIdentForUid(b.UID),
UID = b.UID,
IsModerator = b.IsModerator,
IsAdmin = b.IsAdmin
}).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminUpdateOrAddBannedUser)]
public async Task UpdateOrAddBannedUser(BannedUserDto dto)
{
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
var existingUser =
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
if (existingUser != null)
{
existingUser.Reason = dto.Reason;
_dbContext.Update(existingUser);
}
else
{
await _dbContext.BannedUsers.AddAsync(new Banned
{
CharacterIdentification = dto.CharacterHash,
Reason = dto.Reason
}).ConfigureAwait(false);
}
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false);
var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash);
if (!string.IsNullOrEmpty(bannedUser))
{
await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)]
public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto)
{
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
var existingForbiddenFile =
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
if (existingForbiddenFile != null)
{
existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy;
_dbContext.Update(existingForbiddenFile);
}
else
{
await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry
{
Hash = dto.Hash,
ForbiddenBy = dto.ForbiddenBy
}).ConfigureAwait(false);
}
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false);
await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)]
public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto)
{
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
var existingForbiddenFile =
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
if (existingForbiddenFile != null)
{
existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy;
_dbContext.Update(existingForbiddenFile);
}
else
{
await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry
{
Hash = dto.Hash,
ForbiddenBy = dto.ForbiddenBy
}).ConfigureAwait(false);
}
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false);
}
}

View File

@@ -16,228 +16,233 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Hubs
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
public partial class MareHub
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileAbortUpload)]
public async Task AbortUpload()
{
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileAbortUpload)]
public async Task AbortUpload()
{
_logger.LogInformation("User {AuthenticatedUserId} aborted upload", AuthenticatedUserId);
var userId = AuthenticatedUserId;
var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList();
_dbContext.RemoveRange(notUploadedFiles);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
_logger.LogCallInfo(Api.SendFileAbortUpload);
var userId = AuthenticatedUserId;
var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList();
_dbContext.RemoveRange(notUploadedFiles);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileDeleteAllFiles)]
public async Task DeleteAllFiles()
{
_logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId);
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileDeleteAllFiles)]
public async Task DeleteAllFiles()
{
_logger.LogCallInfo(Api.SendFileDeleteAllFiles);
var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
var request = new DeleteFilesRequest();
request.Hash.AddRange(ownFiles.Select(f => f.Hash));
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
};
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGetFilesSizes)]
public async Task<List<DownloadFileDto>> GetFilesSizes(List<string> hashes)
{
var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.
Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
List<DownloadFileDto> response = new();
FileSizeRequest request = new FileSizeRequest();
request.Hash.AddRange(hashes);
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
};
var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false);
foreach (var hash in grpcResponse.HashToFileSize)
var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
var request = new DeleteFilesRequest();
request.Hash.AddRange(ownFiles.Select(f => f.Hash));
Metadata headers = new Metadata()
{
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash.Key);
var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash.Key);
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
};
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
}
response.Add(new DownloadFileDto
{
FileExists = hash.Value > 0,
ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty,
IsForbidden = forbiddenFile != null,
Hash = hash.Key,
Size = hash.Value,
Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString()
});
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGetFilesSizes)]
public async Task<List<DownloadFileDto>> GetFilesSizes(List<string> hashes)
{
_logger.LogCallInfo(Api.InvokeGetFilesSizes, hashes.Count.ToString());
return response;
}
var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.
Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
List<DownloadFileDto> response = new();
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeFileIsUploadFinished)]
public async Task<bool> IsUploadFinished()
FileSizeRequest request = new FileSizeRequest();
request.Hash.AddRange(hashes);
Metadata headers = new Metadata()
{
var userUid = AuthenticatedUserId;
return await _dbContext.Files.AsNoTracking()
.AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false);
}
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
};
var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false);
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeFileSendFiles)]
public async Task<List<UploadFileDto>> SendFiles(List<string> fileListHashes)
foreach (var hash in grpcResponse.HashToFileSize)
{
var userSentHashes = new HashSet<string>(fileListHashes.Distinct());
_logger.LogInformation("User {AuthenticatedUserId} sending files: {count}", AuthenticatedUserId, userSentHashes.Count);
var notCoveredFiles = new Dictionary<string, UploadFileDto>();
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal));
var downloadFile = allFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal));
List<FileCache> fileCachesToUpload = new();
foreach (var file in userSentHashes)
response.Add(new DownloadFileDto
{
// Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes
if (string.IsNullOrEmpty(file)) { continue; }
if (notCoveredFiles.ContainsKey(file)) { continue; }
if (forbiddenFiles.ContainsKey(file))
{
notCoveredFiles[file] = new UploadFileDto()
{
ForbiddenBy = forbiddenFiles[file].ForbiddenBy,
Hash = file,
IsForbidden = true
};
FileExists = hash.Value > 0,
ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty,
IsForbidden = forbiddenFile != null,
Hash = hash.Key,
Size = hash.Value,
Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString()
});
}
continue;
}
if (existingFiles.ContainsKey(file)) { continue; }
return response;
}
_logger.LogInformation("User {AuthenticatedUserId} needs upload: {file}", AuthenticatedUserId, file);
var userId = AuthenticatedUserId;
fileCachesToUpload.Add(new FileCache()
{
Hash = file,
Uploaded = false,
Uploader = uploader
});
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeFileIsUploadFinished)]
public async Task<bool> IsUploadFinished()
{
_logger.LogCallInfo(Api.InvokeFileIsUploadFinished);
var userUid = AuthenticatedUserId;
return await _dbContext.Files.AsNoTracking()
.AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeFileSendFiles)]
public async Task<List<UploadFileDto>> SendFiles(List<string> fileListHashes)
{
var userSentHashes = new HashSet<string>(fileListHashes.Distinct(StringComparer.Ordinal), StringComparer.Ordinal);
_logger.LogCallInfo(Api.InvokeFileSendFiles, userSentHashes.Count.ToString());
var notCoveredFiles = new Dictionary<string, UploadFileDto>(StringComparer.Ordinal);
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
List<FileCache> fileCachesToUpload = new();
foreach (var file in userSentHashes)
{
// Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes
if (string.IsNullOrEmpty(file)) { continue; }
if (notCoveredFiles.ContainsKey(file)) { continue; }
if (forbiddenFiles.ContainsKey(file))
{
notCoveredFiles[file] = new UploadFileDto()
{
ForbiddenBy = forbiddenFiles[file].ForbiddenBy,
Hash = file,
IsForbidden = true
};
continue;
}
//Save bulk
await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return notCoveredFiles.Values.ToList();
if (existingFiles.ContainsKey(file)) { continue; }
_logger.LogCallInfo(Api.InvokeFileSendFiles, file, "Missing");
var userId = AuthenticatedUserId;
fileCachesToUpload.Add(new FileCache()
{
Hash = file,
Uploaded = false,
Uploader = uploader
});
notCoveredFiles[file] = new UploadFileDto()
{
Hash = file,
};
}
//Save bulk
await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return notCoveredFiles.Values.ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileUploadFileStreamAsync)]
public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileUploadFileStreamAsync)]
public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
{
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash);
var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && !f.Uploaded);
if (relatedFile == null) return;
var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash);
if (forbiddenFile != null) return;
var tempFileName = Path.GetTempFileName();
using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate);
long length = 0;
try
{
_logger.LogInformation("User {AuthenticatedUserId} uploading file: {hash}", AuthenticatedUserId, hash);
await foreach (var chunk in fileContent.ConfigureAwait(false))
{
length += chunk.Length;
await fileStream.WriteAsync(chunk).ConfigureAwait(false);
}
var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && f.Uploaded == false);
if (relatedFile == null) return;
var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash);
if (forbiddenFile != null) return;
var tempFileName = Path.GetTempFileName();
using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate);
long length = 0;
await fileStream.FlushAsync().ConfigureAwait(false);
await fileStream.DisposeAsync().ConfigureAwait(false);
}
catch
{
try
{
await foreach (var chunk in fileContent.ConfigureAwait(false))
{
length += chunk.Length;
await fileStream.WriteAsync(chunk).ConfigureAwait(false);
}
await fileStream.FlushAsync().ConfigureAwait(false);
await fileStream.DisposeAsync().ConfigureAwait(false);
_dbContext.Files.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
catch
{
try
{
await fileStream.FlushAsync().ConfigureAwait(false);
await fileStream.DisposeAsync().ConfigureAwait(false);
_dbContext.Files.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
catch
{
// already removed
}
finally
{
File.Delete(tempFileName);
}
return;
}
_logger.LogInformation("User {AuthenticatedUserId} upload finished: {hash}, size: {length}", AuthenticatedUserId, hash, length);
try
{
var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false));
using var sha1 = SHA1.Create();
using var ms = new MemoryStream(decodedFile);
var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false);
var computedHashString = BitConverter.ToString(computedHash).Replace("-", "");
if (hash != computedHashString)
{
_logger.LogWarning("Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}", computedHashString, hash);
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return;
}
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
};
var streamingCall = _fileServiceClient.UploadFile(headers);
using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read);
int size = 1024 * 1024;
byte[] data = new byte[size];
int readBytes;
while ((readBytes = tempFileStream.Read(data, 0, size)) > 0)
{
await streamingCall.RequestStream.WriteAsync(new UploadFileRequest()
{
FileData = ByteString.CopyFrom(data, 0, readBytes),
Hash = computedHashString,
Uploader = AuthenticatedUserId
}).ConfigureAwait(false);
}
await streamingCall.RequestStream.CompleteAsync();
tempFileStream.Close();
await tempFileStream.DisposeAsync();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Upload failed");
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
// already removed
}
finally
{
File.Delete(tempFileName);
}
return;
}
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Uploaded");
try
{
var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false));
using var sha1 = SHA1.Create();
using var ms = new MemoryStream(decodedFile);
var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false);
var computedHashString = BitConverter.ToString(computedHash).Replace("-", "", StringComparison.Ordinal);
if (!string.Equals(hash, computedHashString, StringComparison.Ordinal))
{
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, hash, "Invalid", computedHashString);
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return;
}
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
};
var streamingCall = _fileServiceClient.UploadFile(headers);
using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read);
int size = 1024 * 1024;
byte[] data = new byte[size];
int readBytes;
while ((readBytes = tempFileStream.Read(data, 0, size)) > 0)
{
await streamingCall.RequestStream.WriteAsync(new UploadFileRequest()
{
FileData = ByteString.CopyFrom(data, 0, readBytes),
Hash = computedHashString,
Uploader = AuthenticatedUserId
}).ConfigureAwait(false);
}
await streamingCall.RequestStream.CompleteAsync().ConfigureAwait(false);
tempFileStream.Close();
await tempFileStream.DisposeAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Pushed");
}
catch (Exception ex)
{
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, "Failed", hash, ex.Message);
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
finally
{
File.Delete(tempFileName);
}
}
}

View File

@@ -0,0 +1,110 @@
using MareSynchronosShared.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.SignalR;
using System.Globalization;
using MareSynchronos.API;
using MareSynchronosServer.Utils;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
private async Task<List<PausedEntry>> GetAllPairedClientsWithPauseState(string? uid = null)
{
uid ??= AuthenticatedUserId;
var query = await (from userPair in _dbContext.ClientPairs
join otherUserPair in _dbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID
where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid
select new
{
UID = Convert.ToString(userPair.OtherUserUID),
GID = "DIRECT",
PauseState = (userPair.IsPaused || otherUserPair.IsPaused)
})
.Union(
(from userGroupPair in _dbContext.GroupPairs
join otherGroupPair in _dbContext.GroupPairs on userGroupPair.GroupGID equals otherGroupPair.GroupGID
where
userGroupPair.GroupUserUID == uid
&& otherGroupPair.GroupUserUID != uid
select new
{
UID = Convert.ToString(otherGroupPair.GroupUserUID),
GID = Convert.ToString(otherGroupPair.GroupGID),
PauseState = (userGroupPair.IsPaused || otherGroupPair.IsPaused)
})
).ToListAsync().ConfigureAwait(false);
return query.GroupBy(g => g.UID, g => (g.GID, g.PauseState),
(key, g) => new PausedEntry
{
UID = key,
PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsPaused = p.PauseState })
.ToList()
}, StringComparer.Ordinal).ToList();
}
private async Task<List<string>> GetAllPairedUnpausedUsers(string? uid = null)
{
uid ??= AuthenticatedUserId;
var ret = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false);
return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList();
}
private async Task<List<string>> SendDataToAllPairedUsers(string apiMethod, object arg)
{
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
await Clients.Users(usersToSendDataTo).SendAsync(apiMethod, arg).ConfigureAwait(false);
return usersToSendDataTo;
}
public string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown";
protected async Task<User> GetAuthenticatedUserUntrackedAsync()
{
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
}
private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> allUserPairs, string userIdent, string? uid = null)
{
uid ??= AuthenticatedUserId;
var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
if (userPair != null)
{
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return;
if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return;
}
var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
if (!string.IsNullOrEmpty(groupUserIdent))
{
await Clients.User(uid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
private async Task SendGroupDeletedToAll(List<GroupPair> groupUsers)
{
foreach (var pair in groupUsers)
{
var pairIdent = await _clientIdentService.GetCharacterIdentForUid(pair.GroupUserUID).ConfigureAwait(false);
if (string.IsNullOrEmpty(pairIdent)) continue;
var pairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false);
foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
{
await UserGroupLeave(groupUserPair, pairs, pairIdent, pair.GroupUserUID).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,548 @@
using MareSynchronos.API;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Authentication;
using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGroupCreate)]
public async Task<GroupCreatedDto> CreateGroup()
{
_logger.LogCallInfo(Api.InvokeGroupCreate);
var existingGroupsByUser = _dbContext.Groups.Count(u => u.OwnerUID == AuthenticatedUserId);
var existingJoinedGroups = _dbContext.GroupPairs.Count(u => u.GroupUserUID == AuthenticatedUserId);
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
{
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
}
var gid = StringUtils.GenerateRandomString(12);
while (await _dbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false))
{
gid = StringUtils.GenerateRandomString(12);
}
gid = "MSS-" + gid;
var passwd = StringUtils.GenerateRandomString(16);
var sha = SHA256.Create();
var hashedPw = StringUtils.Sha256String(passwd);
Group newGroup = new()
{
GID = gid,
HashedPassword = hashedPw,
InvitesEnabled = true,
OwnerUID = AuthenticatedUserId
};
GroupPair initialPair = new()
{
GroupGID = newGroup.GID,
GroupUserUID = AuthenticatedUserId,
IsPaused = false,
IsPinned = true
};
await _dbContext.Groups.AddAsync(newGroup).ConfigureAwait(false);
await _dbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var self = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = newGroup.GID,
OwnedBy = string.IsNullOrEmpty(self.Alias) ? self.UID : self.Alias,
IsDeleted = false,
IsPaused = false,
InvitesEnabled = true
}).ConfigureAwait(false);
_logger.LogCallInfo(Api.InvokeGroupCreate, gid);
return new GroupCreatedDto()
{
GID = newGroup.GID,
Password = passwd
};
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGroupGetGroups)]
public async Task<List<GroupDto>> GetGroups()
{
_logger.LogCallInfo(Api.InvokeGroupGetGroups);
var groups = await _dbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
return groups.Select(g => new GroupDto()
{
GID = g.GroupGID,
Alias = g.Group.Alias,
InvitesEnabled = g.Group.InvitesEnabled,
OwnedBy = string.IsNullOrEmpty(g.Group.Owner.Alias) ? g.Group.Owner.UID : g.Group.Owner.Alias,
IsPaused = g.IsPaused
}).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGroupGetUsersInGroup)]
public async Task<List<GroupPairDto>> GetUsersInGroup(string gid)
{
_logger.LogCallInfo(Api.InvokeGroupGetUsersInGroup, gid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
if (group == null || existingPair == null) return new List<GroupPairDto>();
var allPairs = await _dbContext.GroupPairs.Include(g => g.GroupUser).Where(g => g.GroupGID == gid && g.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
return allPairs.Select(p => new GroupPairDto()
{
GroupGID = gid,
IsPaused = p.IsPaused,
IsRemoved = false,
UserUID = p.GroupUser.UID,
UserAlias = p.GroupUser.Alias,
IsPinned = p.IsPinned
}).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupChangeInviteState)]
public async Task GroupChangeInviteState(string gid, bool enabled)
{
_logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString());
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
group.InvitesEnabled = enabled;
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString(), "Success");
var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToList();
await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = gid,
InvitesEnabled = enabled,
}).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupDelete)]
public async Task GroupDelete(string gid)
{
_logger.LogCallInfo(Api.SendGroupDelete, gid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
_logger.LogCallInfo(Api.SendGroupDelete, gid, "Success");
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false);
_dbContext.RemoveRange(groupPairs);
_dbContext.Remove(group);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
IsDeleted = true,
}).ConfigureAwait(false);
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGroupJoin)]
public async Task<bool> GroupJoin(string gid, string password)
{
_logger.LogCallInfo(Api.InvokeGroupJoin, gid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid || g.Alias == gid).ConfigureAwait(false);
var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
var hashedPw = StringUtils.Sha256String(password);
var existingUserCount = await _dbContext.GroupPairs.CountAsync(g => g.GroupGID == gid).ConfigureAwait(false);
var joinedGroups = await _dbContext.GroupPairs.CountAsync(g => g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
if (group == null
|| !string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal)
|| existingPair != null
|| existingUserCount >= _maxGroupUserCount
|| !group.InvitesEnabled
|| joinedGroups >= _maxJoinedGroupsByUser)
return false;
GroupPair newPair = new()
{
GroupGID = group.GID,
GroupUserUID = AuthenticatedUserId
};
await _dbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.InvokeGroupJoin, gid, "Success");
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
OwnedBy = group.OwnerUID,
IsDeleted = false,
IsPaused = false,
Alias = group.Alias,
InvitesEnabled = true
}).ConfigureAwait(false);
var self = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId);
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = group.GID,
IsPaused = false,
IsRemoved = false,
UserUID = AuthenticatedUserId,
UserAlias = self.Alias,
IsPinned = false
}).ConfigureAwait(false);
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
foreach (var groupUserPair in groupPairs)
{
var userPair = allUserPairs.Single(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue;
if (userPair.IsPausedExcludingGroup(gid) is PauseInfo.Unpaused) continue;
if (userPair.IsPausedPerGroup is PauseInfo.Paused) continue;
var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
if (!string.IsNullOrEmpty(groupUserIdent))
{
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
return true;
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupLeave)]
public async Task GroupLeave(string gid)
{
_logger.LogCallInfo(Api.SendGroupLeave, gid);
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
if (groupPair == null) return;
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToListAsync().ConfigureAwait(false);
var groupPairsWithoutSelf = groupPairs.Where(p => !string.Equals(p.GroupUserUID, AuthenticatedUserId, StringComparison.Ordinal)).ToList();
_dbContext.GroupPairs.Remove(groupPair);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
IsDeleted = true
}).ConfigureAwait(false);
bool ownerHasLeft = string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal);
if (ownerHasLeft)
{
if (!groupPairsWithoutSelf.Any())
{
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted");
_dbContext.Remove(group);
}
else
{
var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(_dbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false);
if (groupHasMigrated.Item1)
{
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Migrated", groupHasMigrated.Item2);
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
OwnedBy = groupHasMigrated.Item2,
Alias = null
}).ConfigureAwait(false);
}
else
{
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted");
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
IsDeleted = true
}).ConfigureAwait(false);
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
return;
}
}
}
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Success");
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = group.GID,
IsRemoved = true,
UserUID = AuthenticatedUserId,
}).ConfigureAwait(false);
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
foreach (var groupUserPair in groupPairsWithoutSelf)
{
await UserGroupLeave(groupUserPair, allUserPairs, userIdent).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupPause)]
public async Task GroupChangePauseState(string gid, bool isPaused)
{
_logger.LogCallInfo(Api.SendGroupPause, gid, isPaused);
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
if (groupPair == null) return;
groupPair.IsPaused = isPaused;
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendGroupPause, gid, isPaused, "Success");
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = gid,
IsPaused = isPaused,
UserUID = AuthenticatedUserId,
}).ConfigureAwait(false);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto
{
GID = gid,
IsPaused = isPaused
}).ConfigureAwait(false);
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
foreach (var groupUserPair in groupPairs)
{
var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
if (userPair != null)
{
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue;
if (userPair.IsPausedExcludingGroup(gid) is PauseInfo.Unpaused) continue;
}
var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
if (!string.IsNullOrEmpty(groupUserIdent))
{
await Clients.User(AuthenticatedUserId).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
await Clients.User(groupUserPair.GroupUserUID).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupRemoveUser)]
public async Task GroupRemoveUser(string gid, string uid)
{
_logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
if (groupPair == null) return;
_logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid, "Success");
_dbContext.GroupPairs.Remove(groupPair);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToList();
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = group.GID,
IsRemoved = true,
UserUID = uid,
}).ConfigureAwait(false);
var userIdent = await _clientIdentService.GetCharacterIdentForUid(uid).ConfigureAwait(false);
if (userIdent == null) return;
await Clients.User(uid).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = gid,
IsDeleted = true,
}).ConfigureAwait(false);
var allUserPairs = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false);
foreach (var groupUserPair in groupPairs)
{
await UserGroupLeave(groupUserPair, allUserPairs, userIdent, uid).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupChangeOwner)]
public async Task ChangeOwnership(string gid, string uid)
{
_logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
var groupPair = await _dbContext.GroupPairs.Include(g => g.GroupUser).SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
if (groupPair == null) return;
var ownedShells = await _dbContext.Groups.CountAsync(g => g.OwnerUID == uid).ConfigureAwait(false);
if (ownedShells >= _maxExistingGroupsByUser) return;
var prevOwner = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
prevOwner.IsPinned = false;
group.Owner = groupPair.GroupUser;
group.Alias = null;
groupPair.IsPinned = true;
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid, "Success");
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = gid,
OwnedBy = string.IsNullOrEmpty(group.Owner.Alias) ? group.Owner.UID : group.Owner.Alias,
Alias = null
}).ConfigureAwait(false);
await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = gid,
UserUID = uid,
IsPinned = true
}).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeGroupChangePassword)]
public async Task<bool> ChangeGroupPassword(string gid, string password)
{
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return false;
if (password.Length < 10) return false;
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, "Success");
group.HashedPassword = StringUtils.Sha256String(password);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupChangePinned)]
public async Task ChangePinned(string gid, string uid, bool isPinned)
{
_logger.LogCallInfo(Api.SendGroupChangePinned, gid, uid, isPinned);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
if (groupPair == null) return;
groupPair.IsPinned = isPinned;
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, uid, isPinned, "Success");
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = gid,
UserUID = uid,
IsPinned = isPinned
}).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendGroupClear)]
public async Task ClearGroup(string gid)
{
_logger.LogCallInfo(Api.SendGroupClear, gid);
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs.Where(p => !p.IsPinned).Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
{
GID = group.GID,
IsDeleted = true,
}).ConfigureAwait(false);
_logger.LogCallInfo(Api.SendGroupClear, gid, "Success");
var notPinned = groupPairs.Where(g => !g.IsPinned).ToList();
_dbContext.GroupPairs.RemoveRange(notPinned);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
foreach (var pair in notPinned)
{
await Clients.Users(groupPairs.Where(p => p.IsPinned).Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
{
GroupGID = pair.GroupGID,
IsRemoved = true,
UserUID = pair.GroupUserUID
}).ConfigureAwait(false);
var pairIdent = await _clientIdentService.GetCharacterIdentForUid(pair.GroupUserUID).ConfigureAwait(false);
if (string.IsNullOrEmpty(pairIdent)) continue;
var allUserPairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false);
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
{
await UserGroupLeave(groupUserPair, allUserPairs, pairIdent).ConfigureAwait(false);
}
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Authentication;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Models;
@@ -11,315 +12,340 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Hubs
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
public partial class MareHub
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserDeleteAccount)]
public async Task DeleteAccount()
{
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserDeleteAccount)]
public async Task DeleteAccount()
_logger.LogCallInfo(Api.SendUserDeleteAccount);
string userid = AuthenticatedUserId;
var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false);
var charaIdent = await _clientIdentService.GetCharacterIdentForUid(userid).ConfigureAwait(false);
var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false);
var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false);
var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false);
var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == userid).ToListAsync().ConfigureAwait(false);
if (lodestone != null)
{
_logger.LogInformation("User {AuthenticatedUserId} deleted their account", AuthenticatedUserId);
string userid = AuthenticatedUserId;
var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false);
var charaIdent = _clientIdentService.GetCharacterIdentForUid(userid);
var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false);
var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false);
var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false);
if (lodestone != null)
{
_dbContext.Remove(lodestone);
}
while (_dbContext.Files.Any(f => f.Uploader == userEntry))
{
await Task.Delay(1000).ConfigureAwait(false);
}
await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false);
_dbContext.RemoveRange(ownPairData);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var otherPairData = await _dbContext.ClientPairs.Include(u => u.User)
.Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false);
foreach (var pair in otherPairData)
{
await Clients.User(pair.User.UID)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = userid,
IsRemoved = true
}, charaIdent).ConfigureAwait(false);
}
_mareMetrics.DecGauge(MetricsAPI.GaugePairs, ownPairData.Count + otherPairData.Count);
_mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused, ownPairData.Count(c => c.IsPaused));
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
_dbContext.RemoveRange(otherPairData);
_dbContext.Remove(userEntry);
_dbContext.Remove(auth);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_dbContext.Remove(lodestone);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserGetOnlineCharacters)]
public async Task<List<string>> GetOnlineCharacters()
while (_dbContext.Files.Any(f => f.Uploader == userEntry))
{
_logger.LogInformation("User {AuthenticatedUserId} requested online characters", AuthenticatedUserId);
var ownUser = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
var otherUsers = await _dbContext.ClientPairs.AsNoTracking()
.Include(u => u.User)
.Include(u => u.OtherUser)
.Where(w => w.User.UID == ownUser.UID && !w.IsPaused)
//.Where(w => !string.IsNullOrEmpty(w.OtherUser.CharacterIdentification))
.Select(e => e.OtherUser).ToListAsync().ConfigureAwait(false);
var otherOnlineUsers =
otherUsers.Where(u => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(u.UID)));
var otherEntries = await _dbContext.ClientPairs.AsNoTracking()
.Include(u => u.User)
.Where(u => otherOnlineUsers.Any(e => e == u.User) && u.OtherUser == ownUser && !u.IsPaused)
.ToListAsync().ConfigureAwait(false);
var ownIdent = _clientIdentService.GetCharacterIdentForUid(ownUser.UID);
await Clients.Users(otherEntries.Select(e => e.User.UID)).SendAsync(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
return otherEntries.Select(e => _clientIdentService.GetCharacterIdentForUid(e.User.UID)).Distinct().ToList();
await Task.Delay(1000).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserGetPairedClients)]
public async Task<List<ClientPairDto>> GetPairedClients()
{
string userid = AuthenticatedUserId;
var query =
from userToOther in _dbContext.ClientPairs
join otherToUser in _dbContext.ClientPairs
on new
{
user = userToOther.UserUID,
other = userToOther.OtherUserUID
await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false);
} equals new
{
user = otherToUser.OtherUserUID,
other = otherToUser.UserUID
} into leftJoin
from otherEntry in leftJoin.DefaultIfEmpty()
where
userToOther.UserUID == userid
select new
_dbContext.RemoveRange(ownPairData);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var otherPairData = await _dbContext.ClientPairs.Include(u => u.User)
.Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false);
foreach (var pair in otherPairData)
{
await Clients.User(pair.User.UID)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
userToOther.OtherUser.Alias,
userToOther.IsPaused,
OtherIsPaused = otherEntry != null && otherEntry.IsPaused,
userToOther.OtherUserUID,
IsSynced = otherEntry != null
};
return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto()
{
VanityUID = f.Alias,
IsPaused = f.IsPaused,
OtherUID = f.OtherUserUID,
IsSynced = f.IsSynced,
IsPausedFromOthers = f.OtherIsPaused
}).ToList();
OtherUID = userid,
IsRemoved = true
}, charaIdent).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)]
public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List<string> visibleCharacterIds)
foreach (var pair in groupPairs)
{
_logger.LogInformation("User {AuthenticatedUserId} pushing character data to {visibleCharacterIds} visible clients", AuthenticatedUserId, visibleCharacterIds.Count);
var user = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
var query =
from userToOther in _dbContext.ClientPairs
join otherToUser in _dbContext.ClientPairs
on new
{
user = userToOther.UserUID,
other = userToOther.OtherUserUID
} equals new
{
user = otherToUser.OtherUserUID,
other = otherToUser.UserUID
}
where
userToOther.UserUID == user.UID
&& !userToOther.IsPaused
&& !otherToUser.IsPaused
select otherToUser.UserUID;
var otherEntries = await query.ToListAsync().ConfigureAwait(false);
otherEntries =
otherEntries.Where(c => visibleCharacterIds.Select(c => c.ToLowerInvariant()).Contains(_clientIdentService.GetCharacterIdentForUid(c)?.ToLowerInvariant() ?? "")).ToList();
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, otherEntries.Count);
await GroupLeave(pair.GroupGID).ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientAddition)]
public async Task SendPairedClientAddition(string uid)
{
if (uid == AuthenticatedUserId || string.IsNullOrWhiteSpace(uid)) return;
uid = uid.Trim();
var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
var otherUser = await _dbContext.Users
.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
var existingEntry =
await _dbContext.ClientPairs.AsNoTracking()
.FirstOrDefaultAsync(p =>
p.User.UID == AuthenticatedUserId && p.OtherUser.UID == otherUser.UID).ConfigureAwait(false);
if (otherUser == null || existingEntry != null) return;
_logger.LogInformation("User {AuthenticatedUserId} adding {uid} to whitelist", AuthenticatedUserId, uid);
ClientPair wl = new ClientPair()
_dbContext.RemoveRange(otherPairData);
_dbContext.Remove(userEntry);
_dbContext.Remove(auth);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserGetOnlineCharacters)]
public async Task<List<string>> GetOnlineCharacters()
{
_logger.LogCallInfo(Api.InvokeUserGetOnlineCharacters);
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
var usersToSendOnlineTo = await SendDataToAllPairedUsers(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
return usersToSendOnlineTo.Select(async e => await _clientIdentService.GetCharacterIdentForUid(e).ConfigureAwait(false)).Select(t => t.Result).Where(t => !string.IsNullOrEmpty(t)).Distinct(System.StringComparer.Ordinal).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserGetPairedClients)]
public async Task<List<ClientPairDto>> GetPairedClients()
{
_logger.LogCallInfo(Api.InvokeUserGetPairedClients);
string userid = AuthenticatedUserId;
var query =
from userToOther in _dbContext.ClientPairs
join otherToUser in _dbContext.ClientPairs
on new
{
user = userToOther.UserUID,
other = userToOther.OtherUserUID
} equals new
{
user = otherToUser.OtherUserUID,
other = otherToUser.UserUID
} into leftJoin
from otherEntry in leftJoin.DefaultIfEmpty()
where
userToOther.UserUID == userid
select new
{
IsPaused = false,
OtherUser = otherUser,
User = user
userToOther.OtherUser.Alias,
userToOther.IsPaused,
OtherIsPaused = otherEntry != null && otherEntry.IsPaused,
userToOther.OtherUserUID,
IsSynced = otherEntry != null
};
await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var otherEntry = OppositeEntry(otherUser.UID);
await Clients.User(user.UID)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
VanityUID = otherUser.Alias,
OtherUID = otherUser.UID,
IsPaused = false,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}, string.Empty).ConfigureAwait(false);
if (otherEntry != null)
{
var userIdent = _clientIdentService.GetCharacterIdentForUid(user.UID);
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs,
new ClientPairDto()
{
VanityUID = user.Alias,
OtherUID = user.UID,
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = false,
IsSynced = true
}, userIdent).ConfigureAwait(false);
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
if (!string.IsNullOrEmpty(otherIdent))
{
await Clients.User(user.UID)
.SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
await Clients.User(otherUser.UID)
.SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
_mareMetrics.IncGauge(MetricsAPI.GaugePairs);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientPauseChange)]
public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused)
return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto()
{
if (otherUserUid == AuthenticatedUserId) return;
ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
if (pair == null) return;
VanityUID = f.Alias,
IsPaused = f.IsPaused,
OtherUID = f.OtherUserUID,
IsSynced = f.IsSynced,
IsPausedFromOthers = f.OtherIsPaused
}).ToList();
}
_logger.LogInformation("User {AuthenticatedUserId} changed pause status with {otherUserUid} to {isPaused}", AuthenticatedUserId, otherUserUid, isPaused);
pair.IsPaused = isPaused;
_dbContext.Update(pair);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var selfCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
var otherCharaIdent = _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID);
var otherEntry = OppositeEntry(otherUserUid);
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)]
public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List<string> visibleCharacterIds)
{
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count);
await Clients.User(AuthenticatedUserId)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = otherUserUid,
IsPaused = isPaused,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}, otherCharaIdent).ConfigureAwait(false);
if (otherEntry != null)
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, async f => await _clientIdentService.GetCharacterIdentForUid(f).ConfigureAwait(false), System.StringComparer.Ordinal)
.Where(f => visibleCharacterIds.Contains(f.Value.Result, System.StringComparer.Ordinal));
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count, allPairedUsersDict.Count());
await Clients.Users(allPairedUsersDict.Select(f => f.Key)).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, allPairedUsersDict.Count());
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientAddition)]
public async Task SendPairedClientAddition(string uid)
{
_logger.LogCallInfo(Api.SendUserPairedClientAddition, uid);
// don't allow adding yourself or nothing
uid = uid.Trim();
if (string.Equals(uid, AuthenticatedUserId, System.StringComparison.Ordinal) || string.IsNullOrWhiteSpace(uid)) return;
// grab other user, check if it exists and if a pair already exists
var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
var existingEntry =
await _dbContext.ClientPairs.AsNoTracking()
.FirstOrDefaultAsync(p =>
p.User.UID == AuthenticatedUserId && p.OtherUserUID == uid).ConfigureAwait(false);
if (otherUser == null || existingEntry != null) return;
// grab self create new client pair and save
var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
_logger.LogCallInfo(Api.SendUserPairedClientAddition, uid, "Success");
ClientPair wl = new ClientPair()
{
IsPaused = false,
OtherUser = otherUser,
User = user
};
await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
// get the opposite entry of the client pair
var otherEntry = OppositeEntry(otherUser.UID);
await Clients.User(user.UID)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = AuthenticatedUserId,
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = isPaused,
IsSynced = true
}, selfCharaIdent).ConfigureAwait(false);
}
VanityUID = otherUser.Alias,
OtherUID = otherUser.UID,
IsPaused = false,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}).ConfigureAwait(false);
// if there's no opposite entry do nothing
if (otherEntry == null) return;
// check if other user is online
var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUser.UID).ConfigureAwait(false);
if (otherIdent == null) return;
// send push with update to other user if other user is online
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs,
new ClientPairDto()
{
VanityUID = user.Alias,
OtherUID = user.UID,
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = false,
IsSynced = true
}).ConfigureAwait(false);
// get own ident and all pairs
var userIdent = await _clientIdentService.GetCharacterIdentForUid(user.UID).ConfigureAwait(false);
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
// if the other user has paused the main user and there was no previous group connection don't send anything
if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, System.StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection))
{
await Clients.User(user.UID)
.SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
await Clients.User(otherUser.UID)
.SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientPauseChange)]
public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused)
{
_logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused);
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
if (pair == null) return;
pair.IsPaused = isPaused;
_dbContext.Update(pair);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused, "Success");
var otherEntry = OppositeEntry(otherUserUid);
await Clients.User(AuthenticatedUserId)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = otherUserUid,
IsPaused = isPaused,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}).ConfigureAwait(false);
if (otherEntry != null)
{
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = AuthenticatedUserId,
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = isPaused,
IsSynced = true
}).ConfigureAwait(false);
var selfCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
var otherCharaIdent = await _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID).ConfigureAwait(false);
if (selfCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return;
if (isPaused)
{
_mareMetrics.IncGauge(MetricsAPI.GaugePairsPaused);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false);
await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false);
}
else
{
_mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false);
await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false);
}
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientRemoval)]
public async Task SendPairedClientRemoval(string uid)
{
if (uid == AuthenticatedUserId) return;
var sender = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
if (otherUser == null) return;
_logger.LogInformation("User {AuthenticatedUserId} removed {uid} from whitelist", AuthenticatedUserId, uid);
ClientPair wl =
await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.User == sender && w.OtherUser == otherUser).ConfigureAwait(false);
if (wl == null) return;
_dbContext.ClientPairs.Remove(wl);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
var otherEntry = OppositeEntry(uid);
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
await Clients.User(sender.UID)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = otherUser.UID,
IsRemoved = true
}, otherIdent).ConfigureAwait(false);
if (otherEntry != null)
{
if (!string.IsNullOrEmpty(otherIdent))
{
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
await Clients.User(sender.UID)
.SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
await Clients.User(otherUser.UID)
.SendAsync(Api.OnUserRemoveOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = sender.UID,
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = false,
IsSynced = false
}, ownIdent).ConfigureAwait(false);
}
}
_mareMetrics.DecGauge(MetricsAPI.GaugePairs);
}
private ClientPair OppositeEntry(string otherUID) =>
_dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientRemoval)]
public async Task SendPairedClientRemoval(string otherUserUid)
{
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid);
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
// check if client pair even exists
ClientPair callerPair =
await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
bool callerHadPaused = callerPair.IsPaused;
if (callerPair == null) return;
// delete from database, send update info to users pair list
_dbContext.ClientPairs.Remove(callerPair);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid, "Success");
await Clients.User(AuthenticatedUserId)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = otherUserUid,
IsRemoved = true
}).ConfigureAwait(false);
// check if opposite entry exists
var oppositeClientPair = OppositeEntry(otherUserUid);
if (oppositeClientPair == null) return;
// check if other user is online, if no then there is no need to do anything further
var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUserUid).ConfigureAwait(false);
if (otherIdent == null) return;
// get own ident and
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs,
new ClientPairDto()
{
OtherUID = AuthenticatedUserId,
IsPausedFromOthers = false,
IsSynced = false
}).ConfigureAwait(false);
// if the other user had paused the user the state will be offline for either, do nothing
bool otherHadPaused = oppositeClientPair.IsPaused;
if (!callerHadPaused && otherHadPaused) return;
var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, otherUserUid, System.StringComparison.Ordinal));
var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection;
// if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing
if (!callerHadPaused && !otherHadPaused && !isPausedInGroup) return;
// if neither user had paused each other and either is not in an unpaused group with each other, change state to offline
if (!callerHadPaused && !otherHadPaused && isPausedInGroup)
{
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
// if the caller had paused other but not the other has paused the caller and they are in an unpaused group together, change state to online
if (callerHadPaused && !otherHadPaused && !isPausedInGroup)
{
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
}
}
private ClientPair OppositeEntry(string otherUID) =>
_dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId);
}

View File

@@ -4,6 +4,7 @@ using System.Security.Claims;
using System.Threading.Tasks;
using MareSynchronos.API;
using MareSynchronosServer.Services;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Authentication;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
@@ -27,9 +28,14 @@ public partial class MareHub : Hub
private readonly SystemInfoService _systemInfoService;
private readonly IHttpContextAccessor _contextAccessor;
private readonly IClientIdentificationService _clientIdentService;
private readonly ILogger<MareHub> _logger;
private readonly MareHubLogger _logger;
private readonly MareDbContext _dbContext;
private readonly Uri _cdnFullUri;
private readonly string _shardName;
private readonly int _maxExistingGroupsByUser;
private readonly int _maxJoinedGroupsByUser;
private readonly int _maxGroupUserCount;
public MareHub(MareMetrics mareMetrics, AuthService.AuthServiceClient authServiceClient, FileService.FileServiceClient fileServiceClient,
MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor,
IClientIdentificationService clientIdentService)
@@ -38,10 +44,15 @@ public partial class MareHub : Hub
_authServiceClient = authServiceClient;
_fileServiceClient = fileServiceClient;
_systemInfoService = systemInfoService;
_cdnFullUri = new Uri(configuration.GetRequiredSection("MareSynchronos").GetValue<string>("CdnFullUrl"));
var config = configuration.GetRequiredSection("MareSynchronos");
_cdnFullUri = new Uri(config.GetValue<string>("CdnFullUrl"));
_shardName = config.GetValue("ShardName", "Main");
_maxExistingGroupsByUser = config.GetValue<int>("MaxExistingGroupsByUser", 3);
_maxJoinedGroupsByUser = config.GetValue<int>("MaxJoinedGroupsByUser", 6);
_maxGroupUserCount = config.GetValue<int>("MaxGroupUserCount", 100);
_contextAccessor = contextAccessor;
_clientIdentService = clientIdentService;
_logger = logger;
_logger = new MareHubLogger(this, logger);
_dbContext = mareDbContext;
}
@@ -51,9 +62,9 @@ public partial class MareHub : Hub
{
_mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections);
var userId = Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
var userId = Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value;
_logger.LogInformation("Connection from {userId}, CI: {characterIdentification}", userId, characterIdentification);
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification);
await Clients.Caller.SendAsync(Api.OnUpdateSystemInfo, _systemInfoService.SystemInfoDto).ConfigureAwait(false);
@@ -62,9 +73,11 @@ public partial class MareHub : Hub
if (!string.IsNullOrEmpty(userId) && !isBanned && !string.IsNullOrEmpty(characterIdentification))
{
var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId).ConfigureAwait(false));
var existingIdent = _clientIdentService.GetCharacterIdentForUid(userId);
if (!string.IsNullOrEmpty(existingIdent) && characterIdentification != existingIdent)
var existingIdent = await _clientIdentService.GetCharacterIdentForUid(userId).ConfigureAwait(false);
if (!string.IsNullOrEmpty(existingIdent) && !string.Equals(characterIdentification, existingIdent, StringComparison.Ordinal))
{
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure", "LoggedIn");
return new ConnectionDto()
{
ServerVersion = Api.Version
@@ -72,27 +85,50 @@ public partial class MareHub : Hub
}
user.LastLoggedIn = DateTime.UtcNow;
_clientIdentService.MarkUserOnline(user.UID, characterIdentification);
await _clientIdentService.MarkUserOnline(user.UID, characterIdentification).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Success");
return new ConnectionDto
{
ServerVersion = Api.Version,
UID = string.IsNullOrEmpty(user.Alias) ? user.UID : user.Alias,
IsModerator = user.IsModerator,
IsAdmin = user.IsAdmin
IsAdmin = user.IsAdmin,
ServerInfo = new ServerInfoDto()
{
MaxGroupsCreatedByUser = _maxExistingGroupsByUser,
ShardName = _shardName,
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
MaxGroupUserCount = _maxGroupUserCount
}
};
}
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure");
return new ConnectionDto()
{
ServerVersion = Api.Version
};
}
[HubMethodName(Api.InvokeCheckClientHealth)]
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
public async Task<bool> CheckClientHealth()
{
var needsReconnect = string.IsNullOrEmpty(await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false));
if (needsReconnect)
{
_logger.LogCallWarning(Api.InvokeCheckClientHealth, needsReconnect);
}
return needsReconnect;
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Connection from {ip}", _contextAccessor.GetIpAddress());
_logger.LogCallInfo("Connect", _contextAccessor.GetIpAddress());
_mareMetrics.IncGauge(MetricsAPI.GaugeConnections);
await base.OnConnectedAsync().ConfigureAwait(false);
}
@@ -101,50 +137,22 @@ public partial class MareHub : Hub
{
_mareMetrics.DecGauge(MetricsAPI.GaugeConnections);
var userCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
var userCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
if (!string.IsNullOrEmpty(userCharaIdent))
{
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == AuthenticatedUserId)!.ConfigureAwait(false);
_mareMetrics.DecGauge(MetricsAPI.GaugeAuthorizedConnections);
_logger.LogInformation("Disconnect from {id}", AuthenticatedUserId);
_logger.LogCallInfo("Disconnect");
var query =
from userToOther in _dbContext.ClientPairs
join otherToUser in _dbContext.ClientPairs
on new
{
user = userToOther.UserUID,
other = userToOther.OtherUserUID
await SendDataToAllPairedUsers(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false);
} equals new
{
user = otherToUser.OtherUserUID,
other = otherToUser.UserUID
}
where
userToOther.UserUID == user.UID
&& !userToOther.IsPaused
&& !otherToUser.IsPaused
select otherToUser.UserUID;
var otherEntries = await query.ToListAsync().ConfigureAwait(false);
_dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == AuthenticatedUserId));
await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false);
_dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == user.UID));
_clientIdentService.MarkUserOffline(user.UID);
await _clientIdentService.MarkUserOffline(AuthenticatedUserId).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
protected string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown";
protected async Task<User> GetAuthenticatedUserUntrackedAsync()
{
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
}
}
}

View File

@@ -26,6 +26,10 @@
<PackageReference Include="Grpc.Net.Client" Version="2.47.0" />
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.3.1" />
<PackageReference Include="lz4net" Version="1.0.15.93" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.733">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.8" />

View File

@@ -9,58 +9,57 @@ using Microsoft.Extensions.Logging;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
namespace MareSynchronosServer
namespace MareSynchronosServer;
public class Program
{
public class Program
public static void Main(string[] args)
{
public static void Main(string[] args)
var hostBuilder = CreateHostBuilder(args);
var host = hostBuilder.Build();
using (var scope = host.Services.CreateScope())
{
var hostBuilder = CreateHostBuilder(args);
var host = hostBuilder.Build();
using (var scope = host.Services.CreateScope())
var services = scope.ServiceProvider;
using var context = services.GetRequiredService<MareDbContext>();
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
{
var services = scope.ServiceProvider;
using var context = services.GetRequiredService<MareDbContext>();
context.Database.Migrate();
context.SaveChanges();
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0")
{
context.Database.Migrate();
context.SaveChanges();
// clean up residuals
var looseFiles = context.Files.Where(f => f.Uploaded == false);
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
context.RemoveRange(unfinishedRegistrations);
context.RemoveRange(looseFiles);
context.SaveChanges();
}
var metrics = services.GetRequiredService<MareMetrics>();
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count());
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
// clean up residuals
var looseFiles = context.Files.Where(f => f.Uploaded == false);
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
context.RemoveRange(unfinishedRegistrations);
context.RemoveRange(looseFiles);
context.SaveChanges();
}
if (args.Length == 0 || args[0] != "dry")
{
host.Run();
}
var metrics = services.GetRequiredService<MareMetrics>();
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count());
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.UseConsoleLifetime()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseContentRoot(AppContext.BaseDirectory);
webBuilder.ConfigureLogging((ctx, builder) =>
{
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
});
webBuilder.UseStartup<Startup>();
});
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
{
host.Run();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.UseConsoleLifetime()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseContentRoot(AppContext.BaseDirectory);
webBuilder.ConfigureLogging((ctx, builder) =>
{
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
});
webBuilder.UseStartup<Startup>();
});
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MareSynchronos.API;
@@ -7,6 +8,7 @@ using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -16,16 +18,19 @@ namespace MareSynchronosServer.Services;
public class SystemInfoService : IHostedService, IDisposable
{
private readonly MareMetrics _mareMetrics;
private readonly IClientIdentificationService clientIdentService;
private readonly IServiceProvider _services;
private readonly IClientIdentificationService _clientIdentService;
private readonly ILogger<SystemInfoService> _logger;
private readonly IHubContext<MareHub> _hubContext;
private Timer _timer;
private string _shardName;
public SystemInfoDto SystemInfoDto { get; private set; } = new();
public SystemInfoService(MareMetrics mareMetrics, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> hubContext)
public SystemInfoService(MareMetrics mareMetrics, IConfiguration configuration, IServiceProvider services, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> hubContext)
{
_mareMetrics = mareMetrics;
this.clientIdentService = clientIdentService;
_services = services;
_clientIdentService = clientIdentService;
_logger = logger;
_hubContext = hubContext;
}
@@ -42,27 +47,28 @@ public class SystemInfoService : IHostedService, IDisposable
private void PushSystemInfo(object state)
{
ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads);
_logger.LogInformation("ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available", workerThreads, ioThreads);
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0")
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
{
SystemInfoDto = new SystemInfoDto()
{
CacheUsage = 0,
CpuUsage = 0,
RAMUsage = 0,
NetworkIn = 0,
NetworkOut = 0,
OnlineUsers = clientIdentService.GetOnlineUsers(),
UploadedFiles = 0
OnlineUsers = _clientIdentService.GetOnlineUsers().Result,
};
_hubContext.Clients.All.SendAsync(Api.OnUpdateSystemInfo, SystemInfoDto);
using var scope = _services.CreateScope();
using var db = scope.ServiceProvider.GetService<MareDbContext>()!;
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.Count());
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.ClientPairs.Count(p => p.IsPaused));
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.Count());
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.Count());
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairsPaused, db.GroupPairs.Count(p => p.IsPaused));
}
}

View File

@@ -22,169 +22,170 @@ using System.Collections.Generic;
using MareSynchronosServer.Services;
using MareSynchronosShared.Services;
using System.Net.Http;
using MareSynchronosServer.Utils;
namespace MareSynchronosServer
namespace MareSynchronosServer;
public class Startup
{
public class Startup
public Startup(IConfiguration configuration)
{
public Startup(IConfiguration configuration)
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
services.AddMemoryCache();
services.AddInMemoryRateLimiting();
services.AddSingleton<SystemInfoService, SystemInfoService>();
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
services.AddTransient(_ => Configuration);
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
var defaultMethodConfig = new MethodConfig
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
services.AddMemoryCache();
services.AddInMemoryRateLimiting();
services.AddSingleton<SystemInfoService, SystemInfoService>();
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
services.AddTransient(_ => Configuration);
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
var defaultMethodConfig = new MethodConfig
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 100,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
}
MaxAttempts = 100,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
}
};
services.AddSingleton(new MareMetrics(new List<string>
{
MetricsAPI.CounterInitializedConnections,
MetricsAPI.CounterUserPushData,
MetricsAPI.CounterUserPushDataTo,
MetricsAPI.CounterUsersRegisteredDeleted,
}, new List<string>
{
MetricsAPI.GaugeAuthorizedConnections,
MetricsAPI.GaugeConnections,
MetricsAPI.GaugePairs,
MetricsAPI.GaugePairsPaused,
MetricsAPI.GaugeAvailableIOWorkerThreads,
MetricsAPI.GaugeAvailableWorkerThreads
}));
services.AddGrpcClient<AuthService.AuthServiceClient>(c =>
{
c.Address = new Uri(mareConfig.GetValue<string>("ServiceAddress"));
}).ConfigureChannel(c =>
{
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
c.HttpHandler = new SocketsHttpHandler()
{
EnableMultipleHttp2Connections = true
};
services.AddSingleton(new MareMetrics(new List<string>
{
MetricsAPI.CounterInitializedConnections,
MetricsAPI.CounterUserPushData,
MetricsAPI.CounterUserPushDataTo,
MetricsAPI.CounterUsersRegisteredDeleted,
}, new List<string>
{
MetricsAPI.GaugeAuthorizedConnections,
MetricsAPI.GaugeConnections,
MetricsAPI.GaugePairs,
MetricsAPI.GaugePairsPaused,
MetricsAPI.GaugeAvailableIOWorkerThreads,
MetricsAPI.GaugeAvailableWorkerThreads
}));
services.AddGrpcClient<AuthService.AuthServiceClient>(c =>
{
c.Address = new Uri(mareConfig.GetValue<string>("ServiceAddress"));
}).ConfigureChannel(c =>
{
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
c.HttpHandler = new SocketsHttpHandler()
{
EnableMultipleHttp2Connections = true
};
});
services.AddGrpcClient<FileService.FileServiceClient>(c =>
{
c.Address = new Uri(mareConfig.GetValue<string>("StaticFileServiceAddress"));
}).ConfigureChannel(c =>
{
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
});
services.AddDbContextPool<MareDbContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
}, mareConfig.GetValue("DbContextPoolSize", 1024));
services.AddAuthentication(options =>
{
options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme;
}).AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { });
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
{
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
hubOptions.EnableDetailedErrors = true;
hubOptions.MaximumParallelInvocationsPerClient = 10;
hubOptions.StreamBufferCapacity = 200;
hubOptions.AddFilter<SignalRLimitFilter>();
});
// add redis related options
var redis = mareConfig.GetValue("RedisConnectionString", string.Empty);
if (!string.IsNullOrEmpty(redis))
{
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
{
options.Configuration.ChannelPrefix = "MareSynchronos";
});
services.AddStackExchangeRedisCache(opt =>
{
opt.Configuration = redis;
opt.InstanceName = "MareSynchronosCache:";
});
services.AddSingleton<IClientIdentificationService, DistributedClientIdentificationService>();
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
}
else
{
services.AddSingleton<IClientIdentificationService, LocalClientIdentificationService>();
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
}
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
services.AddGrpcClient<FileService.FileServiceClient>(c =>
{
if (env.IsDevelopment())
c.Address = new Uri(mareConfig.GetValue<string>("StaticFileServiceAddress"));
}).ConfigureChannel(c =>
{
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
});
services.AddDbContextPool<MareDbContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
}, mareConfig.GetValue("DbContextPoolSize", 1024));
services.AddAuthentication(options =>
{
options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme;
}).AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { });
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
{
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
hubOptions.EnableDetailedErrors = true;
hubOptions.MaximumParallelInvocationsPerClient = 10;
hubOptions.StreamBufferCapacity = 200;
hubOptions.AddFilter<SignalRLimitFilter>();
});
// add redis related options
var redis = mareConfig.GetValue("RedisConnectionString", string.Empty);
if (!string.IsNullOrEmpty(redis))
{
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseIpRateLimiting();
app.UseRouting();
app.UseWebSockets();
var metricServer = new KestrelMetricServer(4980);
metricServer.Start();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MareHub>(Api.Path, options =>
{
options.ApplicationMaxBufferSize = 5242880;
options.TransportMaxBufferSize = 5242880;
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
});
options.Configuration.ChannelPrefix = "MareSynchronos";
});
services.AddStackExchangeRedisCache(opt =>
{
opt.Configuration = redis;
opt.InstanceName = "MareSynchronosCache:";
});
services.AddSingleton<IClientIdentificationService, DistributedClientIdentificationService>();
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
}
else
{
services.AddSingleton<IClientIdentificationService, LocalClientIdentificationService>();
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
}
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseIpRateLimiting();
app.UseRouting();
app.UseWebSockets();
var metricServer = new KestrelMetricServer(4980);
metricServer.Start();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MareHub>(Api.Path, options =>
{
options.ApplicationMaxBufferSize = 5242880;
options.TransportMaxBufferSize = 5242880;
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
});
});
}
}

View File

@@ -0,0 +1,13 @@
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
namespace MareSynchronosServer.Utils;
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext context)
{
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value;
}
}

View File

@@ -0,0 +1,28 @@
using MareSynchronosServer.Hubs;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Utils;
public class MareHubLogger
{
private readonly MareHub _hub;
private readonly ILogger<MareHub> _logger;
public MareHubLogger(MareHub hub, ILogger<MareHub> logger)
{
_hub = hub;
_logger = logger;
}
public void LogCallInfo(string methodName, params object[] args)
{
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
_logger.LogInformation("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
}
public void LogCallWarning(string methodName, params object[] args)
{
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
_logger.LogWarning("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
}
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronosServer.Utils;
public enum PauseInfo
{
NoConnection,
Paused,
Unpaused
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronosServer.Utils;
public record PauseState
{
public string GID { get; set; }
public bool IsPaused { get; set; }
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MareSynchronosServer.Utils;
public record PausedEntry
{
public string UID { get; set; }
public List<PauseState> PauseStates { get; set; } = new();
public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection
: PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection
: PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused;
private IEnumerable<PauseState> PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null);
private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null);
public bool IsPaused
{
get
{
var isDirectlyPaused = IsDirectlyPaused;
bool result;
if (isDirectlyPaused != PauseInfo.NoConnection)
{
result = isDirectlyPaused == PauseInfo.Paused;
}
else
{
result = IsPausedPerGroup == PauseInfo.Paused;
}
return result;
}
}
public PauseInfo IsPausedForSpecificGroup(string gid)
{
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
if (state == null) return PauseInfo.NoConnection;
return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection;
}
public PauseInfo IsPausedExcludingGroup(string gid)
{
var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList();
if (!states.Any()) return PauseInfo.NoConnection;
var result = states.All(p => p.IsPaused);
if (result) return PauseInfo.Paused;
return PauseInfo.Unpaused;
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace MareSynchronosServer.Hubs;
namespace MareSynchronosServer.Utils;
public class SignalRLimitFilter : IHubFilter
{
private readonly IRateLimitProcessor _processor;
@@ -42,7 +42,7 @@ public class SignalRLimitFilter : IHubFilter
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
if (counter.Count > rule.Limit)
{
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown";
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown";
var retry = counter.Timestamp.RetryAfterFrom(rule);
logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName);
throw new HubException($"call limit {retry}");

View File

@@ -0,0 +1,12 @@
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
private record UserPair
{
public string UserUID { get; set; }
public string OtherUserUID { get; set; }
public bool UserPausedOther { get; set; }
public bool OtherPausedUser { get; set; }
}
}