2023-03-26 13:47:18 +00:00
|
|
|
|
using System;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2023-03-26 13:47:18 +00:00
|
|
|
|
using Account;
|
|
|
|
|
using Grpc.Core;
|
|
|
|
|
using Grpc.Net.Client;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Pal.Client.Configuration;
|
2023-02-10 19:48:14 +00:00
|
|
|
|
using Pal.Client.Extensions;
|
|
|
|
|
using Pal.Client.Properties;
|
2023-10-03 20:05:19 +00:00
|
|
|
|
using Version = System.Version;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
namespace Pal.Client.Net;
|
|
|
|
|
|
|
|
|
|
internal partial class RemoteApi
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-10-03 20:05:19 +00:00
|
|
|
|
private static readonly Version PluginVersion = typeof(Plugin).Assembly.GetName().Version!;
|
2023-03-30 20:01:43 +00:00
|
|
|
|
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
|
|
|
|
|
|
|
|
|
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
|
|
|
|
|
ILoggerFactory? loggerFactory = null, bool retry = true)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
using IDisposable? logScope = _logger.BeginScope("TryConnect");
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
var result = await TryConnectImpl(cancellationToken, loggerFactory);
|
|
|
|
|
if (retry && result.ShouldRetry)
|
|
|
|
|
result = await TryConnectImpl(cancellationToken, loggerFactory);
|
2023-02-16 23:54:23 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
return (result.Success, result.Error);
|
|
|
|
|
}
|
2023-02-22 20:54:33 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl(
|
|
|
|
|
CancellationToken cancellationToken,
|
|
|
|
|
ILoggerFactory? loggerFactory)
|
|
|
|
|
{
|
|
|
|
|
if (_configuration.Mode != EMode.Online)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Not Online, not attempting to establish a connection");
|
|
|
|
|
return (false, Localization.ConnectionError_NotOnline, false);
|
2023-02-22 20:54:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
if (_channel == null ||
|
|
|
|
|
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
2023-02-22 20:54:33 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
Dispose();
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("Creating new gRPC channel");
|
|
|
|
|
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
HttpHandler = new SocketsHttpHandler
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
ConnectTimeout = TimeSpan.FromSeconds(5),
|
|
|
|
|
SslOptions = GetSslClientAuthenticationOptions(),
|
|
|
|
|
},
|
|
|
|
|
LoggerFactory = loggerFactory,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
|
|
|
|
|
await _channel.ConnectAsync(cancellationToken);
|
|
|
|
|
}
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
2023-02-13 21:42:24 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogTrace("Acquiring connect lock");
|
|
|
|
|
await _connectLock.WaitAsync(cancellationToken);
|
|
|
|
|
_logger.LogTrace("Obtained connect lock");
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var accountClient = new AccountService.AccountServiceClient(_channel);
|
|
|
|
|
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
|
|
|
|
if (configuredAccount == null)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
|
2023-10-03 20:05:19 +00:00
|
|
|
|
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest
|
|
|
|
|
{
|
|
|
|
|
Version = new()
|
|
|
|
|
{
|
|
|
|
|
Major = PluginVersion.Major,
|
|
|
|
|
Minor = PluginVersion.Minor,
|
|
|
|
|
},
|
|
|
|
|
},
|
2023-03-30 20:01:43 +00:00
|
|
|
|
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
|
|
|
|
cancellationToken: cancellationToken);
|
|
|
|
|
if (createAccountReply.Success)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
|
|
|
|
|
throw new InvalidOperationException("invalid account id returned");
|
2023-02-15 01:38:04 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId);
|
|
|
|
|
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_configurationManager.Save(_configuration);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
|
|
|
|
|
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_chat.Error(Localization.ConnectionError_OldVersion);
|
|
|
|
|
_warnedAboutUpgrade = true;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
|
|
|
|
|
return (false,
|
|
|
|
|
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error),
|
|
|
|
|
false);
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
}
|
2023-02-13 21:42:24 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
|
|
|
if (configuredAccount == null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("No account to login with");
|
|
|
|
|
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
|
|
|
|
|
}
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
if (!_loginInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Logging in with account id {AccountId}",
|
|
|
|
|
configuredAccount.AccountId.ToPartialId());
|
|
|
|
|
LoginReply loginReply = await accountClient.LoginAsync(
|
2023-10-03 20:05:19 +00:00
|
|
|
|
new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
AccountId = configuredAccount.AccountId.ToString(),
|
|
|
|
|
Version = new()
|
|
|
|
|
{
|
|
|
|
|
Major = PluginVersion.Major,
|
|
|
|
|
Minor = PluginVersion.Minor,
|
|
|
|
|
},
|
|
|
|
|
},
|
2023-03-30 20:01:43 +00:00
|
|
|
|
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
|
|
|
|
cancellationToken: cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (loginReply.Success)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("Login successful with account id: {AccountId}",
|
2023-02-22 16:21:48 +00:00
|
|
|
|
configuredAccount.AccountId.ToPartialId());
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
bool save = configuredAccount.EncryptIfNeeded();
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
|
|
|
|
|
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
|
|
|
|
|
{
|
|
|
|
|
configuredAccount.CachedRoles = newRoles;
|
|
|
|
|
save = true;
|
|
|
|
|
}
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
if (save)
|
|
|
|
|
_configurationManager.Save(_configuration);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("Login failed with error {Error}", loginReply.Error);
|
|
|
|
|
_loginInfo = new LoginInfo(null);
|
|
|
|
|
if (loginReply.Error == LoginError.InvalidAccountId)
|
|
|
|
|
{
|
|
|
|
|
_configuration.RemoveAccount(RemoteUrl);
|
|
|
|
|
_configurationManager.Save(_configuration);
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("Attempting connection retry without account id");
|
|
|
|
|
return (false, Localization.ConnectionError_InvalidAccountId, true);
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
|
|
|
|
|
if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_chat.Error(Localization.ConnectionError_OldVersion);
|
|
|
|
|
_warnedAboutUpgrade = true;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error),
|
|
|
|
|
false);
|
2023-02-22 16:21:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
|
|
|
|
|
if (!_loginInfo.IsValid)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn,
|
|
|
|
|
_loginInfo.IsExpired);
|
|
|
|
|
return (false, Localization.ConnectionError_LoginReturnedNoToken, false);
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
return (true, string.Empty, false);
|
|
|
|
|
}
|
|
|
|
|
finally
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogTrace("Releasing connectLock");
|
|
|
|
|
_connectLock.Release();
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
}
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
private async Task<bool> Connect(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var result = await TryConnect(cancellationToken);
|
|
|
|
|
return result.Success;
|
|
|
|
|
}
|
2023-02-16 23:54:23 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
using IDisposable? logScope = _logger.BeginScope("VerifyConnection");
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_warnedAboutUpgrade = false;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory);
|
|
|
|
|
if (!connectionResult.Success)
|
|
|
|
|
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
|
2023-02-06 21:00:38 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("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);
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
_logger.LogInformation("Verification returned no errors.");
|
|
|
|
|
return Localization.ConnectionSuccessful;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class LoginInfo
|
|
|
|
|
{
|
|
|
|
|
public LoginInfo(string? authToken)
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
if (!string.IsNullOrEmpty(authToken))
|
2022-12-21 19:23:48 +00:00
|
|
|
|
{
|
2023-03-30 20:01:43 +00:00
|
|
|
|
IsLoggedIn = true;
|
|
|
|
|
AuthToken = authToken;
|
|
|
|
|
Claims = JwtClaims.FromAuthToken(authToken);
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
2023-03-30 20:01:43 +00:00
|
|
|
|
else
|
|
|
|
|
IsLoggedIn = false;
|
|
|
|
|
}
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
public bool IsLoggedIn { get; }
|
|
|
|
|
public string? AuthToken { get; }
|
|
|
|
|
public JwtClaims? Claims { get; }
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
private DateTimeOffset ExpiresAt =>
|
|
|
|
|
Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
|
2023-02-22 16:21:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
|
2023-03-30 20:01:43 +00:00
|
|
|
|
public bool IsValid => IsLoggedIn && !IsExpired;
|
2022-12-21 19:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|