PalacePal/Pal.Client/Net/RemoteApi.AccountService.cs

191 lines
8.5 KiB
C#
Raw Normal View History

2022-12-21 19:23:48 +00:00
using Account;
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;
2023-02-10 19:48:14 +00:00
using Pal.Client.Extensions;
using Pal.Client.Properties;
2023-02-15 01:38:04 +00:00
using Pal.Client.Configuration;
2022-12-21 19:23:48 +00:00
namespace Pal.Client.Net
{
internal partial class RemoteApi
{
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true)
{
using IDisposable? logScope = _logger.BeginScope("TryConnect");
2023-02-15 22:17:19 +00:00
if (_configuration.Mode != EMode.Online)
2022-12-21 19:23:48 +00:00
{
_logger.LogDebug("Not Online, not attempting to establish a connection");
2023-02-10 19:48:14 +00:00
return (false, Localization.ConnectionError_NotOnline);
2022-12-21 19:23:48 +00:00
}
if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
{
Dispose();
_logger.LogInformation("Creating new gRPC channel");
2022-12-21 19:23:48 +00:00
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(5),
SslOptions = GetSslClientAuthenticationOptions(),
},
LoggerFactory = loggerFactory,
});
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
2022-12-21 19:23:48 +00:00
await _channel.ConnectAsync(cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
2022-12-21 19:23:48 +00:00
var accountClient = new AccountService.AccountServiceClient(_channel);
2023-02-15 22:17:19 +00:00
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
2023-02-15 01:38:04 +00:00
if (configuredAccount == null)
2022-12-21 19:23:48 +00:00
{
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
2022-12-21 19:23:48 +00:00
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))
2023-02-15 01:38:04 +00:00
throw new InvalidOperationException("invalid account id returned");
2023-02-15 22:17:19 +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-02-15 22:17:19 +00:00
_configurationManager.Save(_configuration);
2022-12-21 19:23:48 +00:00
}
else
{
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
2022-12-21 19:23:48 +00:00
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
{
2023-02-15 22:17:19 +00:00
_chatGui.PalError(Localization.ConnectionError_OldVersion);
2022-12-21 19:23:48 +00:00
_warnedAboutUpgrade = true;
}
2023-02-10 19:48:14 +00:00
return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
2022-12-21 19:23:48 +00:00
}
}
cancellationToken.ThrowIfCancellationRequested();
2023-02-15 01:38:04 +00:00
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (configuredAccount == null)
2022-12-21 19:23:48 +00:00
{
_logger.LogWarning("No account to login with");
2023-02-10 19:48:14 +00:00
return (false, Localization.ConnectionError_CreateAccountReturnedNoId);
2022-12-21 19:23:48 +00:00
}
if (!_loginInfo.IsValid)
{
_logger.LogInformation("Logging in with account id {AccountId}", configuredAccount.AccountId.ToPartialId());
2023-02-15 01:38:04 +00:00
LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = configuredAccount.AccountId.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
2022-12-21 19:23:48 +00:00
if (loginReply.Success)
{
_logger.LogInformation("Login successful with account id: {AccountId}", configuredAccount.AccountId.ToPartialId());
2022-12-21 19:23:48 +00:00
_loginInfo = new LoginInfo(loginReply.AuthToken);
bool save = configuredAccount.EncryptIfNeeded();
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
{
configuredAccount.CachedRoles = newRoles;
save = true;
}
if (save)
2023-02-15 22:17:19 +00:00
_configurationManager.Save(_configuration);
2022-12-21 19:23:48 +00:00
}
else
{
_logger.LogError("Login failed with error {Error}", loginReply.Error);
2022-12-21 19:23:48 +00:00
_loginInfo = new LoginInfo(null);
if (loginReply.Error == LoginError.InvalidAccountId)
{
2023-02-15 22:17:19 +00:00
_configuration.RemoveAccount(RemoteUrl);
_configurationManager.Save(_configuration);
2022-12-21 19:23:48 +00:00
if (retry)
{
_logger.LogInformation("Attempting connection retry without account id");
2022-12-21 19:23:48 +00:00
return await TryConnect(cancellationToken, retry: false);
}
else
2023-02-10 19:48:14 +00:00
return (false, Localization.ConnectionError_InvalidAccountId);
2022-12-21 19:23:48 +00:00
}
if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade)
{
2023-02-15 22:17:19 +00:00
_chatGui.PalError(Localization.ConnectionError_OldVersion);
2022-12-21 19:23:48 +00:00
_warnedAboutUpgrade = true;
}
2023-02-10 19:48:14 +00:00
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error));
2022-12-21 19:23:48 +00:00
}
}
if (!_loginInfo.IsValid)
{
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired);
2023-02-10 19:48:14 +00:00
return (false, Localization.ConnectionError_LoginReturnedNoToken);
2022-12-21 19:23:48 +00:00
}
cancellationToken.ThrowIfCancellationRequested();
2022-12-21 19:23:48 +00:00
return (true, string.Empty);
}
private async Task<bool> Connect(CancellationToken cancellationToken)
{
var result = await TryConnect(cancellationToken);
return result.Success;
}
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
{
using IDisposable? logScope = _logger.BeginScope("VerifyConnection");
2022-12-21 19:23:48 +00:00
_warnedAboutUpgrade = false;
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory);
2022-12-21 19:23:48 +00:00
if (!connectionResult.Success)
2023-02-10 19:48:14 +00:00
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
2022-12-21 19:23:48 +00:00
_logger.LogInformation("Connection established, trying to verify auth token");
2022-12-21 19:23:48 +00:00
var accountClient = new AccountService.AccountServiceClient(_channel);
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
2023-02-06 21:00:38 +00:00
_logger.LogInformation("Verification returned no errors.");
2023-02-10 19:48:14 +00:00
return Localization.ConnectionSuccessful;
2022-12-21 19:23:48 +00:00
}
2023-02-15 22:17:19 +00:00
internal sealed class LoginInfo
2022-12-21 19:23:48 +00:00
{
public LoginInfo(string? authToken)
{
if (!string.IsNullOrEmpty(authToken))
{
IsLoggedIn = true;
AuthToken = authToken;
2023-02-11 20:10:45 +00:00
Claims = JwtClaims.FromAuthToken(authToken);
2022-12-21 19:23:48 +00:00
}
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;
}
}
}