/*
* Copyright (c) 2026 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using ProtonVPN.Client.Logic.Auth.Contracts;
using ProtonVPN.Client.Logic.Auth.Contracts.Models;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents;
using ProtonVPN.Client.Logic.Connection.Contracts.RequestCreators;
using ProtonVPN.Client.Logic.Connection.Contracts.ServerListGenerators;
using ProtonVPN.Client.Logic.Servers.Contracts.Models;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.Client.Settings.Contracts.Observers;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Common.Legacy.Vpn;
using ProtonVPN.Crypto.Contracts;
using ProtonVPN.EntityMapping.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.UserCertificateLogs;
using ProtonVPN.ProcessCommunication.Contracts.Entities.Crypto;
using ProtonVPN.ProcessCommunication.Contracts.Entities.LocalAgent;
using ProtonVPN.ProcessCommunication.Contracts.Entities.Settings;
using ProtonVPN.ProcessCommunication.Contracts.Entities.Vpn;
namespace ProtonVPN.Client.Logic.Connection.RequestCreators;
public class ConnectionRequestCreator : ConnectionRequestCreatorBase, IConnectionRequestCreator
{
protected readonly IServerListGenerator ServerListGenerator;
protected readonly ISmartServerListGenerator SmartServerListGenerator;
private readonly IConnectionKeyManager _connectionKeyManager;
private readonly IConnectionCertificateManager _connectionCertificateManager;
public ConnectionRequestCreator(
ILogger logger,
ISettings settings,
IEntityMapper entityMapper,
IConnectionKeyManager connectionKeyManager,
IConnectionCertificateManager connectionCertificateManager,
IServerListGenerator serverListGenerator,
ISmartServerListGenerator smartServerListGenerator,
IFeatureFlagsObserver featureFlagsObserver,
IMainSettingsRequestCreator mainSettingsRequestCreator)
: base(logger, settings, entityMapper, featureFlagsObserver, mainSettingsRequestCreator)
{
ServerListGenerator = serverListGenerator;
SmartServerListGenerator = smartServerListGenerator;
_connectionKeyManager = connectionKeyManager;
_connectionCertificateManager = connectionCertificateManager;
}
public virtual async Task CreateAsync(IConnectionIntent connectionIntent)
{
MainSettingsIpcEntity settings = GetSettings(connectionIntent);
VpnConfigIpcEntity config = GetVpnConfig(settings, connectionIntent);
List preferredProtocols = EntityMapper.Map(config.PreferredProtocols);
ServerListResult serverListResult = GetServerListResult(connectionIntent, preferredProtocols);
VpnServerIpcEntity[] servers = PhysicalServersToVpnServerIpcEntities(serverListResult.PhysicalServers);
bool areAllServersExcluded = servers.Length == 0 && serverListResult.Diagnostic.AreAllCandidatesExcluded;
ConnectionRequestIpcEntity request = new()
{
RetryId = Guid.NewGuid(),
Config = config,
Credentials = await GetVpnCredentialsAsync(),
Protocol = settings.VpnProtocol,
Servers = servers,
Settings = settings,
AreAllServersExcludedByUserPreference = areAllServersExcluded,
};
return request;
}
protected ServerListResult GetServerListResult(IConnectionIntent connectionIntent, IList preferredProtocols)
{
return IsToBypassSmartServerListGenerator(connectionIntent)
? ServerListGenerator.Generate(connectionIntent, preferredProtocols)
: SmartServerListGenerator.Generate(connectionIntent, preferredProtocols);
}
protected override async Task GetVpnCredentialsAsync()
{
await RequestCertificateIfNecessaryAsync();
ConnectionCertificate? connectionCertificate = Settings.ConnectionCertificate;
AsymmetricKeyPair? keyPair = _connectionKeyManager.GetKeyPairOrNull();
return new VpnCredentialsIpcEntity
{
Certificate = connectionCertificate.HasValue ? CreateCertificate(connectionCertificate.Value) : null,
ClientKeyPair = keyPair is null ? null : new AsymmetricKeyPairIpcEntity
{
PublicKey = EntityMapper.Map(keyPair.PublicKey),
SecretKey = EntityMapper.Map(keyPair.SecretKey)
}
};
}
private async Task RequestCertificateIfNecessaryAsync()
{
if (_connectionKeyManager.GetKeyPairOrNull() is null)
{
Logger.Info("Connection keys are missing, forcing new keys and certificate.");
await _connectionCertificateManager.ForceRequestNewKeyPairAndCertificateAsync();
}
else
{
ConnectionCertificate? connectionCertificate = Settings.ConnectionCertificate;
if (connectionCertificate is null)
{
Logger.Info("Connection certificate is missing, requesting a new certificate.");
await _connectionCertificateManager.RequestNewCertificateAsync();
}
else if (connectionCertificate.Value.ExpirationUtcDate <= DateTimeOffset.UtcNow)
{
Logger.Info("Connection certificate is expired, requesting a new certificate.");
await _connectionCertificateManager.RequestNewCertificateAsync();
}
}
}
private ConnectionCertificateIpcEntity CreateCertificate(ConnectionCertificate connectionCertificate)
{
return new()
{
Pem = connectionCertificate.Pem,
ExpirationDateUtc = connectionCertificate.ExpirationUtcDate.UtcDateTime,
};
}
protected VpnServerIpcEntity[] PhysicalServersToVpnServerIpcEntities(IEnumerable physicalServers)
{
IEnumerable hosts = physicalServers
.Select(s => new VpnHost(s.Domain, s.EntryIp, s.Label, GetServerPublicKey(s), s.Signature, s.IsIpv6Supported, s.RelayIpByProtocol));
return EntityMapper.Map(hosts).ToArray();
}
protected PublicKey? GetServerPublicKey(PhysicalServer server)
{
return string.IsNullOrEmpty(server.X25519PublicKey)
? null
: new PublicKey(server.X25519PublicKey, KeyAlgorithm.X25519);
}
}