/*
* Copyright (c) 2025 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.EventMessaging.Contracts;
using ProtonVPN.Client.Logic.Connection.Contracts.Enums;
using ProtonVPN.Client.Logic.Connection.Contracts.GuestHole;
using ProtonVPN.Client.Logic.Connection.Contracts.Messages;
using ProtonVPN.Common.Legacy.Abstract;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.GuestHoleLogs;
namespace ProtonVPN.Client.Logic.Connection.GuestHole;
public class GuestHoleManager : IGuestHoleManager, IEventMessageReceiver
{
private const int CONNECTED_FUNC_DELAY_IN_MS = 1000;
private readonly ILogger _logger;
private readonly IEventMessageSender _eventMessageSender;
private readonly IGuestHoleConnector _guestHoleConnector;
private bool _isActive;
private bool _wasConnected;
private Func>? _onConnectedFunc;
private TaskCompletionSource? _tcs;
private ConnectionStatus _lastVpnStatus = ConnectionStatus.Disconnected;
public bool IsActive => _isActive;
public GuestHoleManager(
ILogger logger,
IEventMessageSender eventMessageSender,
IGuestHoleConnector guestHoleConnector)
{
_logger = logger;
_eventMessageSender = eventMessageSender;
_guestHoleConnector = guestHoleConnector;
}
public async Task ExecuteAsync(Func> onConnectedFunc, CancellationToken cancellationToken) where T : Result
{
_onConnectedFunc = onConnectedFunc;
// Run continuations asynchronously so TrySetResult completes the Task first, and the code awaiting it in
// ExecuteAsync resumes later instead of immediately inside Receive/HandleDisconnection.
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
SetStatus(true);
try
{
await _guestHoleConnector.ConnectToGuestHoleAsync();
Result? result = await _tcs.Task.WaitAsync(cancellationToken);
if (result is null)
{
await DisconnectAsync();
}
return (T?)result;
}
catch (GuestHoleException e)
{
_logger.Warn("Failed to connect to guest hole.", e);
HandleDisconnection();
return null;
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
_logger.Info("Guest hole connection was cancelled.");
await DisconnectAsync();
throw;
}
}
private void SetStatus(bool isActive)
{
_isActive = isActive;
_eventMessageSender.Send(new GuestHoleStatusChangedMessage(isActive));
}
public async void Receive(ConnectionStatusChangedMessage message)
{
if (!_isActive)
{
return;
}
if (_lastVpnStatus == message.ConnectionStatus)
{
return;
}
_lastVpnStatus = message.ConnectionStatus;
switch (message.ConnectionStatus)
{
case ConnectionStatus.Connected when _tcs is not null &&
_onConnectedFunc is not null:
_logger.Info("Connected to guest hole");
_wasConnected = true;
Result? result;
try
{
await Task.Delay(CONNECTED_FUNC_DELAY_IN_MS);
result = await _onConnectedFunc();
}
catch (Exception e)
{
_logger.Error("Failed to execute action inside guest hole", e);
result = null;
}
SetTaskCompletionSourceResult(result);
break;
case ConnectionStatus.Disconnected:
HandleDisconnection();
break;
}
}
private void HandleDisconnection()
{
if (!_wasConnected)
{
SetTaskCompletionSourceResult(null);
}
SetStatus(false);
_logger.Info("Disconnected from guest hole.");
}
private void SetTaskCompletionSourceResult(Result? result)
{
if (_tcs is null)
{
return;
}
_tcs.TrySetResult(result);
_tcs = null;
_onConnectedFunc = null;
_wasConnected = false;
}
public async Task DisconnectAsync()
{
await _guestHoleConnector.DisconnectFromGuestHoleAsync();
}
}