Mare 0.9 (#65)
* add jwt expiry * start of 0.9 api impl * some stuff idk * some more impl * some cleanup * remove grouppair, add configuration, rework some pair drawing stuff * do some stuff * rework some ui * I don't even know anymore * add cancellationtoken * token bla * ui fixes etc * probably individual adding/removing now working fully as expected * add working report popup * I guess it's more syncshell shit or so * popup shit idk * work out most of the syncshell bullshit I guess * delete some old crap * are we actually getting closer to the end * update pair info stuff * more fixes/adjustments, idk * refactor some things * some rework * some more cleanup * cleanup * make menu buttons w i d e * better icon text buttons * add all syncshell folder and ordering fixes --------- Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
This commit is contained in:
@@ -16,8 +16,8 @@ namespace MareSynchronos.WebAPI.Files;
|
||||
public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly FileTransferOrchestrator _orchestrator;
|
||||
|
||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
|
||||
@@ -30,12 +30,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
_fileCompactor = fileCompactor;
|
||||
}
|
||||
|
||||
public List<DownloadFileTransfer> CurrentDownloads { get; private set; } = new();
|
||||
public List<DownloadFileTransfer> CurrentDownloads { get; private set; } = [];
|
||||
|
||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||
|
||||
public bool IsDownloading => !CurrentDownloads.Any();
|
||||
|
||||
public static void MungeBuffer(Span<byte> buffer)
|
||||
{
|
||||
for (int i = 0; i < buffer.Length; ++i)
|
||||
{
|
||||
buffer[i] ^= 42;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelDownload()
|
||||
{
|
||||
CurrentDownloads.Clear();
|
||||
@@ -66,14 +74,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public static void MungeBuffer(Span<byte> buffer)
|
||||
{
|
||||
for (int i = 0; i < buffer.Length; ++i)
|
||||
{
|
||||
buffer[i] ^= 42;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte MungeByte(int byteOrEof)
|
||||
{
|
||||
if (byteOrEof == -1)
|
||||
@@ -86,8 +86,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private static (string fileHash, long fileLengthBytes) ReadBlockFileHeader(FileStream fileBlockStream)
|
||||
{
|
||||
List<char> hashName = new();
|
||||
List<char> fileLength = new();
|
||||
List<char> hashName = [];
|
||||
List<char> fileLength = [];
|
||||
var separator = (char)MungeByte(fileBlockStream.ReadByte());
|
||||
if (separator != '#') throw new InvalidDataException("Data is invalid, first char is not #");
|
||||
|
||||
@@ -177,8 +177,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
Logger.LogDebug("Download start: {id}", gameObjectHandler.Name);
|
||||
|
||||
List<DownloadFileDto> downloadFileInfoFromService = new();
|
||||
downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false));
|
||||
List<DownloadFileDto> downloadFileInfoFromService =
|
||||
[
|
||||
.. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false),
|
||||
];
|
||||
|
||||
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||
|
||||
@@ -219,9 +221,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
// let server predownload files
|
||||
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
|
||||
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
|
||||
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri, requestIdResponse.Content.ReadAsStringAsync().Result);
|
||||
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri,
|
||||
await requestIdResponse.Content.ReadAsStringAsync(token).ConfigureAwait(false));
|
||||
|
||||
Guid requestId = Guid.Parse(requestIdResponse.Content.ReadAsStringAsync().Result.Trim('"'));
|
||||
Guid requestId = Guid.Parse((await requestIdResponse.Content.ReadAsStringAsync().ConfigureAwait(false)).Trim('"'));
|
||||
|
||||
Logger.LogDebug("GUID {requestId} for {n} files on server {uri}", requestId, fileGroup.Count(), fileGroup.First().DownloadUri);
|
||||
|
||||
@@ -235,15 +238,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_downloadStatus.ContainsKey(fileGroup.Key)) return;
|
||||
_downloadStatus[fileGroup.Key].TransferredBytes += bytesDownloaded;
|
||||
if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return;
|
||||
value.TransferredBytes += bytesDownloaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not set download progress");
|
||||
}
|
||||
});
|
||||
await DownloadAndMungeFileHttpClient(fileGroup.Key, requestId, fileGroup.ToList(), blockFile, progress, token).ConfigureAwait(false);
|
||||
await DownloadAndMungeFileHttpClient(fileGroup.Key, requestId, [.. fileGroup], blockFile, progress, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -275,7 +278,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Found file {file} with length {le}, decompressing download", fileHash, fileLengthBytes);
|
||||
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".").Last();
|
||||
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1];
|
||||
|
||||
byte[] compressedFileContent = new byte[fileLengthBytes];
|
||||
_ = await fileBlockStream.ReadAsync(compressedFileContent, token).ConfigureAwait(false);
|
||||
@@ -300,7 +303,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
finally
|
||||
{
|
||||
_orchestrator.ReleaseDownloadSlot();
|
||||
fileBlockStream?.Dispose();
|
||||
if (fileBlockStream != null)
|
||||
await fileBlockStream.DisposeAsync().ConfigureAwait(false);
|
||||
File.Delete(blockFile);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
@@ -314,7 +318,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
|
||||
var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), hashes, ct).ConfigureAwait(false);
|
||||
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<DownloadFileDto>();
|
||||
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
private void PersistFileToStorage(string fileHash, string filePath)
|
||||
@@ -322,7 +326,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
var fi = new FileInfo(filePath);
|
||||
Func<DateTime> RandomDayInThePast()
|
||||
{
|
||||
DateTime start = new(1995, 1, 1);
|
||||
DateTime start = new(1995, 1, 1, 1, 1, 1, DateTimeKind.Local);
|
||||
Random gen = new();
|
||||
int range = (DateTime.Today - start).Days;
|
||||
return () => start.AddDays(gen.Next(range));
|
||||
@@ -334,9 +338,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateCacheEntry(filePath);
|
||||
if (!string.Equals(entry?.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry?.Hash, fileHash);
|
||||
Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry.Hash, fileHash);
|
||||
File.Delete(filePath);
|
||||
_fileDbManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.WebAPI.Files.Models;
|
||||
using MareSynchronos.WebAPI.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Headers;
|
||||
@@ -12,20 +12,23 @@ namespace MareSynchronos.WebAPI.Files;
|
||||
|
||||
public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly object _semaphoreModificationLock = new();
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private int _availableDownloadSlots;
|
||||
private SemaphoreSlim _downloadSemaphore;
|
||||
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
|
||||
|
||||
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, MareConfigService mareConfig, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
|
||||
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, MareConfigService mareConfig,
|
||||
MareMediator mediator, TokenProvider tokenProvider) : base(logger, mediator)
|
||||
{
|
||||
_mareConfig = mareConfig;
|
||||
_serverManager = serverManager;
|
||||
_httpClient = new();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(3000);
|
||||
_tokenProvider = tokenProvider;
|
||||
_httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(3000)
|
||||
};
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
|
||||
@@ -48,12 +51,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
public Uri? FilesCdnUri { private set; get; }
|
||||
public List<FileTransfer> ForbiddenTransfers { get; } = new();
|
||||
public List<FileTransfer> ForbiddenTransfers { get; } = [];
|
||||
public bool IsInitialized => FilesCdnUri != null;
|
||||
|
||||
public void ReleaseDownloadSlot()
|
||||
public void ClearDownloadRequest(Guid guid)
|
||||
{
|
||||
_downloadSemaphore.Release();
|
||||
_downloadReady.Remove(guid, out _);
|
||||
}
|
||||
|
||||
public bool IsDownloadReady(Guid guid)
|
||||
@@ -66,12 +69,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearDownloadRequest(Guid guid)
|
||||
public void ReleaseDownloadSlot()
|
||||
{
|
||||
_downloadReady.Remove(guid, out _);
|
||||
_downloadSemaphore.Release();
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
||||
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||
@@ -109,10 +112,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverManager.GetToken());
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetOrUpdateToken(ct!.Value).ConfigureAwait(false));
|
||||
|
||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
public List<FileTransfer> CurrentUploads { get; } = new();
|
||||
public List<FileTransfer> CurrentUploads { get; } = [];
|
||||
public bool IsUploading => CurrentUploads.Count > 0;
|
||||
|
||||
public bool CancelUpload()
|
||||
@@ -81,7 +81,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
foreach (var kvp in data.FileReplacements)
|
||||
{
|
||||
data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
|
||||
data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -102,7 +102,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
UIDs = uids
|
||||
};
|
||||
var response = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesFilesSendFullPath(_orchestrator.FilesCdnUri!), filesSendDto, ct).ConfigureAwait(false);
|
||||
return await response.Content.ReadFromJsonAsync<List<UploadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<UploadFileDto>();
|
||||
return await response.Content.ReadFromJsonAsync<List<UploadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
private HashSet<string> GetUnverifiedFiles(CharacterData data)
|
||||
@@ -152,7 +152,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
if (!_mareConfigService.Current.UseAlternativeFileUpload && ex is not OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning(ex, "[{hash}] Error during file upload, trying alternative file upload", fileHash);
|
||||
await UploadFileStream(compressedFile, fileHash, true, uploadToken).ConfigureAwait(false);
|
||||
await UploadFileStream(compressedFile, fileHash, munged: true, uploadToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,7 +196,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count);
|
||||
var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList(), visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false);
|
||||
var filesToUpload = await FilesSend([.. unverifiedUploadHashes], visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var file in filesToUpload.Where(f => !f.IsForbidden))
|
||||
{
|
||||
@@ -215,7 +215,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
foreach (var file in filesToUpload.Where(c => c.IsForbidden))
|
||||
{
|
||||
if (_orchestrator.ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
|
||||
if (_orchestrator.ForbiddenTransfers.TrueForAll(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
|
||||
{
|
||||
_orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file)
|
||||
{
|
||||
@@ -248,7 +248,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
||||
}
|
||||
|
||||
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
|
||||
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
|
||||
{
|
||||
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public class DownloadFileTransfer : FileTransfer
|
||||
{
|
||||
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
||||
public DownloadFileTransfer(DownloadFileDto dto) : base(dto) { }
|
||||
public DownloadFileTransfer(DownloadFileDto dto) : base(dto)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool CanBeTransferred => Dto.FileExists && !Dto.IsForbidden && Dto.Size > 0;
|
||||
public Uri DownloadUri => new(Dto.Url);
|
||||
public override long Total
|
||||
{
|
||||
@@ -15,6 +18,5 @@ public class DownloadFileTransfer : FileTransfer
|
||||
}
|
||||
get => Dto.Size;
|
||||
}
|
||||
|
||||
public override bool CanBeTransferred => Dto.FileExists && !Dto.IsForbidden && Dto.Size > 0;
|
||||
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
||||
}
|
||||
@@ -7,4 +7,4 @@ public enum DownloadStatus
|
||||
WaitingForQueue,
|
||||
Downloading,
|
||||
Decompressing
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
public class FileDownloadStatus
|
||||
{
|
||||
public DownloadStatus DownloadStatus { get; set; }
|
||||
public int TotalFiles { get; set; }
|
||||
public int TransferredFiles { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public int TotalFiles { get; set; }
|
||||
public long TransferredBytes { get; set; }
|
||||
}
|
||||
public int TransferredFiles { get; set; }
|
||||
}
|
||||
@@ -11,17 +11,17 @@ public abstract class FileTransfer
|
||||
TransferDto = transferDto;
|
||||
}
|
||||
|
||||
public virtual bool CanBeTransferred => !TransferDto.IsForbidden && (TransferDto is not DownloadFileDto dto || dto.FileExists);
|
||||
public string ForbiddenBy => TransferDto.ForbiddenBy;
|
||||
public long Transferred { get; set; } = 0;
|
||||
public abstract long Total { get; set; }
|
||||
public string Hash => TransferDto.Hash;
|
||||
public bool IsForbidden => TransferDto.IsForbidden;
|
||||
public bool IsInTransfer => Transferred != Total && Transferred > 0;
|
||||
public bool IsTransferred => Transferred == Total;
|
||||
public virtual bool CanBeTransferred => !TransferDto.IsForbidden && (TransferDto is not DownloadFileDto dto || dto.FileExists);
|
||||
public bool IsForbidden => TransferDto.IsForbidden;
|
||||
public abstract long Total { get; set; }
|
||||
public long Transferred { get; set; } = 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@ namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public class UploadFileTransfer : FileTransfer
|
||||
{
|
||||
public UploadFileTransfer(UploadFileDto dto) : base(dto) { }
|
||||
public override long Total { get; set; }
|
||||
public UploadFileTransfer(UploadFileDto dto) : base(dto)
|
||||
{
|
||||
}
|
||||
|
||||
public string LocalFile { get; set; } = string.Empty;
|
||||
}
|
||||
public override long Total { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace MareSynchronos.WebAPI.Files.Models;
|
||||
|
||||
public record UploadProgress(long Uploaded, long Size);
|
||||
public record UploadProgress(long Uploaded, long Size);
|
||||
@@ -1,4 +1,5 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -6,6 +7,7 @@ using System.Text;
|
||||
|
||||
namespace MareSynchronos.WebAPI;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
public partial class ApiController
|
||||
{
|
||||
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
|
||||
@@ -14,7 +16,7 @@ public partial class ApiController
|
||||
|
||||
try
|
||||
{
|
||||
await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false);
|
||||
await PushCharacterDataInternal(data, [.. visibleCharacters]).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -44,14 +46,14 @@ public partial class ApiController
|
||||
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<UserPairDto>> UserGetPairedClients()
|
||||
public async Task<List<UserFullPairDto>> UserGetPairedClients()
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<List<UserPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
|
||||
return await _mareHub!.InvokeAsync<List<UserFullPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
||||
{
|
||||
if (!IsConnected) return new UserProfileDto(dto.User, false, null, null, null);
|
||||
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null);
|
||||
return await _mareHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -90,6 +92,12 @@ public partial class ApiController
|
||||
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.InvokeAsync(nameof(UserUpdateDefaultPermissions), defaultPermissionsDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
||||
{
|
||||
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||
@@ -105,4 +113,5 @@ public partial class ApiController
|
||||
Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString());
|
||||
await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
||||
@@ -25,6 +25,13 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_GroupChangeUserPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateGroupPairPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupDelete(GroupDto groupDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupDelete: {dto}", groupDto);
|
||||
@@ -32,17 +39,6 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairChangePermissions: {perm}", permissionDto);
|
||||
ExecuteSafely(() =>
|
||||
{
|
||||
if (string.Equals(permissionDto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(permissionDto);
|
||||
else _pairManager.SetGroupPairUserPermissions(permissionDto);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo);
|
||||
@@ -113,10 +109,17 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UpdateUserIndividualPairStatusDto: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateIndividualPairStatus(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserAddClientPair(UserPairDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserAddClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.AddUserPair(dto));
|
||||
ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -155,6 +158,13 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateDefaultPermissions: {dto}", dto);
|
||||
_connectionDto!.DefaultPreferredPermissions = dto;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto);
|
||||
@@ -188,18 +198,18 @@ public partial class ApiController
|
||||
_mareHub!.On(nameof(Client_GroupChangePermissions), act);
|
||||
}
|
||||
|
||||
public void OnGroupChangeUserPairPermissions(Action<GroupPairUserPermissionDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupChangeUserPairPermissions), act);
|
||||
}
|
||||
|
||||
public void OnGroupDelete(Action<GroupDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupDelete), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupPairChangePermissions), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairChangeUserInfo(Action<GroupPairUserInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
@@ -242,12 +252,24 @@ public partial class ApiController
|
||||
_mareHub!.On(nameof(Client_UpdateSystemInfo), act);
|
||||
}
|
||||
|
||||
public void OnUpdateUserIndividualPairStatusDto(Action<UserIndividualPairStatusDto> action)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UpdateUserIndividualPairStatusDto), action);
|
||||
}
|
||||
|
||||
public void OnUserAddClientPair(Action<UserPairDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserAddClientPair), act);
|
||||
}
|
||||
|
||||
public void OnUserDefaultPermissionUpdate(Action<DefaultPermissionsDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserUpdateDefaultPermissions), act);
|
||||
}
|
||||
|
||||
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
@@ -42,10 +42,10 @@ public partial class ApiController
|
||||
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<GroupPasswordDto> GroupCreate()
|
||||
public async Task<GroupJoinDto> GroupCreate()
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false);
|
||||
return await _mareHub!.InvokeAsync<GroupJoinDto>(nameof(GroupCreate)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
||||
@@ -66,10 +66,16 @@ public partial class ApiController
|
||||
return await _mareHub!.InvokeAsync<List<BannedGroupUserDto>>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GroupJoin(GroupPasswordDto passwordedGroup)
|
||||
public async Task<GroupJoinInfoDto> GroupJoin(GroupPasswordDto passwordedGroup)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false);
|
||||
return await _mareHub!.InvokeAsync<GroupJoinInfoDto>(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GroupJoinFinalize(GroupJoinDto passwordedGroup)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupLeave(GroupDto group)
|
||||
@@ -96,12 +102,6 @@ public partial class ApiController
|
||||
return await _mareHub!.InvokeAsync<List<GroupFullInfoDto>>(nameof(GroupsGetAll)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<List<GroupPairFullInfoDto>>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupUnbanUser(GroupPairDto groupPair)
|
||||
{
|
||||
CheckConnection();
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
using MareSynchronos.API.Routes;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Dalamud.Utility;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using Dalamud.Utility;
|
||||
using System.Reflection;
|
||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||
using MareSynchronos.WebAPI.SignalR;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Data;
|
||||
using System.Net.Http.Headers;
|
||||
using MareSynchronos.WebAPI.SignalR;
|
||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MareSynchronos.WebAPI;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient
|
||||
{
|
||||
public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)";
|
||||
@@ -27,21 +25,25 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private CancellationTokenSource _connectionCancellationTokenSource;
|
||||
private ConnectionDto? _connectionDto;
|
||||
private bool _doNotNotifyOnNextInfo = false;
|
||||
private CancellationTokenSource? _healthCheckTokenSource = new();
|
||||
private bool _initialized;
|
||||
private string? _lastUsedToken;
|
||||
private HubConnection? _mareHub;
|
||||
private ServerState _serverState;
|
||||
|
||||
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
||||
PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
|
||||
PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator,
|
||||
TokenProvider tokenProvider) : base(logger, mediator)
|
||||
{
|
||||
_hubFactory = hubFactory;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_serverManager = serverManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
_connectionCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||
@@ -49,7 +51,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
Mediator.Subscribe<HubClosedMessage>(this, (msg) => MareHubOnClosed(msg.Exception));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = Task.Run(MareHubOnReconnected));
|
||||
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(msg.Exception));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => CyclePause(msg.UserData));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePause(msg.UserData));
|
||||
|
||||
ServerState = ServerState.Offline;
|
||||
|
||||
@@ -63,6 +65,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
|
||||
public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0);
|
||||
|
||||
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
|
||||
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
|
||||
|
||||
public bool IsConnected => ServerState == ServerState.Connected;
|
||||
@@ -94,7 +97,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateConnections(bool forceGetToken = false)
|
||||
public async Task CreateConnections()
|
||||
{
|
||||
Logger.LogDebug("CreateConnections called");
|
||||
|
||||
@@ -135,25 +138,14 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
{
|
||||
Logger.LogDebug("Building connection");
|
||||
|
||||
if (_serverManager.GetToken() == null || forceGetToken)
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Requesting new JWT");
|
||||
using HttpClient httpClient = new();
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
var postUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
var auth = secretKey.GetHash256();
|
||||
var result = await httpClient.PostAsync(postUri, new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("auth", auth),
|
||||
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
|
||||
}), token).ConfigureAwait(false);
|
||||
AuthFailureMessage = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
result.EnsureSuccessStatusCode();
|
||||
_serverManager.SaveToken(await result.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
Logger.LogDebug("JWT Success");
|
||||
_lastUsedToken = await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (MareAuthFailureException ex)
|
||||
{
|
||||
AuthFailureMessage = ex.Reason;
|
||||
throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested)
|
||||
@@ -164,13 +156,11 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
|
||||
if (token.IsCancellationRequested) break;
|
||||
|
||||
_mareHub = _hubFactory.GetOrCreate();
|
||||
_mareHub = _hubFactory.GetOrCreate(token);
|
||||
InitializeApiHooks();
|
||||
|
||||
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
||||
|
||||
InitializeApiHooks();
|
||||
await LoadIninitialPairs().ConfigureAwait(false);
|
||||
|
||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||
|
||||
ServerState = ServerState.Connected;
|
||||
@@ -200,6 +190,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
Dalamud.Interface.Internal.Notifications.NotificationType.Error));
|
||||
}
|
||||
|
||||
await LoadIninitialPairs().ConfigureAwait(false);
|
||||
await LoadOnlinePairs().ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -239,7 +230,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
{
|
||||
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
|
||||
var perm = pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(true);
|
||||
perm.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
// wait until it's changed
|
||||
while (pair.UserPair!.OwnPermissions != perm)
|
||||
@@ -247,7 +238,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
await Task.Delay(250, cts.Token).ConfigureAwait(false);
|
||||
Logger.LogTrace("Waiting for permissions change for {data}", userData);
|
||||
}
|
||||
perm.SetPaused(false);
|
||||
perm.SetPaused(paused: false);
|
||||
await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
}, cts.Token).ContinueWith((t) => cts.Dispose());
|
||||
|
||||
@@ -266,7 +257,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
base.Dispose(disposing);
|
||||
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
@@ -275,19 +266,24 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
while (!ct.IsCancellationRequested && _mareHub != null)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
|
||||
Logger.LogDebug("Checking Client Health State");
|
||||
|
||||
bool requireReconnect = await RefreshToken(ct).ConfigureAwait(false);
|
||||
|
||||
if (requireReconnect) continue;
|
||||
|
||||
_ = await CheckClientHealth().ConfigureAwait(false);
|
||||
Logger.LogDebug("Checked Client Health State");
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogIn()
|
||||
{
|
||||
Task.Run(() => CreateConnections(forceGetToken: true));
|
||||
_ = Task.Run(() => CreateConnections());
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogOut()
|
||||
{
|
||||
Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
ServerState = ServerState.Offline;
|
||||
}
|
||||
|
||||
@@ -296,28 +292,30 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
if (_mareHub == null) return;
|
||||
|
||||
Logger.LogDebug("Initializing data");
|
||||
OnDownloadReady((guid) => Client_DownloadReady(guid));
|
||||
OnReceiveServerMessage((sev, msg) => Client_ReceiveServerMessage(sev, msg));
|
||||
OnUpdateSystemInfo((dto) => Client_UpdateSystemInfo(dto));
|
||||
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
||||
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
||||
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
|
||||
|
||||
OnUserSendOffline((dto) => Client_UserSendOffline(dto));
|
||||
OnUserAddClientPair((dto) => Client_UserAddClientPair(dto));
|
||||
OnUserReceiveCharacterData((dto) => Client_UserReceiveCharacterData(dto));
|
||||
OnUserRemoveClientPair(dto => Client_UserRemoveClientPair(dto));
|
||||
OnUserSendOnline(dto => Client_UserSendOnline(dto));
|
||||
OnUserUpdateOtherPairPermissions(dto => Client_UserUpdateOtherPairPermissions(dto));
|
||||
OnUserUpdateSelfPairPermissions(dto => Client_UserUpdateSelfPairPermissions(dto));
|
||||
OnUserReceiveUploadStatus(dto => Client_UserReceiveUploadStatus(dto));
|
||||
OnUserUpdateProfile(dto => Client_UserUpdateProfile(dto));
|
||||
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
|
||||
OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto));
|
||||
OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto));
|
||||
OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto));
|
||||
OnUserSendOnline(dto => _ = Client_UserSendOnline(dto));
|
||||
OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto));
|
||||
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
|
||||
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
|
||||
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
|
||||
OnUserDefaultPermissionUpdate(dto => _ = Client_UserUpdateDefaultPermissions(dto));
|
||||
OnUpdateUserIndividualPairStatusDto(dto => _ = Client_UpdateUserIndividualPairStatusDto(dto));
|
||||
|
||||
OnGroupChangePermissions((dto) => Client_GroupChangePermissions(dto));
|
||||
OnGroupDelete((dto) => Client_GroupDelete(dto));
|
||||
OnGroupPairChangePermissions((dto) => Client_GroupPairChangePermissions(dto));
|
||||
OnGroupPairChangeUserInfo((dto) => Client_GroupPairChangeUserInfo(dto));
|
||||
OnGroupPairJoined((dto) => Client_GroupPairJoined(dto));
|
||||
OnGroupPairLeft((dto) => Client_GroupPairLeft(dto));
|
||||
OnGroupSendFullInfo((dto) => Client_GroupSendFullInfo(dto));
|
||||
OnGroupSendInfo((dto) => Client_GroupSendInfo(dto));
|
||||
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
|
||||
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
|
||||
OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto));
|
||||
OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto));
|
||||
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
|
||||
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
|
||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
||||
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
_healthCheckTokenSource?.Dispose();
|
||||
@@ -329,24 +327,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
|
||||
private async Task LoadIninitialPairs()
|
||||
{
|
||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
||||
_pairManager.AddUserPair(userPair, addToLastAddedUser: false);
|
||||
}
|
||||
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Group: {entry}", entry);
|
||||
_pairManager.AddGroup(entry);
|
||||
}
|
||||
foreach (var group in _pairManager.GroupPairs.Keys)
|
||||
|
||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||
{
|
||||
var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false);
|
||||
foreach (var user in users)
|
||||
{
|
||||
Logger.LogDebug("Group Pair: {user}", user);
|
||||
_pairManager.AddGroupPair(user);
|
||||
}
|
||||
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
||||
_pairManager.AddUserPair(userPair);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +395,33 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
Logger.LogWarning(arg, "Connection closed... Reconnecting");
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshToken(CancellationToken ct)
|
||||
{
|
||||
Logger.LogDebug("Checking token");
|
||||
|
||||
bool requireReconnect = false;
|
||||
try
|
||||
{
|
||||
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||
if (!string.Equals(token, _lastUsedToken, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("Reconnecting due to updated token");
|
||||
|
||||
_doNotNotifyOnNextInfo = true;
|
||||
await CreateConnections().ConfigureAwait(false);
|
||||
requireReconnect = true;
|
||||
}
|
||||
}
|
||||
catch (MareAuthFailureException ex)
|
||||
{
|
||||
AuthFailureMessage = ex.Reason;
|
||||
await StopConnection(ServerState.Unauthorized).ConfigureAwait(false);
|
||||
requireReconnect = true;
|
||||
}
|
||||
|
||||
return requireReconnect;
|
||||
}
|
||||
|
||||
private async Task StopConnection(ServerState state)
|
||||
{
|
||||
ServerState = ServerState.Disconnecting;
|
||||
@@ -421,7 +438,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
_connectionDto = null;
|
||||
}
|
||||
|
||||
|
||||
ServerState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
||||
@@ -16,28 +16,58 @@ namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public class HubFactory : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly IPluginLog _pluginLog;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private HubConnection? _instance;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator, ServerConfigurationManager serverConfigurationManager, MareConfigService configService,
|
||||
IPluginLog pluginLog) : base(logger, mediator)
|
||||
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
|
||||
ServerConfigurationManager serverConfigurationManager, MareConfigService configService,
|
||||
TokenProvider tokenProvider, IPluginLog pluginLog) : base(logger, mediator)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_configService = configService;
|
||||
_tokenProvider = tokenProvider;
|
||||
_pluginLog = pluginLog;
|
||||
}
|
||||
|
||||
private HubConnection BuildHubConnection()
|
||||
public async Task DisposeHubAsync()
|
||||
{
|
||||
if (_instance == null || _isDisposed) return;
|
||||
|
||||
Logger.LogDebug("Disposing current HubConnection");
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_instance.Closed -= HubOnClosed;
|
||||
_instance.Reconnecting -= HubOnReconnecting;
|
||||
_instance.Reconnected -= HubOnReconnected;
|
||||
|
||||
await _instance.StopAsync().ConfigureAwait(false);
|
||||
await _instance.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
_instance = null;
|
||||
|
||||
Logger.LogDebug("Current HubConnection disposed");
|
||||
}
|
||||
|
||||
public HubConnection GetOrCreate(CancellationToken ct)
|
||||
{
|
||||
if (!_isDisposed && _instance != null) return _instance;
|
||||
|
||||
return BuildHubConnection(ct);
|
||||
}
|
||||
|
||||
private HubConnection BuildHubConnection(CancellationToken ct)
|
||||
{
|
||||
Logger.LogDebug("Building new HubConnection");
|
||||
|
||||
_instance = new HubConnectionBuilder()
|
||||
.WithUrl(_serverConfigurationManager.CurrentApiUrl + IMareHub.Path, options =>
|
||||
{
|
||||
options.Headers.Add("Authorization", "Bearer " + _serverConfigurationManager.GetToken());
|
||||
options.AccessTokenProvider = () => _tokenProvider.GetOrUpdateToken(ct);
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
})
|
||||
.AddMessagePackProtocol(opt =>
|
||||
@@ -76,6 +106,12 @@ public class HubFactory : MediatorSubscriberBase
|
||||
return _instance;
|
||||
}
|
||||
|
||||
private Task HubOnClosed(Exception? arg)
|
||||
{
|
||||
Mediator.Publish(new HubClosedMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HubOnReconnected(string? arg)
|
||||
{
|
||||
Mediator.Publish(new HubReconnectedMessage(arg));
|
||||
@@ -87,37 +123,4 @@ public class HubFactory : MediatorSubscriberBase
|
||||
Mediator.Publish(new HubReconnectingMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HubOnClosed(Exception? arg)
|
||||
{
|
||||
Mediator.Publish(new HubClosedMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public HubConnection GetOrCreate()
|
||||
{
|
||||
if (!_isDisposed && _instance != null) return _instance;
|
||||
|
||||
return BuildHubConnection();
|
||||
}
|
||||
|
||||
public async Task DisposeHubAsync()
|
||||
{
|
||||
if (_instance == null || _isDisposed) return;
|
||||
|
||||
Logger.LogDebug("Disposing current HubConnection");
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_instance.Closed -= HubOnClosed;
|
||||
_instance.Reconnecting -= HubOnReconnecting;
|
||||
_instance.Reconnected -= HubOnReconnected;
|
||||
|
||||
await _instance.StopAsync().ConfigureAwait(false);
|
||||
await _instance.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
_instance = null;
|
||||
|
||||
Logger.LogDebug("Current HubConnection disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
3
MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs
Normal file
3
MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public record JwtIdentifier(string ApiUrl, string SecretKey);
|
||||
11
MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
11
MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public class MareAuthFailureException : Exception
|
||||
{
|
||||
public MareAuthFailureException(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public string Reason { get; }
|
||||
}
|
||||
118
MareSynchronos/WebAPI/SignalR/TokenProvider.cs
Normal file
118
MareSynchronos/WebAPI/SignalR/TokenProvider.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using MareSynchronos.API.Routes;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public sealed class TokenProvider : IDisposable
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<TokenProvider> _logger;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
|
||||
|
||||
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil)
|
||||
{
|
||||
_logger = logger;
|
||||
_serverManager = serverManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_httpClient = new();
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
}
|
||||
|
||||
private JwtIdentifier CurrentIdentifier => new(_serverManager.CurrentApiUrl, _serverManager.GetSecretKey()!);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
public async Task<string> GetNewToken(bool isRenewal, CancellationToken token)
|
||||
{
|
||||
Uri tokenUri;
|
||||
string response = string.Empty;
|
||||
HttpResponseMessage result;
|
||||
|
||||
try
|
||||
{
|
||||
if (!isRenewal)
|
||||
{
|
||||
_logger.LogDebug("GetNewToken: Requesting");
|
||||
|
||||
tokenUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
var secretKey = _serverManager.GetSecretKey()!;
|
||||
var auth = secretKey.GetHash256();
|
||||
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("auth", auth),
|
||||
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
|
||||
}), token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("GetNewToken: Renewal");
|
||||
|
||||
tokenUri = MareAuth.RenewTokenFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
HttpRequestMessage request = new(HttpMethod.Get, tokenUri.ToString());
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenCache[CurrentIdentifier]);
|
||||
result = await _httpClient.SendAsync(request, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
result.EnsureSuccessStatusCode();
|
||||
_tokenCache[CurrentIdentifier] = response;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokenCache.TryRemove(CurrentIdentifier, out _);
|
||||
|
||||
_logger.LogError(ex, "GetNewToken: Failure to get token");
|
||||
|
||||
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new MareAuthFailureException(response);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(response);
|
||||
_logger.LogTrace("GetNewToken: JWT {token}", response);
|
||||
_logger.LogDebug("GetNewToken: Valid until {date}, ValidClaim until {date}", jwtToken.ValidTo,
|
||||
new DateTime(long.Parse(jwtToken.Claims.Single(c => string.Equals(c.Type, "expiration_date", StringComparison.Ordinal)).Value), DateTimeKind.Utc));
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrUpdateToken(CancellationToken ct, bool forceRenew = false)
|
||||
{
|
||||
bool renewal = forceRenew;
|
||||
if (!forceRenew && _tokenCache.TryGetValue(CurrentIdentifier, out var token))
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(token);
|
||||
if (jwtToken.ValidTo == DateTime.MinValue || jwtToken.ValidTo.Subtract(TimeSpan.FromMinutes(5)) > DateTime.UtcNow)
|
||||
{
|
||||
_logger.LogTrace("GetOrUpdate: Returning token from cache");
|
||||
return token;
|
||||
}
|
||||
|
||||
renewal = true;
|
||||
}
|
||||
|
||||
_logger.LogTrace("GetOrUpdate: Getting new token");
|
||||
return await GetNewToken(renewal, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user