From 42bcc905b9466f5e6fcdaa33e1c9da2963831a2c Mon Sep 17 00:00:00 2001 From: haga Date: Fri, 31 Jan 2025 00:38:44 +0100 Subject: [PATCH 1/3] Make unit test runnable in user mode under linux - use sudo -A instead of buggy pkexec (needs SUDO_ASKPASS utility) - configure dotnet to allow pcap `sudo setcap cap_net_raw,cap_net_admin=eip dotnet_physical_path` --- src/Fluxzy.Core/Misc/ProcessUtilX.cs | 48 +++++++++++++++++++ src/Fluxzy.Core/Misc/ProcessUtils.cs | 3 ++ .../System/OutOfProcAuthorityManager.cs | 1 - test/Fluxzy.Tests/Startup.cs | 5 +- test/Fluxzy.Tests/Utility.cs | 19 ++++---- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/Fluxzy.Core/Misc/ProcessUtilX.cs b/src/Fluxzy.Core/Misc/ProcessUtilX.cs index 3629e62bf..f98f76d4f 100644 --- a/src/Fluxzy.Core/Misc/ProcessUtilX.cs +++ b/src/Fluxzy.Core/Misc/ProcessUtilX.cs @@ -2,13 +2,18 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; namespace Fluxzy.Misc { internal static class ProcessUtilX { + private static readonly string[] AskPassBinaries = new[] { "ksshaskpass", "ssh-askpass", "lxqt-sudo" }; + + private static readonly HashSet RequiredCapabilities = new HashSet( new[] { "cap_net_raw", "cap_net_admin" }, StringComparer.OrdinalIgnoreCase); @@ -35,5 +40,48 @@ public static async Task HasCaptureCapabilities() return availableCapabilities.IsSupersetOf(RequiredCapabilities); } + + public static async Task GetExecutablePath(string executableName) + { + var processRunResult = await ProcessUtils.QuickRunAsync("which", executableName, null); + + if (processRunResult.ExitCode != 0) + return null; + + return processRunResult.StandardOutputMessage?.Trim('\r', '\n'); + } + + + public static async Task RunElevatedSudoALinux( + string commandName, string[] args, bool redirectStdOut, + string askPasswordPrompt, bool redirectStandardError = false) + { + var tasks = AskPassBinaries + .Select(binary => GetExecutablePath(binary)); + + var results = await Task.WhenAll(tasks); + var result = results.FirstOrDefault(x => x != null) ?? + throw new Exception( + $"No askpass binary found. Must install one of the following:" + + $" {string.Join(", ", AskPassBinaries)}"); + + var execCommandName = "sudo"; + var preArgs = new List() { "-A", "-E", "-p", $"{askPasswordPrompt}", commandName }; + + var startInfo = new ProcessStartInfo() { + FileName = execCommandName, + RedirectStandardOutput = redirectStdOut, + RedirectStandardError = redirectStandardError, + UseShellExecute = false, + }; + + startInfo.EnvironmentVariables["SUDO_ASKPASS"] = result; + foreach (var arg in preArgs.Concat(args)) + startInfo.ArgumentList.Add(arg); + + var process = Process.Start(startInfo); + + return process!; + } } } diff --git a/src/Fluxzy.Core/Misc/ProcessUtils.cs b/src/Fluxzy.Core/Misc/ProcessUtils.cs index 3887d58fc..82f95803e 100644 --- a/src/Fluxzy.Core/Misc/ProcessUtils.cs +++ b/src/Fluxzy.Core/Misc/ProcessUtils.cs @@ -244,6 +244,9 @@ public static bool IsCommandAvailable(string commandName) return process; } + + + public static Process? RunElevated( string commandName, string[] args, bool redirectStdOut, string askPasswordPrompt) diff --git a/src/Fluxzy/System/OutOfProcAuthorityManager.cs b/src/Fluxzy/System/OutOfProcAuthorityManager.cs index 2c6cc6bfa..4d9854488 100644 --- a/src/Fluxzy/System/OutOfProcAuthorityManager.cs +++ b/src/Fluxzy/System/OutOfProcAuthorityManager.cs @@ -31,7 +31,6 @@ await ProcessUtils.QuickRunAsync($"{_currentBinaryFullPath.RemoveFileExtension() $" cert uninstall {thumbPrint}"); // We are using pkexec for linux - var result = await ProcessUtils.QuickRunAsync("pkexec", $"{_currentBinaryFullPath.RemoveFileExtension()} cert uninstall {thumbPrint}"); diff --git a/test/Fluxzy.Tests/Startup.cs b/test/Fluxzy.Tests/Startup.cs index 132b64c3d..408b71946 100644 --- a/test/Fluxzy.Tests/Startup.cs +++ b/test/Fluxzy.Tests/Startup.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Linq; using System.Net; +using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; using Fluxzy.Certificates; @@ -50,8 +51,8 @@ public Startup(IMessageSink messageSink) CertificateContext.InstallDefaultCertificate(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - _ = Task.Run(async () => - await Utility.AcquireCapabilities(new FileInfo("fluxzynetcap").FullName)) + Task.Run(async () => { await Utility.AcquireCapabilitiesLinux(new FileInfo("fluxzynetcap").FullName); + }) .GetAwaiter().GetResult(); } } diff --git a/test/Fluxzy.Tests/Utility.cs b/test/Fluxzy.Tests/Utility.cs index 944e1f379..8ed4a522e 100644 --- a/test/Fluxzy.Tests/Utility.cs +++ b/test/Fluxzy.Tests/Utility.cs @@ -1,5 +1,7 @@ // Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak +using System; +using System.IO; using System.Threading.Tasks; using Fluxzy.Misc; @@ -11,25 +13,24 @@ public static class Utility /// Make an executable have the required capabilities /// /// - public static async Task AcquireCapabilities(string executablePath) + public static async Task AcquireCapabilitiesLinux(string executablePath) { + if (!File.Exists(executablePath)) { + executablePath = (await ProcessUtilX.GetExecutablePath(executablePath)) + ?? throw new InvalidOperationException("Executable not found"); + } + if (await ProcessUtilX.CanElevated()) return true; // Already root - no need to set capabilities if (!ProcessUtils.IsCommandAvailable("setcap")) return false; - if (!ProcessUtils.IsCommandAvailable("pkexec")) - return false; - - var fullCommand = $"setcap cap_net_raw,cap_net_admin=eip \"{executablePath}\""; - - var process = await ProcessUtils.RunElevatedAsync("pkexec", - new []{ "setcap", "cap_net_raw,cap_net_admin=eip", executablePath}, + var process = await ProcessUtilX.RunElevatedSudoALinux("setcap", + new []{ "cap_net_raw,cap_net_admin=eip", executablePath}, false, "Please enter your password to set the required capabilities"); await process!.WaitForExitAsync(); - return process.ExitCode == 0; } } From b4622aa1242beaa69e93be1ce8beedcb634520c4 Mon Sep 17 00:00:00 2001 From: haga Date: Fri, 31 Jan 2025 00:42:00 +0100 Subject: [PATCH 2/3] Prefer ssh-askpass first --- src/Fluxzy.Core/Misc/ProcessUtilX.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fluxzy.Core/Misc/ProcessUtilX.cs b/src/Fluxzy.Core/Misc/ProcessUtilX.cs index f98f76d4f..b2de96684 100644 --- a/src/Fluxzy.Core/Misc/ProcessUtilX.cs +++ b/src/Fluxzy.Core/Misc/ProcessUtilX.cs @@ -11,7 +11,7 @@ namespace Fluxzy.Misc { internal static class ProcessUtilX { - private static readonly string[] AskPassBinaries = new[] { "ksshaskpass", "ssh-askpass", "lxqt-sudo" }; + private static readonly string[] AskPassBinaries = new[] { "ssh-askpass", "ksshaskpass", "lxqt-sudo" }; private static readonly HashSet RequiredCapabilities = new HashSet( From 33f52707b2a868e3acebef6793942724c788d2aa Mon Sep 17 00:00:00 2001 From: haga Date: Fri, 31 Jan 2025 00:49:44 +0100 Subject: [PATCH 3/3] Stabilize test --- test/Fluxzy.Tests/UnitTests/Rules/AverageThrottlerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Fluxzy.Tests/UnitTests/Rules/AverageThrottlerTests.cs b/test/Fluxzy.Tests/UnitTests/Rules/AverageThrottlerTests.cs index 5cb5ef8ce..ed4ae6c71 100644 --- a/test/Fluxzy.Tests/UnitTests/Rules/AverageThrottlerTests.cs +++ b/test/Fluxzy.Tests/UnitTests/Rules/AverageThrottlerTests.cs @@ -62,6 +62,8 @@ public async Task SimpleThrottle(int delaySeconds) var stream = await response.Content.ReadAsStreamAsync(); await stream.CopyToAsync(Stream.Null); + await Task.Delay(500); + watch.Stop(); Assert.True(watch.ElapsedMilliseconds > delaySeconds * 1000,