XIVAuth
This commit is contained in:
@@ -25,6 +25,8 @@ using System.Diagnostics;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using MareSynchronos.API.Dto.Account;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
@@ -67,10 +69,18 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private int _secretKeyIdx = -1;
|
private int _secretKeyIdx = -1;
|
||||||
private bool _showModalForUserAddition;
|
private bool _showModalForUserAddition;
|
||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
|
private bool _registrationInProgress = false;
|
||||||
|
private bool _registrationSuccess = false;
|
||||||
|
private string? _registrationMessage;
|
||||||
|
private RegisterReplyDto? _registrationReply;
|
||||||
|
private readonly AccountRegistrationService _registerService;
|
||||||
|
private string _secretKey = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
|
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
|
||||||
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
|
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService, AccountRegistrationService registerService)
|
||||||
: base(logger, mediator, "###SnowcloakSyncMainUI", performanceCollectorService)
|
: base(logger, mediator, "###SnowcloakSyncMainUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
@@ -78,6 +88,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
|
_registerService = registerService;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_uidDisplayHandler = uidDisplayHandler;
|
_uidDisplayHandler = uidDisplayHandler;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
@@ -109,7 +120,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
// changed min size
|
// changed min size
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(300, 400),
|
MinimumSize = new Vector2(500, 400),
|
||||||
MaximumSize = new Vector2(600, 2000),
|
MaximumSize = new Vector2(600, 2000),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -146,7 +157,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(icon, label, (150 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
|
if (_uiSharedService.IconTextButton(icon, label, (165 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
|
||||||
{
|
{
|
||||||
_selectedMenu = menu;
|
_selectedMenu = menu;
|
||||||
}
|
}
|
||||||
@@ -166,7 +177,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(icon, label, (150 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
|
if (_uiSharedService.IconTextButton(icon, label, (165 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
|
||||||
{
|
{
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
@@ -176,7 +187,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
// Adjust both values below to change size, 40 seems good to fit the buttons
|
// Adjust both values below to change size, 40 seems good to fit the buttons
|
||||||
// 150 seems decent enough to fit the text into it, could be smaller
|
// 150 seems decent enough to fit the text into it, could be smaller
|
||||||
var sidebarWidth = (_sidebarCollapsed ? 40 : 150) * ImGuiHelpers.GlobalScale;
|
// Elf note: Adjusted to 165 since "Character Analysis" hung off the end a bit
|
||||||
|
var sidebarWidth = (_sidebarCollapsed ? 40 : 165) * ImGuiHelpers.GlobalScale;
|
||||||
|
|
||||||
using (var child = ImRaii.Child("Sidebar", new Vector2(sidebarWidth, -1), true))
|
using (var child = ImRaii.Child("Sidebar", new Vector2(sidebarWidth, -1), true))
|
||||||
{
|
{
|
||||||
@@ -356,10 +368,44 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.Dummy(new(10));
|
ImGui.Dummy(new(10));
|
||||||
var keys = _serverManager.CurrentServer!.SecretKeys;
|
var keys = _serverManager.CurrentServer!.SecretKeys;
|
||||||
|
ImGui.BeginDisabled(_registrationInProgress || _uiSharedService.ApiController.ServerState == ServerState.Connecting || _uiSharedService.ApiController.ServerState == ServerState.Reconnecting);
|
||||||
if (keys.Any())
|
if (keys.Any())
|
||||||
{
|
{
|
||||||
if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key;
|
if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key;
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Log in with XIVAuth (experimental)"))
|
||||||
|
{
|
||||||
|
_registrationInProgress = true;
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reply = await _registerService.XIVAuth(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (!reply.Success)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Registration failed: {err}", reply.ErrorMessage);
|
||||||
|
_registrationMessage = reply.ErrorMessage;
|
||||||
|
if (_registrationMessage.IsNullOrEmpty())
|
||||||
|
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_registrationMessage = "Account registered. Welcome to Snowcloak!";
|
||||||
|
_secretKey = reply.SecretKey ?? "";
|
||||||
|
_registrationReply = reply;
|
||||||
|
_registrationSuccess = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Registration failed");
|
||||||
|
_registrationSuccess = false;
|
||||||
|
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_registrationInProgress = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add character with existing key"))
|
||||||
{
|
{
|
||||||
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
|
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
|
||||||
{
|
{
|
||||||
@@ -372,7 +418,43 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
_ = _apiController.CreateConnections();
|
_ = _apiController.CreateConnections();
|
||||||
}
|
}
|
||||||
|
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
|
||||||
|
|
||||||
|
if (_registrationInProgress)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Waiting for the server...");
|
||||||
|
}
|
||||||
|
else if (!_registrationMessage.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
if (!_registrationSuccess)
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudYellow, _registrationMessage);
|
||||||
|
else
|
||||||
|
ImGui.TextWrapped(_registrationMessage);
|
||||||
|
}
|
||||||
|
if (_secretKey.Length > 0 && _secretKey.Length != 64)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else if (_secretKey.Length == 64)
|
||||||
|
{
|
||||||
|
using var saveDisabled = ImRaii.Disabled(_uiSharedService.ApiController.ServerState == ServerState.Connecting || _uiSharedService.ApiController.ServerState == ServerState.Reconnecting);
|
||||||
|
if (ImGui.Button("Save and Connect"))
|
||||||
|
{
|
||||||
|
string keyName;
|
||||||
|
if (_serverManager.CurrentServer == null) _serverManager.SelectServer(0);
|
||||||
|
if (_registrationReply != null && _secretKey.Equals(_registrationReply.SecretKey, StringComparison.Ordinal))
|
||||||
|
keyName = _registrationReply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})";
|
||||||
|
else
|
||||||
|
keyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})";
|
||||||
|
_serverManager.CurrentServer!.SecretKeys.Add(_serverManager.CurrentServer.SecretKeys.Select(k => k.Key).LastOrDefault() + 1, new SecretKey()
|
||||||
|
{
|
||||||
|
FriendlyName = keyName,
|
||||||
|
Key = _secretKey,
|
||||||
|
});
|
||||||
|
_serverManager.AddCurrentCharacterToServer(save: false);
|
||||||
|
_ = Task.Run(() => _uiSharedService.ApiController.CreateConnections());
|
||||||
|
}
|
||||||
|
}
|
||||||
_uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
|
_uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.
|
The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted.
|
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files may be automatically deleted.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.
|
Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.
|
||||||
@@ -263,6 +263,39 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
|
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.TextUnformatted("If you have not used Snowcloak before, click below to register a new account.");
|
ImGui.TextUnformatted("If you have not used Snowcloak before, click below to register a new account.");
|
||||||
|
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Log in with XIVAuth (experimental)"))
|
||||||
|
{
|
||||||
|
_registrationInProgress = true;
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reply = await _registerService.XIVAuth(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (!reply.Success)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Registration failed: {err}", reply.ErrorMessage);
|
||||||
|
_registrationMessage = reply.ErrorMessage;
|
||||||
|
if (_registrationMessage.IsNullOrEmpty())
|
||||||
|
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_registrationMessage = "Account registered. Welcome to Snowcloak!";
|
||||||
|
_secretKey = reply.SecretKey ?? "";
|
||||||
|
_registrationReply = reply;
|
||||||
|
_registrationSuccess = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Registration failed");
|
||||||
|
_registrationSuccess = false;
|
||||||
|
_registrationMessage = "An unknown error occured. Please try again later.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_registrationInProgress = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account"))
|
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account"))
|
||||||
{
|
{
|
||||||
_registrationInProgress = true;
|
_registrationInProgress = true;
|
||||||
@@ -298,7 +331,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
|
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
|
||||||
if (_registrationInProgress)
|
if (_registrationInProgress)
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Sending request...");
|
ImGui.TextUnformatted("Waiting for the server...");
|
||||||
}
|
}
|
||||||
else if (!_registrationMessage.IsNullOrEmpty())
|
else if (!_registrationMessage.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
|
@@ -9,6 +9,8 @@ using System.Net.Http.Headers;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace MareSynchronos.WebAPI;
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
@@ -17,16 +19,18 @@ public sealed class AccountRegistrationService : IDisposable
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<AccountRegistrationService> _logger;
|
private readonly ILogger<AccountRegistrationService> _logger;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
private string GenerateSecretKey()
|
private string GenerateSecretKey()
|
||||||
{
|
{
|
||||||
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
|
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountRegistrationService(ILogger<AccountRegistrationService> logger, ServerConfigurationManager serverManager)
|
public AccountRegistrationService(ILogger<AccountRegistrationService> logger, DalamudUtilService dalamudUtilService, ServerConfigurationManager serverManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_httpClient = new(
|
_httpClient = new(
|
||||||
new HttpClientHandler
|
new HttpClientHandler
|
||||||
{
|
{
|
||||||
@@ -43,6 +47,72 @@ public sealed class AccountRegistrationService : IDisposable
|
|||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<RegisterReplyDto> XIVAuth(CancellationToken token)
|
||||||
|
{
|
||||||
|
var secretKey = GenerateSecretKey();
|
||||||
|
var hashedSecretKey = secretKey.GetHash256();
|
||||||
|
var playerName = _dalamudUtilService.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||||
|
var worldId = (ushort)_dalamudUtilService.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
|
var worldName = _dalamudUtilService.WorldData.Value[(worldId)];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var sessionID = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)).Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||||
|
|
||||||
|
Uri handshakeUri = new Uri("https://account.snowcloak-sync.com/register");
|
||||||
|
var handshakePayload = new { session_id = sessionID, hashed_secret = hashedSecretKey, character_name = playerName, home_world = worldName };
|
||||||
|
var handshakeResponse = await _httpClient.PostAsJsonAsync(handshakeUri, handshakePayload, token).ConfigureAwait(false);
|
||||||
|
handshakeResponse.EnsureSuccessStatusCode();
|
||||||
|
var register = await handshakeResponse.Content.ReadFromJsonAsync<RegisterResponse>(cancellationToken: token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (register is null || string.IsNullOrWhiteSpace(register.link_url) ||
|
||||||
|
string.IsNullOrWhiteSpace(register.poll_url))
|
||||||
|
{
|
||||||
|
return new RegisterReplyDto() { Success = false, ErrorMessage = "Malformed registration response." };
|
||||||
|
}
|
||||||
|
|
||||||
|
Util.OpenLink(register.link_url);
|
||||||
|
const int maxAttempts = 600 / 15; // Try once every 15 seconds for 10 minutes
|
||||||
|
var pollUri = new Uri(register.poll_url);
|
||||||
|
PollResponse? lastPoll = null;
|
||||||
|
for (int i = 0; i < maxAttempts; i++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
using var resp = await _httpClient.GetAsync(pollUri, token).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Gone)
|
||||||
|
{
|
||||||
|
// Server marked this as having been consumed already OR it got TLL'd out
|
||||||
|
return new RegisterReplyDto()
|
||||||
|
{
|
||||||
|
Success = false, ErrorMessage = "Registration session expired. Please try again."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
lastPoll = await resp.Content.ReadFromJsonAsync<PollResponse>(cancellationToken: token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (lastPoll?.status?.Equals("bound", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
|
{
|
||||||
|
// yay
|
||||||
|
return new RegisterReplyDto()
|
||||||
|
{
|
||||||
|
Success = true, ErrorMessage = null, UID = lastPoll?.uid, SecretKey = secretKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Pending, keep polling
|
||||||
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(15), token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
// Timed out
|
||||||
|
return new RegisterReplyDto()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage =
|
||||||
|
"Timed out waiting for authorisation. Please try again, and complete the process within 10 minutes."
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token)
|
public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token)
|
||||||
{
|
{
|
||||||
var secretKey = GenerateSecretKey();
|
var secretKey = GenerateSecretKey();
|
||||||
@@ -67,4 +137,19 @@ public sealed class AccountRegistrationService : IDisposable
|
|||||||
SecretKey = secretKey
|
SecretKey = secretKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private sealed class RegisterResponse
|
||||||
|
{
|
||||||
|
public string link_url { get; set; } = "";
|
||||||
|
public string poll_url { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PollResponse
|
||||||
|
{
|
||||||
|
public string status { get; set; } = "";
|
||||||
|
public string? uid { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user