/* * 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.IO.Pipes; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; using ProtonVPN.Configurations.Contracts; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.ProcessCommunicationLogs; using ProtonVPN.OperatingSystems.Processes.Contracts; using ProtonVPN.OperatingSystems.Registries.Contracts; using ProtonVPN.ProcessCommunication.Common; namespace ProtonVPN.ProcessCommunication.Service; public class NamedPipeAuthorizationMiddleware { private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly IIssueReporter _issueReporter; private readonly IConfiguration _config; private readonly IPipeStreamProcessIdentifier _pipeStreamProcessIdentifier; private readonly IRegistryEditor _registryEditor; private readonly Func _recreateAndStartAsyncFunc; private readonly RegistryUri _registryUri; private const string INSTALLED_FOLDER_VERSION_PATTERN = @"\\v(\d+\.\d+\.\d+)\\"; private readonly Regex _installedFolderVersionRegex = new(INSTALLED_FOLDER_VERSION_PATTERN); private event EventHandler _invokingServiceStop; private bool _wasServiceStopRequested; public NamedPipeAuthorizationMiddleware(RequestDelegate next, ILogger logger, IIssueReporter issueReporter, IConfiguration config, IPipeStreamProcessIdentifier pipeStreamProcessIdentifier, IRegistryEditor registryEditor, EventHandler invokingServiceStop, Func recreateAndStartAsyncFunction) { _next = next; _logger = logger; _issueReporter = issueReporter; _config = config; _pipeStreamProcessIdentifier = pipeStreamProcessIdentifier; _registryEditor = registryEditor; _recreateAndStartAsyncFunc = recreateAndStartAsyncFunction; _invokingServiceStop = invokingServiceStop; _registryUri = RegistryUri.CreateLocalMachineUri($@"SYSTEM\CurrentControlSet\Services\{_config.ServiceName}", "ImagePath"); } public async Task InvokeAsync(HttpContext context) { AuthorizationResult authorizationResult = await AuthorizeAsync(context); if (authorizationResult.StatusCode > 0) { context.Response.StatusCode = authorizationResult.StatusCode; string installedServicePath = Path.GetFullPath(_registryEditor.ReadString(_registryUri)); string clientProcessVersionString = GetVersionFromFilePath(authorizationResult.ClientProcessFileName); string serviceProcessVersionString = GetVersionFromFilePath(authorizationResult.ServerProcessFileName); string installedServiceVersionString = GetVersionFromFilePath(installedServicePath); if (Version.TryParse(clientProcessVersionString, out Version clientProcessVersion) && Version.TryParse(serviceProcessVersionString, out Version serviceProcessVersion) && clientProcessVersion != serviceProcessVersion && Version.TryParse(installedServiceVersionString, out Version installedServiceVersion) && installedServiceVersion > serviceProcessVersion) { string serviceStopDescription = $"Client Process Path: '{authorizationResult.ClientProcessFileName}' Version '{clientProcessVersionString}', " + $"Service Process Path: '{authorizationResult.ServerProcessFileName}' Version '{serviceProcessVersionString}', " + $"Installed Service Path: '{installedServicePath}' Version '{installedServiceVersionString}'"; await StopServiceAsync(serviceStopDescription); context.Response.StatusCode = StatusCodes.Status409Conflict; } _logger.Error($"Sending HTTP status code {context.Response.StatusCode} to gRPC client. " + $"Client Process Path: '{authorizationResult.ClientProcessFileName}' Version '{clientProcessVersionString}', " + $"Service Process Path: '{authorizationResult.ServerProcessFileName}' Version '{serviceProcessVersionString}', " + $"Installed Service Path: '{installedServicePath}' Version '{installedServiceVersionString}'"); context.Response.Headers[HttpConfiguration.CLIENT_PROCESS_PATH] = authorizationResult.ClientProcessFileName; context.Response.Headers[HttpConfiguration.SERVICE_PROCESS_PATH] = authorizationResult.ServerProcessFileName; context.Response.Headers[HttpConfiguration.INSTALLED_SERVICE_PATH] = installedServicePath; context.Response.Headers[HttpConfiguration.CLIENT_PROCESS_VERSION] = clientProcessVersionString; context.Response.Headers[HttpConfiguration.SERVICE_PROCESS_VERSION] = serviceProcessVersionString; context.Response.Headers[HttpConfiguration.INSTALLED_SERVICE_VERSION] = installedServiceVersionString; } else { await _next(context); } } private async Task StopServiceAsync(string serviceStopDescription) { await _semaphore.WaitAsync(); try { if (_wasServiceStopRequested) { _logger.Warn( "Not requesting service to stop (due to version mismatch) as it was already requested."); } else { const string serviceStopTitle = "Stopping the service due to a version mismatch between the " + "service and client processes when a more recent version of the service is installed."; _logger.Warn($"{serviceStopTitle} {serviceStopDescription}"); _issueReporter.CaptureMessage(serviceStopTitle, serviceStopDescription); _invokingServiceStop?.Invoke(this, EventArgs.Empty); _wasServiceStopRequested = true; } } finally { _semaphore.Release(); } } private string GetVersionFromFilePath(string filePath) { MatchCollection matches = _installedFolderVersionRegex.Matches(filePath); if (matches.Count > 0) { Match lastMatch = matches[matches.Count - 1]; return lastMatch.Groups[1].Value; } return string.Empty; } private async Task AuthorizeAsync(HttpContext context) { try { NamedPipeServerStream namedPipe = GetNamedPipe(context); string clientProcessPath = _pipeStreamProcessIdentifier.GetClientProcessFullFilePath(namedPipe) ?? string.Empty; string serverProcessPath = _pipeStreamProcessIdentifier.GetServerProcessFullFilePath(namedPipe) ?? string.Empty; string expectedServiceProcessPath = Path.GetFullPath(_config.ServiceExePath); if (!expectedServiceProcessPath.Equals(serverProcessPath, StringComparison.InvariantCultureIgnoreCase)) { _logger.Warn($"The owner of the Named Pipe is not this Service. Dispose and recreate."); await _recreateAndStartAsyncFunc(); return AuthorizationResult.Error(StatusCodes.Status409Conflict, clientProcessPath, serverProcessPath); } string expectedClientProcessPath = Path.GetFullPath(_config.ClientExePath); if (expectedClientProcessPath.Equals(clientProcessPath, StringComparison.InvariantCultureIgnoreCase)) { return AuthorizationResult.Ok(); } _logger.Warn($"The connected client is unauthorized. " + $"Client path: '{clientProcessPath}'. Expected: '{expectedClientProcessPath}'."); return AuthorizationResult.Error(StatusCodes.Status401Unauthorized, clientProcessPath, serverProcessPath); } catch (Exception ex) { _logger.Error("An exception was thrown when checking if the gRPC client is authorized.", ex); _issueReporter.CaptureError(ex); return AuthorizationResult.Error(StatusCodes.Status500InternalServerError); } } // The properties needed are under internal types and therefore can only be obtained through reflection private NamedPipeServerStream GetNamedPipe(HttpContext context) { Type http2StreamType = Type.GetType("Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Stream, Microsoft.AspNetCore.Server.Kestrel.Core"); Type namedPipesConnectionType = Type.GetType("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal.NamedPipeConnection, Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes"); PropertyInfo connectionContextProperty = http2StreamType.GetProperty("ConnectionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); object connectionContext = connectionContextProperty.GetValue(context.Features); PropertyInfo namedPipeProperty = namedPipesConnectionType.GetProperty("NamedPipe", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); NamedPipeServerStream namedPipe = (NamedPipeServerStream)namedPipeProperty.GetValue(connectionContext); return namedPipe; } }