/*
* Copyright (c) 2024 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 System.Net;
using System.Net.Http.Headers;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.IssueReporting.Contracts;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ProcessCommunicationLogs;
using ProtonVPN.ProcessCommunication.Common;
namespace ProtonVPN.ProcessCommunication.Client;
public class ResponseHandler : DelegatingHandler
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly TimeSpan _minimumRestartInterval = TimeSpan.FromMinutes(2);
private readonly ILogger _logger;
private readonly IIssueReporter _issueReporter;
private readonly ISettings _settings;
private readonly EventHandler _invokingClientRestart;
private readonly bool _isToHandle;
private bool _isAppRestartInvoked;
public ResponseHandler(ILogger logger, IIssueReporter issueReporter, ISettings settings, EventHandler invokingClientRestart, HttpMessageHandler innerHandler)
: base(innerHandler)
{
_logger = logger;
_issueReporter = issueReporter;
_settings = settings;
_invokingClientRestart = invokingClientRestart;
_isToHandle = logger is not null && issueReporter is not null && settings is not null;
}
protected override async Task SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
await HandleResponseAsync(response);
return response;
}
private async Task HandleResponseAsync(HttpResponseMessage response)
{
if (_isToHandle && !response.IsSuccessStatusCode)
{
string clientProcessPath = GetHeaderValue(response.Headers, HttpConfiguration.CLIENT_PROCESS_PATH);
string serviceProcessPath = GetHeaderValue(response.Headers, HttpConfiguration.SERVICE_PROCESS_PATH);
string installedServicePath = GetHeaderValue(response.Headers, HttpConfiguration.INSTALLED_SERVICE_PATH);
string clientProcessVersion = GetHeaderValue(response.Headers, HttpConfiguration.CLIENT_PROCESS_VERSION);
string serviceProcessVersion = GetHeaderValue(response.Headers, HttpConfiguration.SERVICE_PROCESS_VERSION);
string installedServiceVersion = GetHeaderValue(response.Headers, HttpConfiguration.INSTALLED_SERVICE_VERSION);
_logger.Error(
$"Received HTTP status code {response.StatusCode} from gRPC server. " +
$"Client Process Path: '{clientProcessPath}' Version '{clientProcessVersion}', " +
$"Service Process Path: '{serviceProcessPath}' Version '{serviceProcessVersion}', " +
$"Installed Service Path: '{installedServicePath}' Version '{installedServiceVersion}'");
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
string logMessageDetails =
$"Client Process Path: '{clientProcessPath}', " +
$"Service Process Path: '{serviceProcessPath}', " +
$"Installed Service Path: '{installedServicePath}'";
await Handle401UnauthorizedAsync(clientProcessVersion, serviceProcessVersion, logMessageDetails);
}
}
}
private async Task Handle401UnauthorizedAsync(string clientProcessVersion, string serviceProcessVersion,
string logMessageDetails)
{
await _semaphore.WaitAsync();
try
{
Handle401Unauthorized(clientProcessVersion, serviceProcessVersion, logMessageDetails);
}
finally
{
_semaphore.Release();
}
}
private void Handle401Unauthorized(string clientProcessVersionString, string serviceProcessVersionString,
string logMessageDetails)
{
string versions = $"ClientProcessVersion: {clientProcessVersionString}, ServiceProcessVersion: {serviceProcessVersionString}";
if (Version.TryParse(clientProcessVersionString, out Version clientProcessVersion) &&
Version.TryParse(serviceProcessVersionString, out Version serviceProcessVersion) &&
clientProcessVersion < serviceProcessVersion)
{
HandleClientVersionLowerThanServiceVersion(versions);
}
else
{
const string logExplanation = "Received 401 Unauthorized from gRPC server but " +
"the client process version is not below the service process version.";
_logger.Warn($"{logExplanation} {versions}");
_issueReporter.CaptureMessage(logExplanation, logMessageDetails);
}
}
private void HandleClientVersionLowerThanServiceVersion(string versions)
{
if (_isAppRestartInvoked)
{
_logger.Warn($"Ignoring 401 Unauthorized because client restart was already invoked. {versions}");
return;
}
if (_settings.LastProcessVersionMismatchRestartVersions == versions &&
_settings.LastProcessVersionMismatchRestartUtcDate is not null &&
(_settings.LastProcessVersionMismatchRestartUtcDate + _minimumRestartInterval) > DateTimeOffset.UtcNow)
{
string logMessage = $"Cannot restart the client because that was done for " +
$"the current version pair less than {_minimumRestartInterval} ago " +
$"(Last restart date: {_settings.LastProcessVersionMismatchRestartUtcDate}). {versions}";
_logger.Error(logMessage);
return;
}
const string logExplanation = "Restarting the client because the version is inferior to the service process version.";
_logger.Warn($"{logExplanation} {versions}");
_issueReporter.CaptureMessage(logExplanation, versions);
_settings.LastProcessVersionMismatchRestartVersions = versions;
_settings.LastProcessVersionMismatchRestartUtcDate = DateTimeOffset.UtcNow;
_invokingClientRestart?.Invoke(this, EventArgs.Empty);
_isAppRestartInvoked = true;
}
private string GetHeaderValue(HttpResponseHeaders headers, string headerKey)
{
string value = null;
if (headers.TryGetValues(headerKey, out IEnumerable values))
{
value = values.FirstOrDefault();
}
return value;
}
}