using Account; using Dalamud.Logging; using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Pal.Client.Extensions; using Pal.Client.Properties; using Pal.Client.Configuration; namespace Pal.Client.Net { internal partial class RemoteApi { private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { if (_configuration.Mode != EMode.Online) { PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); return (false, Localization.ConnectionError_NotOnline); } if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) { Dispose(); PluginLog.Information("TryConnect: Creating new gRPC channel"); _channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(5), SslOptions = GetSslClientAuthenticationOptions(), }, LoggerFactory = loggerFactory, }); PluginLog.Information($"TryConnect: Connecting to upstream service at {RemoteUrl}"); await _channel.ConnectAsync(cancellationToken); } cancellationToken.ThrowIfCancellationRequested(); var accountClient = new AccountService.AccountServiceClient(_channel); IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); if (configuredAccount == null) { PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); if (createAccountReply.Success) { if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) throw new InvalidOperationException("invalid account id returned"); configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); PluginLog.Information($"TryConnect: Account created with id {accountId.ToPartialId()}"); _configurationManager.Save(_configuration); } else { PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); } } cancellationToken.ThrowIfCancellationRequested(); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (configuredAccount == null) { PluginLog.Warning("TryConnect: No account to login with"); return (false, Localization.ConnectionError_CreateAccountReturnedNoId); } if (!_loginInfo.IsValid) { PluginLog.Information($"TryConnect: Logging in with account id {configuredAccount.AccountId.ToPartialId()}"); LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = configuredAccount.AccountId.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); if (loginReply.Success) { PluginLog.Information($"TryConnect: Login successful with account id: {configuredAccount.AccountId.ToPartialId()}"); _loginInfo = new LoginInfo(loginReply.AuthToken); bool save = configuredAccount.EncryptIfNeeded(); List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) { configuredAccount.CachedRoles = newRoles; save = true; } if (save) _configurationManager.Save(_configuration); } else { PluginLog.Error($"TryConnect: Login failed with error {loginReply.Error}"); _loginInfo = new LoginInfo(null); if (loginReply.Error == LoginError.InvalidAccountId) { _configuration.RemoveAccount(RemoteUrl); _configurationManager.Save(_configuration); if (retry) { PluginLog.Information("TryConnect: Attempting connection retry without account id"); return await TryConnect(cancellationToken, retry: false); } else return (false, Localization.ConnectionError_InvalidAccountId); } if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) { _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); } } if (!_loginInfo.IsValid) { PluginLog.Error($"TryConnect: Login state is loggedIn={_loginInfo.IsLoggedIn}, expired={_loginInfo.IsExpired}"); return (false, Localization.ConnectionError_LoginReturnedNoToken); } cancellationToken.ThrowIfCancellationRequested(); return (true, string.Empty); } private async Task Connect(CancellationToken cancellationToken) { var result = await TryConnect(cancellationToken); return result.Success; } public async Task VerifyConnection(CancellationToken cancellationToken = default) { _warnedAboutUpgrade = false; var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory); if (!connectionResult.Success) return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error); PluginLog.Information("VerifyConnection: Connection established, trying to verify auth token"); var accountClient = new AccountService.AccountServiceClient(_channel); await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); PluginLog.Information("VerifyConnection: Verification returned no errors."); return Localization.ConnectionSuccessful; } internal sealed class LoginInfo { public LoginInfo(string? authToken) { if (!string.IsNullOrEmpty(authToken)) { IsLoggedIn = true; AuthToken = authToken; Claims = JwtClaims.FromAuthToken(authToken); } else IsLoggedIn = false; } public bool IsLoggedIn { get; } public string? AuthToken { get; } public JwtClaims? Claims { get; } public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; public bool IsValid => IsLoggedIn && !IsExpired; } } }