/*
* 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 System.IO.Pipes;
using System.Security.Principal;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.OperatingSystems.Registries.Contracts;
using ProtonVPN.ProcessCommunication.Common;
namespace ProtonVPN.ProcessCommunication.Client;
public class NamedPipesConnectionFactory : INamedPipesConnectionFactory
{
private readonly RegistryUri _registryUri = RegistryUri.CreateLocalMachineUri(
NamedPipeConfiguration.REGISTRY_PATH, NamedPipeConfiguration.REGISTRY_KEY);
private readonly TimeSpan _minConnectionRetryInterval = TimeSpan.FromSeconds(1);
private readonly TimeSpan _maxConnectionRetryInterval = TimeSpan.FromSeconds(5);
private readonly TimeSpan _minRegistryRetryInterval = TimeSpan.FromSeconds(1);
private readonly TimeSpan _maxRegistryRetryInterval = TimeSpan.FromSeconds(5);
private readonly IRegistryEditor _registryEditor;
private TimeSpan? _connectionRetryInterval;
private TimeSpan? _registryRetryInterval;
private string? _pipeName;
private readonly CancellationTokenSource _cancellationTokenSource = new();
public NamedPipesConnectionFactory(IRegistryEditor registryEditor)
{
_registryEditor = registryEditor;
}
public async ValueTask ConnectAsync(SocketsHttpConnectionContext _,
CancellationToken cancellationToken = default)
{
return await InternalConnectAsync(CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, _cancellationTokenSource.Token).Token);
}
private async Task InternalConnectAsync(CancellationToken cancellationToken)
{
string pipeName = await GetPipeNameAsync(cancellationToken) ?? throw new ArgumentNullException("PipeName");
NamedPipeClientStream clientStream = new(
serverName: ".",
pipeName: pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.WriteThrough | PipeOptions.Asynchronous,
impersonationLevel: TokenImpersonationLevel.Anonymous);
try
{
_pipeName = pipeName;
await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false);
// No authorization checks are made because user processes without admin permissions such as this Client
// cannot get the executable path of SYSTEM processes such as our Service
return clientStream;
}
catch
{
clientStream?.Dispose();
_connectionRetryInterval = _connectionRetryInterval is null
? _minConnectionRetryInterval
: TimeSpanExtensions.Min(_connectionRetryInterval.Value * 2, _maxConnectionRetryInterval);
await Task.Delay(_connectionRetryInterval.Value, cancellationToken);
throw;
}
}
private async Task GetPipeNameAsync(CancellationToken cancellationToken)
{
_registryRetryInterval = null;
while (!cancellationToken.IsCancellationRequested)
{
string? pipeName = GetPipeName();
if (!string.IsNullOrWhiteSpace(pipeName))
{
return pipeName;
}
_registryRetryInterval = _registryRetryInterval is null
? _minRegistryRetryInterval
: TimeSpanExtensions.Min(_registryRetryInterval.Value * 2, _maxRegistryRetryInterval);
await Task.Delay(_registryRetryInterval.Value, cancellationToken);
continue;
}
return null;
}
private string? GetPipeName()
{
return _registryEditor.ReadString(_registryUri);
}
public void Stop()
{
_cancellationTokenSource.Cancel();
}
public bool HasPipeNameChanged()
{
return _pipeName != GetPipeName();
}
}