From 2f365923603be27b27ad81835b98dedc3298d94b Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 29 Feb 2024 14:43:23 +1000 Subject: [PATCH 1/2] stub out encryption config --- src/SeqCli/Config/SeqCliConfig.cs | 1 + .../Config/SeqCliEncryptionProviderConfig.cs | 24 ++++ src/SeqCli/Encryptor/ExternalEncryption.cs | 116 ++++++++++++++++++ src/SeqCli/Encryptor/IEncryption.cs | 7 ++ src/SeqCli/Encryptor/PlaintextEncryption.cs | 14 +++ .../Encryptor/WindowsNativeEncryption.cs | 31 +++++ src/SeqCli/SeqCli.csproj | 1 + src/SeqCli/Util/PasswordHash.cs | 28 +++++ 8 files changed, 222 insertions(+) create mode 100644 src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs create mode 100644 src/SeqCli/Encryptor/ExternalEncryption.cs create mode 100644 src/SeqCli/Encryptor/IEncryption.cs create mode 100644 src/SeqCli/Encryptor/PlaintextEncryption.cs create mode 100644 src/SeqCli/Encryptor/WindowsNativeEncryption.cs create mode 100644 src/SeqCli/Util/PasswordHash.cs diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index 307f14b8..8ff11a08 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -54,6 +54,7 @@ public static void Write(SeqCliConfig data) public ConnectionConfig Connection { get; set; } = new ConnectionConfig(); public OutputConfig Output { get; set; } = new(); public ForwarderConfig Forwarder { get; set; } = new(); + public SeqCliEncryptionProviderConfig EncryptionProviderProvider { get; set; } = new SeqCliEncryptionProviderConfig(); public Dictionary Profiles { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs new file mode 100644 index 00000000..c7818e18 --- /dev/null +++ b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs @@ -0,0 +1,24 @@ +// Copyright 2024 Datalust Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SeqCli.Config; + +public class SeqCliEncryptionProviderConfig +{ + public string? Encryptor { get; set; } + public string? EncryptorArgs { get; set; } + + public string? Decryptor { get; set; } + public string? DecryptorArgs { get; set; } +} \ No newline at end of file diff --git a/src/SeqCli/Encryptor/ExternalEncryption.cs b/src/SeqCli/Encryptor/ExternalEncryption.cs new file mode 100644 index 00000000..b9db753f --- /dev/null +++ b/src/SeqCli/Encryptor/ExternalEncryption.cs @@ -0,0 +1,116 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Text; +using System.Threading; +using SeqCli.Config; + +namespace SeqCli.Encryptor; + +public class ExternalEncryption : IEncryption +{ + public ExternalEncryption(SeqCliEncryptionProviderConfig providerConfig) + { + _encryptor = providerConfig.Encryptor!; + _encryptorArgs = providerConfig.EncryptorArgs; + + _decryptor = providerConfig.Decryptor!; + _decryptorArgs = providerConfig.DecryptorArgs; + } + + readonly string _encryptor; + readonly string? _encryptorArgs; + readonly string _decryptor; + readonly string? _decryptorArgs; + + public byte[] Encrypt(byte[] unencrypted) + { + var exit = Invoke(_encryptor, _encryptorArgs, unencrypted, out var encrypted, out var err); + if (exit != 0) + { + throw new Exception($"Encryptor failed with exit code {exit} and produced: {err}"); + } + + return encrypted; + } + + public byte[] Decrypt(byte[] encrypted) + { + var exit = Invoke(_decryptor, _decryptorArgs, encrypted, out var decrypted, out var err); + if (exit != 0) + { + throw new Exception($"Decryptor failed with exit code {exit} and produced: {err}"); + } + + return decrypted; + } + + static int Invoke(string fullExePath, string? args, byte[] stdin, out byte[] stdout, out string stderr) + { + var startInfo = new ProcessStartInfo + { + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + ErrorDialog = false, + FileName = fullExePath, + Arguments = args + }; + + using var process = Process.Start(startInfo); + using var errorComplete = new ManualResetEvent(false); + + if (process == null) + throw new InvalidOperationException("The process did not start."); + + var stderrBuf = new StringBuilder(); + process.ErrorDataReceived += (o, e) => + { + if (e.Data == null) + // ReSharper disable once AccessToDisposedClosure + errorComplete.Set(); + else + stderrBuf.Append(e.Data); + }; + process.BeginErrorReadLine(); + + process.StandardInput.BaseStream.Write(stdin); + process.StandardInput.BaseStream.Close(); + + var stdoutBuf = ArrayPool.Shared.Rent(512); + var stdoutBufLength = 0; + while (true) + { + var remaining = stdoutBuf.Length - stdoutBufLength; + if (remaining == 0) + { + var newBuffer = ArrayPool.Shared.Rent(stdoutBuf.Length * 2); + stdoutBuf.CopyTo(newBuffer.AsSpan()); + + ArrayPool.Shared.Return(stdoutBuf); + stdoutBuf = newBuffer; + + remaining = stdoutBuf.Length - stdoutBufLength; + } + + var read = process.StandardOutput.BaseStream.Read(stdoutBuf, stdoutBufLength, remaining); + + if (read == 0) + { + break; + } + + stdoutBufLength += read; + } + + errorComplete.WaitOne(); + stderr = stderrBuf.ToString(); + + stdout = stdoutBuf.AsSpan()[..stdoutBufLength].ToArray(); + ArrayPool.Shared.Return(stdoutBuf); + + return process.ExitCode; + } +} \ No newline at end of file diff --git a/src/SeqCli/Encryptor/IEncryption.cs b/src/SeqCli/Encryptor/IEncryption.cs new file mode 100644 index 00000000..0294fa82 --- /dev/null +++ b/src/SeqCli/Encryptor/IEncryption.cs @@ -0,0 +1,7 @@ +namespace SeqCli.Encryptor; + +public interface IEncryption +{ + public byte[] Encrypt(byte[] unencrypted); + public byte[] Decrypt(byte[] encrypted); +} \ No newline at end of file diff --git a/src/SeqCli/Encryptor/PlaintextEncryption.cs b/src/SeqCli/Encryptor/PlaintextEncryption.cs new file mode 100644 index 00000000..53a8df3e --- /dev/null +++ b/src/SeqCli/Encryptor/PlaintextEncryption.cs @@ -0,0 +1,14 @@ +namespace SeqCli.Encryptor; + +class PlaintextEncryption : IEncryption +{ + public byte[] Encrypt(byte[] unencrypted) + { + return unencrypted; + } + + public byte[] Decrypt(byte[] encrypted) + { + return encrypted; + } +} \ No newline at end of file diff --git a/src/SeqCli/Encryptor/WindowsNativeEncryption.cs b/src/SeqCli/Encryptor/WindowsNativeEncryption.cs new file mode 100644 index 00000000..323a82df --- /dev/null +++ b/src/SeqCli/Encryptor/WindowsNativeEncryption.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using SeqCli.Util; + +namespace SeqCli.Encryptor; + +public class WindowsNativeEncryption : IEncryption +{ + public byte[] Encrypt(byte[] unencrypted) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException("Windows native encryption is only supported on Windows"); + + var salt = PasswordHash.GenerateSalt(); + var data = ProtectedData.Protect(unencrypted, salt, DataProtectionScope.LocalMachine); + + return [..data, ..salt]; + } + + public byte[] Decrypt(byte[] encrypted) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException("Windows native encryption is only supported on Windows"); + + var data = encrypted[..^16]; + var salt = encrypted[^16..]; + + return ProtectedData.Unprotect(data, salt, DataProtectionScope.LocalMachine); + } +} \ No newline at end of file diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index 5d83f459..c29883b2 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -47,6 +47,7 @@ + diff --git a/src/SeqCli/Util/PasswordHash.cs b/src/SeqCli/Util/PasswordHash.cs new file mode 100644 index 00000000..91576624 --- /dev/null +++ b/src/SeqCli/Util/PasswordHash.cs @@ -0,0 +1,28 @@ +using System; +using System.Security.Cryptography; + +namespace SeqCli.Util; + +static class PasswordHash +{ + const int SaltSize = 16, + HashSize = 64, + HashIter = 500_000; + + public static byte[] GenerateSalt() + { + var salt = new byte[SaltSize]; + using var cp = RandomNumberGenerator.Create(); + cp.GetBytes(salt); + return salt; + } + + public static byte[] Calculate(string password, byte[] salt) + { + if (password == null) throw new ArgumentNullException(nameof(password)); + if (salt == null) throw new ArgumentNullException(nameof(salt)); + + using var algorithm = new Rfc2898DeriveBytes(password, salt, HashIter, HashAlgorithmName.SHA512); + return algorithm.GetBytes(HashSize); + } +} \ No newline at end of file From 6877c1f88779b018639de2d5b826e5df1d71dfb4 Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 29 Feb 2024 15:05:53 +1000 Subject: [PATCH 2/2] get things building on windows --- .../Cli/Commands/Forwarder/InstallCommand.cs | 102 +++++++++--------- .../Cli/Commands/Forwarder/RestartCommand.cs | 32 +++--- .../Cli/Commands/Forwarder/RunCommand.cs | 3 + .../Cli/Commands/Forwarder/StartCommand.cs | 28 ++--- .../Cli/Commands/Forwarder/StatusCommand.cs | 18 ++-- .../Cli/Commands/Forwarder/StopCommand.cs | 28 ++--- .../Commands/Forwarder/UninstallCommand.cs | 16 +-- .../DpapiMachineScopeDataProtection.cs | 2 + .../SeqForwarderWindowsService.cs | 2 + .../Forwarder/Util/ServiceConfiguration.cs | 20 ++-- src/SeqCli/Program.cs | 4 + src/SeqCli/SeqCli.csproj | 14 ++- test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj | 2 +- test/SeqCli.Tests/SeqCli.Tests.csproj | 2 +- 14 files changed, 160 insertions(+), 113 deletions(-) diff --git a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs index 7dbc9873..4fc70ed0 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/InstallCommand.cs @@ -15,20 +15,26 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.ServiceProcess; +using System.Threading.Tasks; using Seq.Forwarder.Cli.Features; using Seq.Forwarder.Config; using Seq.Forwarder.ServiceProcess; using Seq.Forwarder.Util; +using SeqCli; +using SeqCli.Cli; +using SeqCli.Cli.Features; // ReSharper disable once ClassNeverInstantiated.Global namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "install", "Install the Seq Forwarder as a Windows service")] + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class InstallCommand : Command { readonly StoragePathFeature _storagePath; @@ -51,70 +57,70 @@ public InstallCommand() string ServiceUsername => _serviceCredentials.IsUsernameSpecified ? _serviceCredentials.Username : "NT AUTHORITY\\LocalService"; - protected override int Run(TextWriter cout) + protected override Task Run() { try { if (!_setup) { - Install(cout); - return 0; + Install(); + return Task.FromResult(0); } - var exit = Setup(cout); + var exit = Setup(); if (exit == 0) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Setup completed successfully."); Console.ResetColor(); } - return exit; + return Task.FromResult(exit); } catch (DirectoryNotFoundException dex) { - cout.WriteLine("Could not install the service, directory not found: " + dex.Message); - return -1; + Console.WriteLine("Could not install the service, directory not found: " + dex.Message); + return Task.FromResult(-1); } catch (Exception ex) { - cout.WriteLine("Could not install the service: " + ex.Message); - return -1; + Console.WriteLine("Could not install the service: " + ex.Message); + return Task.FromResult(-1); } } - int Setup(TextWriter cout) + int Setup() { ServiceController controller; try { - cout.WriteLine("Checking the status of the Seq Forwarder service..."); + Console.WriteLine("Checking the status of the Seq Forwarder service..."); controller = new ServiceController(SeqForwarderWindowsService.WindowsServiceName); - cout.WriteLine("Status is {0}", controller.Status); + Console.WriteLine("Status is {0}", controller.Status); } catch (InvalidOperationException) { - Install(cout); + Install(); var controller2 = new ServiceController(SeqForwarderWindowsService.WindowsServiceName); - return Start(controller2, cout); + return Start(controller2); } - cout.WriteLine("Service is installed; checking path and dependency configuration..."); - Reconfigure(controller, cout); + Console.WriteLine("Service is installed; checking path and dependency configuration..."); + Reconfigure(controller); if (controller.Status != ServiceControllerStatus.Running) - return Start(controller, cout); + return Start(controller); return 0; } - static void Reconfigure(ServiceController controller, TextWriter cout) + static void Reconfigure(ServiceController controller) { var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); - if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" depend= Winmgmt/Tcpip/CryptSvc", cout.WriteLine, cout.WriteLine)) - cout.WriteLine("Could not reconfigure service dependencies; ignoring."); + if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" depend= Winmgmt/Tcpip/CryptSvc", Console.WriteLine, Console.WriteLine)) + Console.WriteLine("Could not reconfigure service dependencies; ignoring."); - if (!ServiceConfiguration.GetServiceBinaryPath(controller, cout, out var path)) + if (!ServiceConfiguration.GetServiceBinaryPath(controller, out var path)) return; var current = "\"" + Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName) + "\""; @@ -124,57 +130,57 @@ static void Reconfigure(ServiceController controller, TextWriter cout) var seqRun = path.IndexOf(Program.BinaryName + "\" run", StringComparison.OrdinalIgnoreCase); if (seqRun == -1) { - cout.WriteLine("Current binary path is an unrecognized format."); + Console.WriteLine("Current binary path is an unrecognized format."); return; } - cout.WriteLine("Existing service binary path is: {0}", path); + Console.WriteLine("Existing service binary path is: {0}", path); var trimmed = path.Substring((seqRun + Program.BinaryName + " ").Length); var newPath = current + trimmed; - cout.WriteLine("Updating service binary path configuration to: {0}", newPath); + Console.WriteLine("Updating service binary path configuration to: {0}", newPath); var escaped = newPath.Replace("\"", "\\\""); - if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" binPath= \"" + escaped + "\"", cout.WriteLine, cout.WriteLine)) + if (0 != CaptiveProcess.Run(sc, "config \"" + controller.ServiceName + "\" binPath= \"" + escaped + "\"", Console.WriteLine, Console.WriteLine)) { - cout.WriteLine("Could not reconfigure service path; ignoring."); + Console.WriteLine("Could not reconfigure service path; ignoring."); return; } - cout.WriteLine("Service binary path reconfigured successfully."); + Console.WriteLine("Service binary path reconfigured successfully."); } - static int Start(ServiceController controller, TextWriter cout) + static int Start(ServiceController controller) { controller.Start(); if (controller.Status != ServiceControllerStatus.Running) { - cout.WriteLine("Waiting up to 60 seconds for the service to start (currently: " + controller.Status + ")..."); + Console.WriteLine("Waiting up to 60 seconds for the service to start (currently: " + controller.Status + ")..."); controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(60)); } if (controller.Status == ServiceControllerStatus.Running) { - cout.WriteLine("Started."); + Console.WriteLine("Started."); return 0; } - cout.WriteLine("The service hasn't started successfully."); + Console.WriteLine("The service hasn't started successfully."); return -1; } [DllImport("shlwapi.dll")] static extern bool PathIsNetworkPath(string pszPath); - void Install(TextWriter cout) + void Install() { - cout.WriteLine("Installing service..."); + Console.WriteLine("Installing service..."); if (PathIsNetworkPath(_storagePath.StorageRootPath)) throw new ArgumentException("Seq requires a local (or SAN) storage location; network shares are not supported."); - cout.WriteLine($"Updating the configuration in {_storagePath.ConfigFilePath}..."); + Console.WriteLine($"Updating the configuration in {_storagePath.ConfigFilePath}..."); var config = SeqForwarderConfig.ReadOrInit(_storagePath.ConfigFilePath); if (!string.IsNullOrEmpty(_listenUri.ListenUri)) @@ -190,21 +196,21 @@ void Install(TextWriter cout) "If a service user account is specified, a password for the account must also be specified."); // https://technet.microsoft.com/en-us/library/cc794944(v=ws.10).aspx - cout.WriteLine($"Ensuring {_serviceCredentials.Username} is granted 'Log on as a Service' rights..."); + Console.WriteLine($"Ensuring {_serviceCredentials.Username} is granted 'Log on as a Service' rights..."); AccountRightsHelper.EnsureServiceLogOnRights(_serviceCredentials.Username); } - cout.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}..."); + Console.WriteLine($"Granting {ServiceUsername} rights to {_storagePath.StorageRootPath}..."); GiveFullControl(_storagePath.StorageRootPath); - cout.WriteLine($"Granting {ServiceUsername} rights to {config.Diagnostics.InternalLogPath}..."); + Console.WriteLine($"Granting {ServiceUsername} rights to {config.Diagnostics.InternalLogPath}..."); GiveFullControl(config.Diagnostics.InternalLogPath); var listenUri = MakeListenUriReservationPattern(config.Api.ListenUri); - cout.WriteLine($"Adding URL reservation at {listenUri} for {ServiceUsername}..."); - var netshResult = CaptiveProcess.Run("netsh", $"http add urlacl url={listenUri} user=\"{ServiceUsername}\"", cout.WriteLine, cout.WriteLine); + Console.WriteLine($"Adding URL reservation at {listenUri} for {ServiceUsername}..."); + var netshResult = CaptiveProcess.Run("netsh", $"http add urlacl url={listenUri} user=\"{ServiceUsername}\"", Console.WriteLine, Console.WriteLine); if (netshResult != 0) - cout.WriteLine($"Could not add URL reservation for {listenUri}: `netsh` returned {netshResult}; ignoring"); + Console.WriteLine($"Could not add URL reservation for {listenUri}: `netsh` returned {netshResult}; ignoring"); var exePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, Program.BinaryName); var forwarderRunCmdline = $"\"{exePath}\" run --storage=\"{_storagePath.StorageRootPath}\""; @@ -220,19 +226,19 @@ void Install(TextWriter cout) scCmdline += $" obj= {_serviceCredentials.Username} password= {_serviceCredentials.Password}"; var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); - if (0 != CaptiveProcess.Run(sc, scCmdline, cout.WriteLine, cout.WriteLine)) + if (0 != CaptiveProcess.Run(sc, scCmdline, Console.WriteLine, Console.WriteLine)) { throw new ArgumentException("Service setup failed"); } - cout.WriteLine("Setting service restart policy..."); - if (0 != CaptiveProcess.Run(sc, $"failure \"{SeqForwarderWindowsService.WindowsServiceName}\" actions= restart/60000/restart/60000/restart/60000// reset= 600000", cout.WriteLine, cout.WriteLine)) - cout.WriteLine("Could not set service restart policy; ignoring"); - cout.WriteLine("Setting service description..."); - if (0 != CaptiveProcess.Run(sc, $"description \"{SeqForwarderWindowsService.WindowsServiceName}\" \"Durable storage and forwarding of application log events\"", cout.WriteLine, cout.WriteLine)) - cout.WriteLine("Could not set service description; ignoring"); + Console.WriteLine("Setting service restart policy..."); + if (0 != CaptiveProcess.Run(sc, $"failure \"{SeqForwarderWindowsService.WindowsServiceName}\" actions= restart/60000/restart/60000/restart/60000// reset= 600000", Console.WriteLine, Console.WriteLine)) + Console.WriteLine("Could not set service restart policy; ignoring"); + Console.WriteLine("Setting service description..."); + if (0 != CaptiveProcess.Run(sc, $"description \"{SeqForwarderWindowsService.WindowsServiceName}\" \"Durable storage and forwarding of application log events\"", Console.WriteLine, Console.WriteLine)) + Console.WriteLine("Could not set service description; ignoring"); - cout.WriteLine("Service installed successfully."); + Console.WriteLine("Service installed successfully."); } void GiveFullControl(string target) diff --git a/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs index fb97172a..bf04e666 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RestartCommand.cs @@ -15,18 +15,22 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.ServiceProcess; +using System.Threading.Tasks; using Seq.Forwarder.ServiceProcess; +using SeqCli.Cli; // ReSharper disable UnusedType.Global namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "restart", "Restart the Windows service")] + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class RestartCommand : Command { - protected override int Run(TextWriter cout) + protected override Task Run() { try { @@ -34,47 +38,47 @@ protected override int Run(TextWriter cout) if (controller.Status != ServiceControllerStatus.Stopped) { - cout.WriteLine("Stopping {0}...", controller.ServiceName); + Console.WriteLine("Stopping {0}...", controller.ServiceName); controller.Stop(); if (controller.Status != ServiceControllerStatus.Stopped) { - cout.WriteLine("Waiting up to 60 seconds for the service to stop (currently: " + + Console.WriteLine("Waiting up to 60 seconds for the service to stop (currently: " + controller.Status + ")..."); controller.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60)); } if (controller.Status != ServiceControllerStatus.Stopped) { - cout.WriteLine("The service hasn't stopped successfully."); - return -1; + Console.WriteLine("The service hasn't stopped successfully."); + return Task.FromResult(-1); } } - cout.WriteLine("Starting {0}...", controller.ServiceName); + Console.WriteLine("Starting {0}...", controller.ServiceName); controller.Start(); if (controller.Status != ServiceControllerStatus.Running) { - cout.WriteLine("Waiting up to 15 seconds for the service to start (currently: " + controller.Status + ")..."); + Console.WriteLine("Waiting up to 15 seconds for the service to start (currently: " + controller.Status + ")..."); controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(15)); } if (controller.Status == ServiceControllerStatus.Running) { - cout.WriteLine("Started."); - return 0; + Console.WriteLine("Started."); + return Task.FromResult(0); } - cout.WriteLine("The service hasn't started successfully."); - return -1; + Console.WriteLine("The service hasn't started successfully."); + return Task.FromResult(-1); } catch (Exception ex) { - cout.WriteLine(ex.Message); + Console.WriteLine(ex.Message); if (ex.InnerException != null) - cout.WriteLine(ex.InnerException.Message); - return 1; + Console.WriteLine(ex.InnerException.Message); + return Task.FromResult(1); } } } diff --git a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs index 0af8e266..4c39c38b 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs @@ -18,8 +18,10 @@ using Serilog.Events; using Serilog.Formatting.Compact; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; @@ -198,6 +200,7 @@ static string GetRollingLogFilePathFormat(string internalLogPath) return Path.Combine(internalLogPath, "seq-forwarder-.log"); } + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] static int RunService(ServerService service) { #if WINDOWS diff --git a/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs index 2b48ca4f..150c9f6d 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StartCommand.cs @@ -15,50 +15,54 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.ServiceProcess; +using System.Threading.Tasks; using Seq.Forwarder.ServiceProcess; +using SeqCli.Cli; namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "start", "Start the Windows service")] + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StartCommand : Command { - protected override int Run(TextWriter cout) + protected override Task Run() { try { var controller = new ServiceController(SeqForwarderWindowsService.WindowsServiceName); if (controller.Status != ServiceControllerStatus.Stopped) { - cout.WriteLine("Cannot start {0}, current status is: {1}", controller.ServiceName, controller.Status); - return -1; + Console.WriteLine("Cannot start {0}, current status is: {1}", controller.ServiceName, controller.Status); + return Task.FromResult(-1); } - cout.WriteLine("Starting {0}...", controller.ServiceName); + Console.WriteLine("Starting {0}...", controller.ServiceName); controller.Start(); if (controller.Status != ServiceControllerStatus.Running) { - cout.WriteLine("Waiting up to 15 seconds for the service to start (currently: " + controller.Status + ")..."); + Console.WriteLine("Waiting up to 15 seconds for the service to start (currently: " + controller.Status + ")..."); controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(15)); } if (controller.Status == ServiceControllerStatus.Running) { - cout.WriteLine("Started."); - return 0; + Console.WriteLine("Started."); + return Task.FromResult(0); } - cout.WriteLine("The service hasn't started successfully."); - return -1; + Console.WriteLine("The service hasn't started successfully."); + return Task.FromResult(-1); } catch (Exception ex) { - cout.WriteLine(ex.Message); + Console.WriteLine(ex.Message); if (ex.InnerException != null) - cout.WriteLine(ex.InnerException.Message); - return -1; + Console.WriteLine(ex.InnerException.Message); + return Task.FromResult(-1); } } } diff --git a/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs index c9261d93..40b05669 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StatusCommand.cs @@ -15,35 +15,39 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.ServiceProcess; +using System.Threading.Tasks; using Seq.Forwarder.ServiceProcess; +using SeqCli.Cli; namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "status", "Show the status of the Seq Forwarder service")] + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StatusCommand : Command { - protected override int Run(TextWriter cout) + protected override Task Run() { try { var controller = new ServiceController(SeqForwarderWindowsService.WindowsServiceName); - cout.WriteLine("The Seq Forwarder service is installed and {0}.", controller.Status.ToString().ToLowerInvariant()); + Console.WriteLine("The Seq Forwarder service is installed and {0}.", controller.Status.ToString().ToLowerInvariant()); } catch (InvalidOperationException) { - cout.WriteLine("The Seq Forwarder service is not installed."); + Console.WriteLine("The Seq Forwarder service is not installed."); } catch (Exception ex) { - cout.WriteLine(ex.Message); + Console.WriteLine(ex.Message); if (ex.InnerException != null) - cout.WriteLine(ex.InnerException.Message); - return 1; + Console.WriteLine(ex.InnerException.Message); + return Task.FromResult(1); } - return 0; + return Task.FromResult(1); } } } diff --git a/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs index 66c16637..d5ee2f1a 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/StopCommand.cs @@ -15,16 +15,20 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.ServiceProcess; +using System.Threading.Tasks; using Seq.Forwarder.ServiceProcess; +using SeqCli.Cli; namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "stop", "Stop the Windows service")] + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class StopCommand : Command { - protected override int Run(TextWriter cout) + protected override Task Run() { try { @@ -32,34 +36,34 @@ protected override int Run(TextWriter cout) if (controller.Status != ServiceControllerStatus.Running) { - cout.WriteLine("Cannot stop {0}, current status is: {1}", controller.ServiceName, controller.Status); - return -1; + Console.WriteLine("Cannot stop {0}, current status is: {1}", controller.ServiceName, controller.Status); + return Task.FromResult(-1); } - cout.WriteLine("Stopping {0}...", controller.ServiceName); + Console.WriteLine("Stopping {0}...", controller.ServiceName); controller.Stop(); if (controller.Status != ServiceControllerStatus.Stopped) { - cout.WriteLine("Waiting up to 60 seconds for the service to stop (currently: " + controller.Status + ")..."); + Console.WriteLine("Waiting up to 60 seconds for the service to stop (currently: " + controller.Status + ")..."); controller.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60)); } if (controller.Status == ServiceControllerStatus.Stopped) { - cout.WriteLine("Stopped."); - return 0; + Console.WriteLine("Stopped."); + return Task.FromResult(0); } - cout.WriteLine("The service hasn't stopped successfully."); - return -1; + Console.WriteLine("The service hasn't stopped successfully."); + return Task.FromResult(-1); } catch (Exception ex) { - cout.WriteLine(ex.Message); + Console.WriteLine(ex.Message); if (ex.InnerException != null) - cout.WriteLine(ex.InnerException.Message); - return -1; + Console.WriteLine(ex.InnerException.Message); + return Task.FromResult(-1); } } } diff --git a/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs index ee717ab5..96fbed59 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/UninstallCommand.cs @@ -16,32 +16,34 @@ using System; using System.IO; +using System.Threading.Tasks; using Seq.Forwarder.ServiceProcess; using Seq.Forwarder.Util; +using SeqCli.Cli; namespace Seq.Forwarder.Cli.Commands { [Command("forwarder", "uninstall", "Uninstall the Windows service")] class UninstallCommand : Command { - protected override int Run(TextWriter cout) + protected override Task Run() { try { - cout.WriteLine("Uninstalling service..."); + Console.WriteLine("Uninstalling service..."); var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); - var exitCode = CaptiveProcess.Run(sc, $"delete \"{SeqForwarderWindowsService.WindowsServiceName}\"", cout.WriteLine, cout.WriteLine); + var exitCode = CaptiveProcess.Run(sc, $"delete \"{SeqForwarderWindowsService.WindowsServiceName}\"", Console.WriteLine, Console.WriteLine); if (exitCode != 0) throw new InvalidOperationException($"The `sc.exe delete` call failed with exit code {exitCode}."); - cout.WriteLine("Service uninstalled successfully."); - return 0; + Console.WriteLine("Service uninstalled successfully."); + return Task.FromResult(0); } catch (Exception ex) { - cout.WriteLine("Could not uninstall the service: " + ex.Message); - return -1; + Console.WriteLine("Could not uninstall the service: " + ex.Message); + return Task.FromResult(-1); } } } diff --git a/src/SeqCli/Forwarder/Cryptography/DpapiMachineScopeDataProtection.cs b/src/SeqCli/Forwarder/Cryptography/DpapiMachineScopeDataProtection.cs index 134eaaa3..635bb415 100644 --- a/src/SeqCli/Forwarder/Cryptography/DpapiMachineScopeDataProtection.cs +++ b/src/SeqCli/Forwarder/Cryptography/DpapiMachineScopeDataProtection.cs @@ -15,11 +15,13 @@ #if WINDOWS using System; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text; namespace Seq.Forwarder.Cryptography { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class DpapiMachineScopeDataProtect : IStringDataProtector { public string Unprotect(string @protected) diff --git a/src/SeqCli/Forwarder/ServiceProcess/SeqForwarderWindowsService.cs b/src/SeqCli/Forwarder/ServiceProcess/SeqForwarderWindowsService.cs index 013f67be..a77efec1 100644 --- a/src/SeqCli/Forwarder/ServiceProcess/SeqForwarderWindowsService.cs +++ b/src/SeqCli/Forwarder/ServiceProcess/SeqForwarderWindowsService.cs @@ -14,12 +14,14 @@ #if WINDOWS +using System.Diagnostics.CodeAnalysis; using System.Net; using System.ServiceProcess; using Seq.Forwarder.Web.Host; namespace Seq.Forwarder.ServiceProcess { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] class SeqForwarderWindowsService : ServiceBase { readonly ServerService _serverService; diff --git a/src/SeqCli/Forwarder/Util/ServiceConfiguration.cs b/src/SeqCli/Forwarder/Util/ServiceConfiguration.cs index 2fda1fd5..b15c4775 100644 --- a/src/SeqCli/Forwarder/Util/ServiceConfiguration.cs +++ b/src/SeqCli/Forwarder/Util/ServiceConfiguration.cs @@ -23,16 +23,17 @@ namespace Seq.Forwarder.Util { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public static class ServiceConfiguration { - public static bool GetServiceBinaryPath(ServiceController controller, TextWriter cout, [MaybeNullWhen(false)] out string path) + public static bool GetServiceBinaryPath(ServiceController controller, [MaybeNullWhen(false)] out string path) { var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); var config = new StringBuilder(); - if (0 != CaptiveProcess.Run(sc, "qc \"" + controller.ServiceName + "\"", l => config.AppendLine(l), cout.WriteLine)) + if (0 != CaptiveProcess.Run(sc, "qc \"" + controller.ServiceName + "\"", l => config.AppendLine(l), Console.WriteLine)) { - cout.WriteLine("Could not query service path; ignoring."); + Console.WriteLine("Could not query service path; ignoring."); path = null; return false; } @@ -46,7 +47,7 @@ public static bool GetServiceBinaryPath(ServiceController controller, TextWriter if (line == null) { - cout.WriteLine("No existing binary path could be determined."); + Console.WriteLine("No existing binary path could be determined."); path = null; return false; } @@ -55,17 +56,16 @@ public static bool GetServiceBinaryPath(ServiceController controller, TextWriter return true; } - static bool GetServiceCommandLine(string serviceName, TextWriter cout, [MaybeNullWhen(false)] out string path) + static bool GetServiceCommandLine(string serviceName, [MaybeNullWhen(false)] out string path) { if (serviceName == null) throw new ArgumentNullException(nameof(serviceName)); - if (cout == null) throw new ArgumentNullException(nameof(cout)); var sc = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "sc.exe"); var config = new StringBuilder(); - if (0 != CaptiveProcess.Run(sc, "qc \"" + serviceName + "\"", l => config.AppendLine(l), cout.WriteLine)) + if (0 != CaptiveProcess.Run(sc, "qc \"" + serviceName + "\"", l => config.AppendLine(l), Console.WriteLine)) { - cout.WriteLine("Could not query service path; ignoring."); + Console.WriteLine("Could not query service path; ignoring."); path = null; return false; } @@ -79,7 +79,7 @@ static bool GetServiceCommandLine(string serviceName, TextWriter cout, [MaybeNul if (line == null) { - cout.WriteLine("No existing binary path could be determined."); + Console.WriteLine("No existing binary path could be determined."); path = null; return false; } @@ -92,7 +92,7 @@ public static bool GetServiceStoragePath(string serviceName, out string? storage { if (serviceName == null) throw new ArgumentNullException(nameof(serviceName)); - if (GetServiceCommandLine(serviceName, new StringWriter(), out var binpath) && + if (GetServiceCommandLine(serviceName, out var binpath) && binpath.Contains("--storage=\"")) { var start = binpath.IndexOf("--storage=\"", StringComparison.Ordinal) + 11; diff --git a/src/SeqCli/Program.cs b/src/SeqCli/Program.cs index e7d55cdc..f23952a4 100644 --- a/src/SeqCli/Program.cs +++ b/src/SeqCli/Program.cs @@ -26,6 +26,10 @@ namespace SeqCli; class Program { +#if WINDOWS + public const string BinaryName = "seqcli.exe"; +#endif + static async Task Main(string[] args) { var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Error); diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index c29883b2..fbb39cf1 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -15,6 +15,18 @@ false false false + true + true + true + + + WINDOWS + + + OSX + + + LINUX @@ -43,11 +55,11 @@ + - diff --git a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj index 2c417644..c5ee3ea4 100644 --- a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj +++ b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net8.0-windows + net8.0 diff --git a/test/SeqCli.Tests/SeqCli.Tests.csproj b/test/SeqCli.Tests/SeqCli.Tests.csproj index 30d682cc..e93f0a96 100644 --- a/test/SeqCli.Tests/SeqCli.Tests.csproj +++ b/test/SeqCli.Tests/SeqCli.Tests.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-windows + net8.0