From 2c3877e8023ea8bfa590dfd4d29a5d164ddb02b7 Mon Sep 17 00:00:00 2001 From: Richard Markiewicz Date: Tue, 18 Jun 2024 06:59:41 -0400 Subject: [PATCH] feat(installer): add a basic configuration check (#888) --- README.md | 1 - package/WindowsManaged/.gitignore | 3 +- .../WindowsManaged/Actions/CustomActions.cs | 382 +++++++++++++++++- package/WindowsManaged/Actions/FileAccess.cs | 30 ++ .../WindowsManaged/Actions/GatewayActions.cs | 14 + .../WindowsManaged/Actions/Impersonation.cs | 82 ++++ package/WindowsManaged/Actions/WinAPI.cs | 99 ++++- .../WindowsManaged/Configuration/Gateway.cs | 57 +++ .../WindowsManaged/Configuration/Listener.cs | 9 + package/WindowsManaged/Configuration/Ngrok.cs | 19 + .../Configuration/SubProvisionerPublicKey.cs | 13 + .../Configuration/Subscriber.cs | 9 + .../WindowsManaged/Configuration/Tunnel.cs | 25 ++ .../WindowsManaged/Configuration/WebApp.cs | 23 ++ .../WindowsManaged/DevolutionsGateway.csproj | 1 + .../Dialogs/ExitDialog.Designer.cs | 19 + package/WindowsManaged/Dialogs/ExitDialog.cs | 24 ++ package/WindowsManaged/Program.cs | 8 + .../Properties/GatewayProperties.g.cs | 36 +- .../Properties/GatewayProperties.g.tt | 5 +- .../Resources/DevolutionsGateway_en-us.wxl | 1 + .../Resources/DevolutionsGateway_fr-fr.wxl | 1 + package/WindowsManaged/Resources/Includes.cs | 2 + package/WindowsManaged/Resources/Strings.g.cs | 4 + .../Resources/Strings_en-US.json | 4 + .../Resources/Strings_fr-FR.json | 4 + 26 files changed, 854 insertions(+), 21 deletions(-) create mode 100644 package/WindowsManaged/Actions/FileAccess.cs create mode 100644 package/WindowsManaged/Actions/Impersonation.cs create mode 100644 package/WindowsManaged/Configuration/Gateway.cs create mode 100644 package/WindowsManaged/Configuration/Listener.cs create mode 100644 package/WindowsManaged/Configuration/Ngrok.cs create mode 100644 package/WindowsManaged/Configuration/SubProvisionerPublicKey.cs create mode 100644 package/WindowsManaged/Configuration/Subscriber.cs create mode 100644 package/WindowsManaged/Configuration/Tunnel.cs create mode 100644 package/WindowsManaged/Configuration/WebApp.cs diff --git a/README.md b/README.md index 4236a2267..2060fd72c 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,6 @@ Stable options are: * **Proto** (_String_): MUST be set to `http`. * **Domain** (_String_): The domain to request, as registered in the ngrok dashboard. - * **Metadata** (_String_): Arbitrary user-defined metadata that will appear in the ngrok service API when listing tunnel sessions. * **CircuitBreaker** (_Ratio_): Reject requests when 5XX responses exceed this ratio. * **Compression** (_Boolean_): Enable gzip compression for HTTP responses. diff --git a/package/WindowsManaged/.gitignore b/package/WindowsManaged/.gitignore index 47fa67f51..740731ab0 100644 --- a/package/WindowsManaged/.gitignore +++ b/package/WindowsManaged/.gitignore @@ -2,4 +2,5 @@ Debug Release wix fr-FR.* -*_missing.json \ No newline at end of file +*_missing.json +packages \ No newline at end of file diff --git a/package/WindowsManaged/Actions/CustomActions.cs b/package/WindowsManaged/Actions/CustomActions.cs index 1d722bcd3..58ba77fbb 100644 --- a/package/WindowsManaged/Actions/CustomActions.cs +++ b/package/WindowsManaged/Actions/CustomActions.cs @@ -4,23 +4,31 @@ using Microsoft.Win32; using Microsoft.Win32.SafeHandles; using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.ServiceProcess; using System.Text; +using Newtonsoft.Json; using WixSharp; using File = System.IO.File; +using DevolutionsGateway.Configuration; +using static DevolutionsGateway.Actions.WinAPI; +using System.Security.Principal; namespace DevolutionsGateway.Actions { public class CustomActions { + private const string GatewayConfigFile = "gateway.json"; + private static readonly string[] ConfigFiles = new[] { - "gateway.json", + GatewayConfigFile, "server.crt", "server.key", "provisioner.pem", @@ -33,6 +41,8 @@ public class CustomActions Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Devolutions", "Gateway"); + public const string DefaultUsersFile = "users.txt"; + [CustomAction] public static ActionResult CheckInstalledNetFx45Version(Session session) { @@ -422,6 +432,334 @@ public static ActionResult CreateProgramDataDirectory(Session session) return ActionResult.Success; } + [CustomAction] + public static ActionResult EvaluateConfiguration(Session session) + { + ActionResult result = ActionResult.Success; + Dictionary results = new Dictionary(); + + uint read = FILE_READ_DATA /* aka FILE_LIST_DIRECTORY */ | + FILE_READ_EA | FILE_EXECUTE /* aka FILE_TRAVERSE */ | + FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE; + uint write = read | FILE_WRITE_DATA /* aka FILE_ADD_FILE */ | + FILE_APPEND_DATA /* aka FILE_ADD_SUBDIRECTORY */ | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES; + uint modify = write | DELETE; + + // Attempt to open a path with the specified access, as a means to check for permissions + bool CanAccess(string path, bool isDirectory, uint desiredAccess) + { + using SafeFileHandle handle = CreateFile( + path, desiredAccess, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, IntPtr.Zero, + OPEN_EXISTING, + isDirectory ? FILE_FLAG_BACKUP_SEMANTICS : 0, + IntPtr.Zero); + + int lastError = Marshal.GetLastWin32Error(); + + if (handle.IsInvalid) + { + // ERROR_SUCCESS, ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND, ERROR_ACCESS_DENIED + if (!new[] {0, 2, 3, 5}.Contains(lastError)) + { + session.Log($"CreateFile failed (error: {lastError})"); + throw new Win32Exception(lastError); + } + } + + return !handle.IsInvalid; + } + + bool CheckAccess(string path, FileAccess desiredAccess, bool isDirectory) + { + if (string.IsNullOrEmpty(path)) + { + return true; + } + + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(ProgramDataDirectory, path); + } + + uint accessMask; + + switch (desiredAccess) + { + case FileAccess.Write: + { + accessMask = write; + break; + } + case FileAccess.Modify: + { + accessMask = modify; + break; + } + default: + { + accessMask = read; + break; + } + } + + session.Log($"checking effective access {accessMask} to {path}"); + + try + { + if (CanAccess(path, isDirectory, accessMask)) + { + results[path] = (true, desiredAccess, null); + + return true; + } + + results[path] = (false, desiredAccess, null); + + session.Log($"effective access to {path} does not match desired access {accessMask}"); + return false; + + } + catch (Exception e) + { + results[path] = (false, desiredAccess, e); + + session.Log($"failed to check effective access to {path}: {e.Message}"); + return false; + } + } + + IdentityReference account = + new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null).Translate(typeof(NTAccount)); + + try + { + string[] userDomain = account.Value.Split('\\'); + Gateway config = null; + + session.Log($"evaluating configuration as {account.Value}"); + + using (Impersonation _ = new Impersonation(userDomain[1], userDomain[0], string.Empty)) + { + if (!TryReadGatewayConfig(session, out config)) + { + session.Log("failed to load or parse the configuration file"); + } + + if (!CheckAccess(ProgramDataDirectory, FileAccess.Modify, true)) + { + result = ActionResult.Failure; + } + + List readFiles = new() + { + config.DelegationPrivateKeyFile, + config.ProvisionerPublicKeyFile, + config.ProvisionerPrivateKeyFile, + config.TlsCertificateSource == "External" ? config.TlsCertificateFile : null, + config.TlsCertificateSource == "External" ? config.TlsPrivateKeyFile : null, + }; + + foreach (string readFile in readFiles.Where(x => !string.IsNullOrEmpty(x))) + { + if (!CheckAccess(readFile, FileAccess.Read, false)) + { + result = ActionResult.Failure; + } + } + + List writeFiles = new() + { + (config.WebApp?.Enabled ?? false) && config.WebApp.Authentication == "Custom" + ? config.WebApp.UsersFile + : null, + }; + + foreach (string writeFile in writeFiles.Where(x => !string.IsNullOrEmpty(x))) + { + if (!CheckAccess(writeFile, FileAccess.Write, false)) + { + result = ActionResult.Failure; + } + } + } + + string jrlFile = config.JrlFile; + + if (!Path.IsPathRooted(jrlFile)) + { + jrlFile = Path.Combine(ProgramDataDirectory, jrlFile); + } + + List modifyFiles = new(); + + try + { + if (File.Exists(jrlFile)) + { + modifyFiles.Add(jrlFile); + } + } + catch + { + } + + using (Impersonation _ = new Impersonation(userDomain[1], userDomain[0], string.Empty)) + { + string logDirectory = ProgramDataDirectory; + string logPattern = "gateway.*.log"; + + if (!string.IsNullOrEmpty(config.LogFile)) + { + try + { + logDirectory = Path.GetDirectoryName(config.LogFile); + logPattern = $"{Path.GetFileName(config.LogFile)}.*.log"; + + if (!CheckAccess(logDirectory, FileAccess.Modify, true)) + { + result = ActionResult.Failure; + } + + } + catch (Exception e) + { + if (logDirectory is not null) + { + results[logDirectory] = (false, FileAccess.Modify, e); + } + + session.Log($"unexpected error while checking configuration: {e}"); + result = ActionResult.Failure; + } + } + + if (!string.IsNullOrEmpty(logDirectory)) + { + try + { + modifyFiles.AddRange(Directory.GetFiles(logDirectory, logPattern) + .OrderBy(x => new FileInfo(x).CreationTime) + .Take(10)); + } + catch (Exception e) + { + session.Log($"unexpected error while checking configuration: {e}"); + result = ActionResult.Failure; + } + } + + foreach (string modifyFile in modifyFiles.Where(x => !string.IsNullOrEmpty(x))) + { + if (!CheckAccess(modifyFile, FileAccess.Modify, false)) + { + result = ActionResult.Failure; + } + } + } + + string recordingPath = config.RecordingPath; + + if (!Path.IsPathRooted(recordingPath)) + { + recordingPath = Path.Combine(ProgramDataDirectory, recordingPath); + } + + if (Directory.Exists(recordingPath)) + { + using Impersonation _ = new Impersonation(userDomain[1], userDomain[0], string.Empty); + if (!CheckAccess(config.RecordingPath, FileAccess.Modify, true)) + { + result = ActionResult.Failure; + } + else + { + if (!string.IsNullOrEmpty(recordingPath)) + { + try + { + foreach (string recordingDir in Directory.GetDirectories(recordingPath) + .OrderBy(x => new DirectoryInfo(x).CreationTime) + .Take(10)) + { + if (!CheckAccess(recordingDir, FileAccess.Modify, true)) + { + result = ActionResult.Failure; + } + } + } + catch (Exception e) + { + results[recordingPath] = (false, FileAccess.Modify, e); + session.Log($"unexpected error while checking configuration: {e}"); + result = ActionResult.Failure; + } + } + } + } + } + catch (Exception e) + { + session.Log($"unexpected error while checking configuration: {e}"); + result = ActionResult.Failure; + } + + try + { + if (result == ActionResult.Failure) + { + StringBuilder builder = new StringBuilder(); + + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + + foreach (string key in results.Keys) + { + builder.AppendLine(""); + builder.Append($""); + builder.Append($""); + builder.Append($""); + builder.Append($""); + builder.Append($""); + builder.AppendLine(""); + } + + builder.AppendLine("
PathAccountAccessSuccessError
{key}{account.Value}{results[key].Item2.AsString()}{results[key].Item1}{results[key].Item3}
"); + builder.AppendLine(""); + builder.AppendLine(""); + + string tempPath = session.Get(GatewayProperties.userTempPath); + + if (string.IsNullOrEmpty(tempPath)) + { + tempPath = Path.GetTempPath(); + } + + string reportPath = Path.Combine(tempPath, $"{session.Get(GatewayProperties.installId)}.{Includes.ERROR_REPORT_FILENAME}"); + + session.Log($"writing configuration issues to {reportPath}"); + + File.WriteAllText(reportPath, builder.ToString()); + } + } + catch (Exception e) + { + session.Log($"unexpected error while writing results: {e}"); + } + + return result; + } + [CustomAction] public static ActionResult GetInstallDirFromRegistry(Session session) { @@ -629,7 +967,11 @@ public static ActionResult SetGatewayStartupType(Session session) [CustomAction] public static ActionResult SetInstallId(Session session) { - session.Set(GatewayProperties.installId, Guid.NewGuid()); + if (session.Get(GatewayProperties.installId) == Guid.Empty) + { + session.Set(GatewayProperties.installId, Guid.NewGuid()); + } + return ActionResult.Success; } @@ -653,7 +995,7 @@ public static ActionResult SetUsersDatabaseFilePermissions(Session session) { try { - SetFileSecurity(session, Path.Combine(ProgramDataDirectory, "users.txt"), Includes.USERS_FILE_SDDL); + SetFileSecurity(session, Path.Combine(ProgramDataDirectory, DefaultUsersFile), Includes.USERS_FILE_SDDL); return ActionResult.Success; } catch (Exception e) @@ -864,7 +1206,7 @@ private static ActionResult ExecuteCommand(Session session, string command) { string tempFilePath = tempFilePathBuilder.ToString().TrimStart('\\', '?'); - using FileStream fileStream = new(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using FileStream fileStream = new(tempFilePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite); using StreamReader streamReader = new(fileStream); result = streamReader.ReadToEnd(); } @@ -1007,5 +1349,37 @@ private static bool TryGetPowerShellVersion(out Version powerShellVersion) return Version.TryParse(version, out powerShellVersion); } + + internal static bool TryReadGatewayConfig(ILogger logger, out Gateway gatewayConfig) + { + gatewayConfig = new Gateway(); + string configPath = Path.Combine(ProgramDataDirectory, GatewayConfigFile); + + if (!File.Exists(configPath)) + { + return false; + } + + try + { + using StreamReader reader = new StreamReader(configPath); + using JsonReader jsonReader = new JsonTextReader(reader); + + JsonSerializer serializer = new JsonSerializer(); + gatewayConfig = serializer.Deserialize(jsonReader); + + return true; + } + catch (Exception e) + { + logger.Log($"failed to load configuration file at {configPath}: {e}"); + return false; + } + } + + internal static bool TryReadGatewayConfig(Session session, out Gateway gatewayConfig) + { + return TryReadGatewayConfig(LogDelegate.WithSession(session), out gatewayConfig); + } } } diff --git a/package/WindowsManaged/Actions/FileAccess.cs b/package/WindowsManaged/Actions/FileAccess.cs new file mode 100644 index 000000000..8ccc0f421 --- /dev/null +++ b/package/WindowsManaged/Actions/FileAccess.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevolutionsGateway.Actions +{ + internal enum FileAccess + { + Read, + Write, + Modify, + } + + internal static class FileAccessExtensions + { + internal static string AsString(this FileAccess access) + { + switch (access) + { + case FileAccess.Read: return "read"; + case FileAccess.Write: return "write"; + case FileAccess.Modify: return "modify"; + default: return "unknown"; + } + } + } +} diff --git a/package/WindowsManaged/Actions/GatewayActions.cs b/package/WindowsManaged/Actions/GatewayActions.cs index e82530170..c4e0ac23b 100644 --- a/package/WindowsManaged/Actions/GatewayActions.cs +++ b/package/WindowsManaged/Actions/GatewayActions.cs @@ -443,6 +443,19 @@ private static ElevatedManagedAction BuildConfigureAction( return action; } + private static readonly ElevatedManagedAction evaluateConfiguration = new( + new Id($"CA.{nameof(evaluateConfiguration)}"), + CustomActions.EvaluateConfiguration, + Return.ignore, + When.After, new Step(setUserDatabasePermissions.Id), + GatewayProperties.uninstalling.Equal(false), + Sequence.InstallExecuteSequence) + { + Execute = Execute.deferred, + Impersonate = false, + UsesProperties = UseProperties(new IWixProperty[] { GatewayProperties.installId, GatewayProperties.userTempPath }) + }; + internal static readonly Action[] Actions = { isFirstInstall, @@ -476,5 +489,6 @@ private static ElevatedManagedAction BuildConfigureAction( configurePublicKey, configureWebApp, configureWebAppUser, + evaluateConfiguration, }; } diff --git a/package/WindowsManaged/Actions/Impersonation.cs b/package/WindowsManaged/Actions/Impersonation.cs new file mode 100644 index 000000000..3487dc50f --- /dev/null +++ b/package/WindowsManaged/Actions/Impersonation.cs @@ -0,0 +1,82 @@ +using System; +using System.ComponentModel; +using System.Security.Principal; + +namespace DevolutionsGateway.Actions +{ + internal class Impersonation : IDisposable + { + private bool disposed; + + private WindowsImpersonationContext impersonationContext; + + internal Impersonation(string user, string domain, string password) + { + IntPtr userTokenDuplication = IntPtr.Zero; + + if (!WinAPI.LogonUser(user, domain, password, WinAPI.LOGON32_LOGON_SERVICE, + WinAPI.LOGON32_PROVIDER_DEFAULT, out IntPtr userToken)) + { + throw new Win32Exception(); + } + + try + { + if (WinAPI.DuplicateToken(userToken, 2, ref userTokenDuplication)) + { + WindowsIdentity winid = new WindowsIdentity(userTokenDuplication); + this.impersonationContext = winid.Impersonate(); + } + else + { + throw new Win32Exception(); + } + } + finally + { + if (userTokenDuplication != IntPtr.Zero) + { + WinAPI.CloseHandle(userTokenDuplication); + } + + if (userToken != IntPtr.Zero) + { + WinAPI.CloseHandle(userToken); + } + } + } + + ~Impersonation() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Revert() + { + if (impersonationContext == null) + { + return; + } + + impersonationContext.Undo(); + impersonationContext = null; + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + Revert(); + disposed = true; + } + } +} diff --git a/package/WindowsManaged/Actions/WinAPI.cs b/package/WindowsManaged/Actions/WinAPI.cs index 815caf78a..10947b8dc 100644 --- a/package/WindowsManaged/Actions/WinAPI.cs +++ b/package/WindowsManaged/Actions/WinAPI.cs @@ -1,5 +1,6 @@ using Microsoft.Win32.SafeHandles; using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -14,14 +15,22 @@ internal static class WinAPI internal const uint DACL_SECURITY_INFORMATION = 0x00000004; internal const int EM_SETCUEBANNER = 0x1501; - + internal static uint FILE_ATTRIBUTE_NORMAL = 0x00000080; + internal const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + internal static uint FILE_SHARE_READ = 0x00000001; internal static uint FILE_SHARE_WRITE = 0x00000002; - internal static uint GENERIC_WRITE = 0x40000000; + internal static uint FILE_SHARE_DELETE = 0x00000004; + + internal const uint OPEN_EXISTING = 3; + + internal static uint LOGON32_LOGON_SERVICE = 5; + + internal static uint LOGON32_PROVIDER_DEFAULT = 0; internal static uint MOVEFILE_DELAY_UNTIL_REBOOT = 0x04; @@ -55,6 +64,58 @@ internal static class WinAPI internal static uint WAIT_TIMEOUT = 0x00000102; + /* Generic access rights */ + + internal const uint GENERAL_ALL = 0x10000000; + + internal const uint GENERIC_EXECUTE = 0x20000000; + + internal const uint GENERIC_WRITE = 0x40000000; + + internal const uint GENERIC_READ = 0x80000000; + + /* Standard access rights */ + + internal const uint DELETE = 0x00010000; + + internal const uint READ_CONTROL = 0x00020000; + + internal const uint SYNCHRONIZE = 0x00100000; + + internal const uint WRITE_DAC = 0x00040000; + + internal const uint WRITE_OWNER = 0x00080000; + + /* File access rights */ + + internal const uint FILE_ADD_FILE = 2; + + internal const uint FILE_ADD_SUBDIRECTORY = 4; + + internal const uint FILE_APPEND_DATA = 4; + + internal const uint FILE_CREATE_PIPE_INSTANCE = 4; + + internal const uint FILE_DELETE_CHILD = 64; + + internal const uint FILE_EXECUTE = 32; + + internal const uint FILE_LIST_DIRECTORY = 1; + + internal const uint FILE_READ_ATTRIBUTES = 128; + + internal const uint FILE_READ_DATA = 1; + + internal const uint FILE_READ_EA = 8; + + internal const uint FILE_TRAVERSE = 32; + + internal const uint FILE_WRITE_ATTRIBUTES = 256; + + internal const uint FILE_WRITE_DATA = 2; + + internal const uint FILE_WRITE_EA = 16; + [StructLayout(LayoutKind.Sequential)] internal struct QUERY_SERVICE_CONFIG { @@ -91,10 +152,13 @@ internal struct PROCESS_INFORMATION [StructLayout(LayoutKind.Sequential)] internal struct SECURITY_ATTRIBUTES - { - internal uint nLength; - internal IntPtr lpSecurityDescriptor; - [MarshalAs(UnmanagedType.Bool)] internal bool bInheritHandle; + { + internal uint nLength; + + internal IntPtr lpSecurityDescriptor; + + [MarshalAs(UnmanagedType.Bool)] + internal bool bInheritHandle; } [StructLayout(LayoutKind.Sequential)] @@ -232,9 +296,16 @@ internal static extern bool CreateProcess( [DllImport("kernel32", EntryPoint = "DeleteFileW", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool DeleteFile( - [MarshalAs(UnmanagedType.LPWStr)] string lpFileName + [MarshalAs(UnmanagedType.LPWStr)] + string lpFileName ); + [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool DuplicateToken( + IntPtr token, + uint impersonationLevel, + ref IntPtr DuplicateTokenHandle); + [DllImport("kernel32", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); @@ -242,7 +313,8 @@ internal static extern bool DeleteFile( [DllImport("Kernel32", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Auto, SetLastError = true)] internal static extern uint GetFinalPathNameByHandle( IntPtr hFile, - [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, + [MarshalAs(UnmanagedType.LPTStr)] + StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags); @@ -255,10 +327,19 @@ internal static extern uint GetTempFileName( [MarshalAs(UnmanagedType.LPWStr)] string lpPrefixString, uint uUnique, [Out] StringBuilder lpTempFileName); - + [DllImport("kernel32", SetLastError = true)] internal static extern IntPtr LocalFree(IntPtr hMem); + [DllImport("advapi32", EntryPoint = "LogonUserW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool LogonUser( + string username, + string domain, + string password, + uint logonType, + uint logonProvider, + out IntPtr phToken); + [DllImport("kernel32", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool MoveFileEx( diff --git a/package/WindowsManaged/Configuration/Gateway.cs b/package/WindowsManaged/Configuration/Gateway.cs new file mode 100644 index 000000000..4cb2139c8 --- /dev/null +++ b/package/WindowsManaged/Configuration/Gateway.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel; + +namespace DevolutionsGateway.Configuration +{ + public class Gateway + { + public string Id { get; set; } + + public string Hostname { get; set; } + + public string ProvisionerPublicKeyFile { get; set; } + + public string ProvisionerPrivateKeyFile { get; set; } + + public SubProvisionerPublicKey SubProvisionerPublicKey { get; set; } + + public string DelegationPrivateKeyFile { get; set; } + + [DefaultValue("External")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public string TlsCertificateSource { get; set; } + + public string TlsCertificateSubjectName { get; set; } + + public string TlsCertificateStoreName { get; set; } + + public string TlsCertificateStoreLocation { get; set; } + + public string TlsCertificateFile { get; set; } + + public string TlsPrivateKeyFile { get; set; } + + public string TlsPrivateKeyPassword { get; set; } + + public Listener[] Listeners { get; set; } + + public Subscriber Subscriber { get; set; } + + [DefaultValue("recordings")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public string RecordingPath { get; set; } + + [DefaultValue("jrl.json")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public string JrlFile { get; set; } + + public string LogFile { get; set; } + + public Ngrok Ngrok { get; set; } + + public WebApp WebApp { get; set; } + + public string VerbosityProfile { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/Listener.cs b/package/WindowsManaged/Configuration/Listener.cs new file mode 100644 index 000000000..afbf9b6ae --- /dev/null +++ b/package/WindowsManaged/Configuration/Listener.cs @@ -0,0 +1,9 @@ +namespace DevolutionsGateway.Configuration +{ + public class Listener + { + public string InternalUrl { get; set; } + + public string ExternalUrl { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/Ngrok.cs b/package/WindowsManaged/Configuration/Ngrok.cs new file mode 100644 index 000000000..dbe17f0b4 --- /dev/null +++ b/package/WindowsManaged/Configuration/Ngrok.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace DevolutionsGateway.Configuration +{ + public class Ngrok + { + public string AuthToken { get; set; } + + public int HeartbeatInterval { get; set; } + + public int HeartbeatTolerance { get; set; } + + public string Metadata { get; set; } + + public string ServerAddr { get; set; } + + public Dictionary Tunnels { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/SubProvisionerPublicKey.cs b/package/WindowsManaged/Configuration/SubProvisionerPublicKey.cs new file mode 100644 index 000000000..cffc5f487 --- /dev/null +++ b/package/WindowsManaged/Configuration/SubProvisionerPublicKey.cs @@ -0,0 +1,13 @@ +namespace DevolutionsGateway.Configuration +{ + public class SubProvisionerPublicKey + { + public string Id { get; set; } + + public string Value { get; set; } + + public string Format { get; set; } + + public string Encoding { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/Subscriber.cs b/package/WindowsManaged/Configuration/Subscriber.cs new file mode 100644 index 000000000..1928438fd --- /dev/null +++ b/package/WindowsManaged/Configuration/Subscriber.cs @@ -0,0 +1,9 @@ +namespace DevolutionsGateway.Configuration +{ + public class Subscriber + { + internal string Url { get; set; } + + internal string Token { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/Tunnel.cs b/package/WindowsManaged/Configuration/Tunnel.cs new file mode 100644 index 000000000..0ee80f19f --- /dev/null +++ b/package/WindowsManaged/Configuration/Tunnel.cs @@ -0,0 +1,25 @@ +namespace DevolutionsGateway.Configuration +{ + public class Tunnel + { + public string[] AllowCidrs { get; set; } + + public string[] DenyCidrs { get; set; } + + public string Metadata { get; set; } + + public string Proto { get; set; } + + // HTTP + + public string Domain { get; set; } + + public string CircuitBreaker { get; set; } + + public bool Compression { get; set; } + + // TCP + + public string RemoteAddr { get; set; } + } +} diff --git a/package/WindowsManaged/Configuration/WebApp.cs b/package/WindowsManaged/Configuration/WebApp.cs new file mode 100644 index 000000000..902895886 --- /dev/null +++ b/package/WindowsManaged/Configuration/WebApp.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using Newtonsoft.Json; + +namespace DevolutionsGateway.Configuration +{ + public class WebApp + { + public bool Enabled { get; set; } + + public string Authentication { get; set; } + + public int AppTokenMaximumLifetime { get; set; } + + public int LoginLimitRate { get; set; } + + [DefaultValue(Actions.CustomActions.DefaultUsersFile)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + + public string UsersFile { get; set; } + + public string StaticRootPath { get; set; } + } +} diff --git a/package/WindowsManaged/DevolutionsGateway.csproj b/package/WindowsManaged/DevolutionsGateway.csproj index ba6889920..b10f674cd 100644 --- a/package/WindowsManaged/DevolutionsGateway.csproj +++ b/package/WindowsManaged/DevolutionsGateway.csproj @@ -22,6 +22,7 @@ + diff --git a/package/WindowsManaged/Dialogs/ExitDialog.Designer.cs b/package/WindowsManaged/Dialogs/ExitDialog.Designer.cs index ae11228a7..f29d19bfa 100644 --- a/package/WindowsManaged/Dialogs/ExitDialog.Designer.cs +++ b/package/WindowsManaged/Dialogs/ExitDialog.Designer.cs @@ -33,6 +33,7 @@ private void InitializeComponent() { this.imgPanel = new System.Windows.Forms.Panel(); this.textPanel = new System.Windows.Forms.Panel(); + this.ViewErrorsButton = new System.Windows.Forms.LinkLabel(); this.title = new System.Windows.Forms.Label(); this.description = new System.Windows.Forms.Label(); this.image = new System.Windows.Forms.PictureBox(); @@ -64,6 +65,7 @@ private void InitializeComponent() // // textPanel // + this.textPanel.Controls.Add(this.ViewErrorsButton); this.textPanel.Controls.Add(this.title); this.textPanel.Controls.Add(this.description); this.textPanel.Location = new System.Drawing.Point(162, 12); @@ -71,6 +73,21 @@ private void InitializeComponent() this.textPanel.Size = new System.Drawing.Size(320, 289); this.textPanel.TabIndex = 8; // + // ViewErrorsButton + // + this.ViewErrorsButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ViewErrorsButton.AutoSize = true; + this.ViewErrorsButton.BackColor = System.Drawing.Color.Transparent; + this.ViewErrorsButton.Location = new System.Drawing.Point(4, 276); + this.ViewErrorsButton.Name = "ViewErrorsButton"; + this.ViewErrorsButton.Size = new System.Drawing.Size(94, 13); + this.ViewErrorsButton.TabIndex = 10; + this.ViewErrorsButton.TabStop = true; + this.ViewErrorsButton.Text = "[ViewErrorsButton]"; + this.ViewErrorsButton.Visible = false; + this.ViewErrorsButton.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.ViewErrorsButton_LinkClicked); + // // title // this.title.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) @@ -212,6 +229,7 @@ private void InitializeComponent() this.Load += new System.EventHandler(this.OnLoad); this.imgPanel.ResumeLayout(false); this.textPanel.ResumeLayout(false); + this.textPanel.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.image)).EndInit(); this.bottomPanel.ResumeLayout(false); this.bottomPanel.PerformLayout(); @@ -235,5 +253,6 @@ private void InitializeComponent() private System.Windows.Forms.Button next; private System.Windows.Forms.Button cancel; private System.Windows.Forms.Panel textPanel; + private System.Windows.Forms.LinkLabel ViewErrorsButton; } } \ No newline at end of file diff --git a/package/WindowsManaged/Dialogs/ExitDialog.cs b/package/WindowsManaged/Dialogs/ExitDialog.cs index 9cae311c6..d13a7e83f 100644 --- a/package/WindowsManaged/Dialogs/ExitDialog.cs +++ b/package/WindowsManaged/Dialogs/ExitDialog.cs @@ -4,11 +4,14 @@ using System.Drawing; using System.IO; using System.Windows.Forms; +using DevolutionsGateway.Resources; namespace WixSharpSetup.Dialogs; public partial class ExitDialog : GatewayDialog { + private string warningsFile = null; + public ExitDialog() { InitializeComponent(); @@ -33,6 +36,16 @@ public override void OnLoad(object sender, System.EventArgs e) this.Localize(); } + if (Guid.TryParse(Wizard.Globals["installId"], out Guid installId)) + { + this.warningsFile = Path.Combine(Path.GetTempPath(), $"{installId}.{Includes.ERROR_REPORT_FILENAME}"); + + if (File.Exists(this.warningsFile)) + { + this.ViewErrorsButton.Visible = true; + } + } + base.OnLoad(sender, e); } @@ -73,4 +86,15 @@ void viewLog_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) //attempt to view the log. } } + + private void ViewErrorsButton_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + try + { + Process.Start(this.warningsFile); + } + catch + { + } + } } diff --git a/package/WindowsManaged/Program.cs b/package/WindowsManaged/Program.cs index 63af07cac..73f658e83 100644 --- a/package/WindowsManaged/Program.cs +++ b/package/WindowsManaged/Program.cs @@ -14,6 +14,7 @@ using System.Text.RegularExpressions; using System.Windows.Forms; using System.Xml; +using Newtonsoft.Json; using WixSharp; using WixSharp.CommonTasks; using WixSharpSetup.Dialogs; @@ -290,6 +291,7 @@ static void Main() project.ResolveWildCards(true); project.DefaultRefAssemblies.Add(typeof(ZipArchive).Assembly.Location); + project.DefaultRefAssemblies.Add(typeof(JsonSerializer).Assembly.Location); project.Actions = GatewayActions.Actions; project.RegValues = new RegValue[] { @@ -362,6 +364,12 @@ private static void Project_UnhandledException(ExceptionEventArgs e) private static void Project_UIInitialized(SetupEventArgs e) { + e.Session.Set(GatewayProperties.userTempPath, Path.GetTempPath()); + + Guid installId = Guid.NewGuid(); + e.Session.Set(GatewayProperties.installId, installId); + Wizard.Globals["installId"] = installId.ToString(); + string lcid = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "fr" ? frFR.Key : enUS.Key; using Stream stream = Assembly.GetExecutingAssembly() diff --git a/package/WindowsManaged/Properties/GatewayProperties.g.cs b/package/WindowsManaged/Properties/GatewayProperties.g.cs index 4be9ba018..cee118641 100644 --- a/package/WindowsManaged/Properties/GatewayProperties.g.cs +++ b/package/WindowsManaged/Properties/GatewayProperties.g.cs @@ -810,7 +810,7 @@ public Boolean ConfigureWebApp internal static readonly WixProperty authenticationMode = new() { Id = "P.AUTHENTICATIONMODE", - Default = AuthenticationMode.None, + Default = AuthenticationMode.Custom, Name = "AuthenticationMode", Secure = true, Hidden = false, @@ -1044,12 +1044,12 @@ public Boolean DebugPowerShell internal static readonly WixProperty installId = new() { - Id = "P.InstallId", + Id = "P.INSTALLID", Default = new Guid("00000000-0000-0000-0000-000000000000"), Name = "InstallId", - Secure = false, + Secure = true, Hidden = false, - Public = false + Public = true }; public Guid InstallId @@ -1068,6 +1068,32 @@ public Guid InstallId } } + internal static readonly WixProperty userTempPath = new() + { + Id = "P.USERTEMPPATH", + Default = "", + Name = "UserTempPath", + Secure = true, + Hidden = false, + Public = true + }; + + public String UserTempPath + { + get + { + string stringValue = this.FnGetPropValue(userTempPath.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(userTempPath, value); + } + } + } + internal static readonly WixProperty netFx45Version = new() { Id = "P.NetFx45Version", @@ -1306,6 +1332,8 @@ public Boolean Maintenance installId, + userTempPath, + netFx45Version, firstInstall, diff --git a/package/WindowsManaged/Properties/GatewayProperties.g.tt b/package/WindowsManaged/Properties/GatewayProperties.g.tt index ea0472d82..c9026f7bd 100644 --- a/package/WindowsManaged/Properties/GatewayProperties.g.tt +++ b/package/WindowsManaged/Properties/GatewayProperties.g.tt @@ -243,7 +243,7 @@ namespace DevolutionsGateway.Properties new PropertyDefinition("TcpListenerScheme", Statics.TcpProtocol), new PropertyDefinition("ConfigureWebApp", false, comment: "`true` to configure the standalone web application interactively"), - new PropertyDefinition("AuthenticationMode", Statics.AuthenticationMode.None), + new PropertyDefinition("AuthenticationMode", Statics.AuthenticationMode.Custom), new PropertyDefinition("WebUsername", ""), new PropertyDefinition("WebPassword", "", hidden: true), @@ -254,7 +254,8 @@ namespace DevolutionsGateway.Properties new PropertyDefinition("NgrokRemoteAddress", ""), new PropertyDefinition("DebugPowerShell", false), - new PropertyDefinition("InstallId", Guid.Empty, isPublic: false, secure: false), + new PropertyDefinition("InstallId", Guid.Empty, isPublic: true, secure: true), + new PropertyDefinition("UserTempPath", "", isPublic: true, secure: true), new PropertyDefinition("NetFx45Version", 0, isPublic: false, secure: false), new PropertyDefinition("FirstInstall", false, isPublic: false, secure: false), new PropertyDefinition("Upgrading", false, isPublic: false, secure: false), diff --git a/package/WindowsManaged/Resources/DevolutionsGateway_en-us.wxl b/package/WindowsManaged/Resources/DevolutionsGateway_en-us.wxl index 60bf8522d..2d5b561ad 100644 --- a/package/WindowsManaged/Resources/DevolutionsGateway_en-us.wxl +++ b/package/WindowsManaged/Resources/DevolutionsGateway_en-us.wxl @@ -44,6 +44,7 @@ Search View + View configuration issues View Log Certificate diff --git a/package/WindowsManaged/Resources/DevolutionsGateway_fr-fr.wxl b/package/WindowsManaged/Resources/DevolutionsGateway_fr-fr.wxl index 6c64c4c78..dfe14a44d 100644 --- a/package/WindowsManaged/Resources/DevolutionsGateway_fr-fr.wxl +++ b/package/WindowsManaged/Resources/DevolutionsGateway_fr-fr.wxl @@ -20,6 +20,7 @@ Rechercher Afficher + Afficher les problèmes de configuration Afficher le journal Personnalisée diff --git a/package/WindowsManaged/Resources/Includes.cs b/package/WindowsManaged/Resources/Includes.cs index 2838882a8..030e7af70 100644 --- a/package/WindowsManaged/Resources/Includes.cs +++ b/package/WindowsManaged/Resources/Includes.cs @@ -25,6 +25,8 @@ internal static class Includes internal static string INFO_URL = "https://server.devolutions.net"; + internal static string ERROR_REPORT_FILENAME = "ConfigErrors.html"; + /// /// SDDL string representing desired %programdata%\devolutions\gateway ACL /// Easiest way to generate an SDDL is to configure the required access, and then query the path with PowerShell: `Get-Acl | Format-List` diff --git a/package/WindowsManaged/Resources/Strings.g.cs b/package/WindowsManaged/Resources/Strings.g.cs index 211ac9ee7..a0a59317f 100644 --- a/package/WindowsManaged/Resources/Strings.g.cs +++ b/package/WindowsManaged/Resources/Strings.g.cs @@ -173,6 +173,10 @@ public static string I18n(this MsiRuntime runtime, string res) /// public const string ViewLogButton = "ViewLogButton"; /// + /// View configuration issues + /// + public const string ViewErrorsButton = "ViewErrorsButton"; + /// /// Install Location /// public const string Group_InstallLocation = "Group_InstallLocation"; diff --git a/package/WindowsManaged/Resources/Strings_en-US.json b/package/WindowsManaged/Resources/Strings_en-US.json index 3bfa33a2c..2f549bdd2 100644 --- a/package/WindowsManaged/Resources/Strings_en-US.json +++ b/package/WindowsManaged/Resources/Strings_en-US.json @@ -171,6 +171,10 @@ { "id": "ViewLogButton", "text": "View Log" + }, + { + "id": "ViewErrorsButton", + "text": "View configuration issues" } ], "propertyGroups": [ diff --git a/package/WindowsManaged/Resources/Strings_fr-FR.json b/package/WindowsManaged/Resources/Strings_fr-FR.json index 0bb923da8..ae522634f 100644 --- a/package/WindowsManaged/Resources/Strings_fr-FR.json +++ b/package/WindowsManaged/Resources/Strings_fr-FR.json @@ -77,6 +77,10 @@ { "id": "ViewLogButton", "text": "Afficher le journal" + }, + { + "id": "ViewErrorsButton", + "text": "Afficher les problèmes de configuration" } ], "enums": [