diff --git a/Elfo.Wardein.Abstractions/Configuration/Models/WatcherModels/WebWatcherConfigurationModel.cs b/Elfo.Wardein.Abstractions/Configuration/Models/WatcherModels/WebWatcherConfigurationModel.cs index ad6f832..d0e2cc8 100644 --- a/Elfo.Wardein.Abstractions/Configuration/Models/WatcherModels/WebWatcherConfigurationModel.cs +++ b/Elfo.Wardein.Abstractions/Configuration/Models/WatcherModels/WebWatcherConfigurationModel.cs @@ -9,10 +9,10 @@ public class WebWatcherConfigurationModel : IAmConfigurationModelWithResolution { [JsonProperty(PropertyName = "associatedIISPool")] public string AssociatedIISPool { get; set; } - + [JsonProperty(PropertyName = "url")] public Uri Url { get; set; } - + [JsonProperty(PropertyName = "urlAlias")] public string UrlAlias { get; set; } @@ -43,5 +43,22 @@ public class WebWatcherConfigurationModel : IAmConfigurationModelWithResolution public string RestoredMessage { get; set; } public int MaxRetryCount { get; set; } = 2; public int SendReminderEmailAfterRetryCount { get; set; } = 120; + public enum HttpCallApiMethod + { + Get = 1, + Post = 2 + } + + public HttpCallApiMethod Method { get; } + + /// + /// Request Body that may be required for POST request. + /// + public string Body { get; } = null; + + /// + /// Request headers. + /// + public IDictionary Headers { get; } = null; } } diff --git a/Elfo.Wardein.Abstractions/WebWatcher/IAmUrlResponseManager.cs b/Elfo.Wardein.Abstractions/WebWatcher/IAmUrlResponseManager.cs index 068baff..ad95356 100644 --- a/Elfo.Wardein.Abstractions/WebWatcher/IAmUrlResponseManager.cs +++ b/Elfo.Wardein.Abstractions/WebWatcher/IAmUrlResponseManager.cs @@ -1,11 +1,12 @@ -using System; +using Elfo.Wardein.Abstractions.Configuration.Models; using System.Threading.Tasks; +using static Elfo.Wardein.Abstractions.Configuration.Models.WebWatcherConfigurationModel; namespace Elfo.Wardein.Abstractions.WebWatcher { public interface IAmUrlResponseManager { - Task IsHealthy(bool assertWithStatusCode, string assertWithRegex, Uri url); + Task IsHealthy(WebWatcherConfigurationModel configuration, HttpCallApiMethod method); Task RestartPool(string poolName); } } diff --git a/Elfo.Wardein.Core.Tests/WebWatcherCallWithApiClient.cs b/Elfo.Wardein.Core.Tests/WebWatcherCallWithApiClient.cs new file mode 100644 index 0000000..0eacc27 --- /dev/null +++ b/Elfo.Wardein.Core.Tests/WebWatcherCallWithApiClient.cs @@ -0,0 +1,105 @@ +using Elfo.Wardein.Abstractions.Configuration.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Win32.SafeHandles; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Security.Principal; +using System.Threading.Tasks; + +namespace Elfo.Wardein.Core.Tests +{ + + [TestClass] + public class WebWatcherCallWithApiClient + { + private WebWatcherConfigurationModel configuration; + private string userNameToImpersonate; + private string domainToImpersonate; + private string userPasswordToImpersonate; + + [TestInitialize] + public void Initialize() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); + + userNameToImpersonate = configuration["Impersonate:userNameToImpersonate"]; + domainToImpersonate = configuration["Impersonate:userDomainToImpersonate"]; + userPasswordToImpersonate = configuration["Impersonate:userPasswordToImpersonate"]; + } + + async Task IsSuccessStatusCode(HttpClient client) + { + var response = await client.GetAsync(configuration.Url.AbsoluteUri); + return response.IsSuccessStatusCode; + } + + [TestMethod] + [TestCategory("ManualTest")] + public async Task IsHealthy() + { + var client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true, PreAuthenticate = true, AllowAutoRedirect = true }); + client.BaseAddress = new Uri(configuration.Url.AbsoluteUri); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = false; + try + { + ImpersonateUser iu = new ImpersonateUser(); + iu.RunImpersonated(domainToImpersonate, userNameToImpersonate, userPasswordToImpersonate, + () => IsSuccessStatusCode(client).Wait()); + response = await IsSuccessStatusCode(client); + } + catch(Exception ex) { + Console.WriteLine($"There is an error: {ex}"); + } + + Assert.IsTrue(response); + } + } + + [TestClass] + public class ImpersonateUser + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, + int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken); + + // Test harness. + // If you incorporate this code into a DLL, be sure to demand FullTrust. + [TestMethod] + [TestCategory("ManualTest")] + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + public void RunImpersonated(string domainName, string userName, string password, Action action) + { + try + { + const int LOGON32_PROVIDER_DEFAULT = 0; + //This parameter causes LogonUser to create a primary token. + const int LOGON32_LOGON_INTERACTIVE = 2; + + // Call LogonUser to obtain a handle to an access token. + SafeAccessTokenHandle safeAccessTokenHandle; + bool returnValue = LogonUser(userName, domainName, password, + LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, + out safeAccessTokenHandle); + + if (false == returnValue) + { + int ret = Marshal.GetLastWin32Error(); + throw new System.ComponentModel.Win32Exception(ret); + } + WindowsIdentity.RunImpersonated(safeAccessTokenHandle, action); + } + catch (Exception ex) + { + Console.WriteLine("Exception occurred. " + ex.Message); + } + } + } +} diff --git a/Elfo.Wardein.Core/Elfo.Wardein.Core.csproj b/Elfo.Wardein.Core/Elfo.Wardein.Core.csproj index 7fd153a..da1dadc 100644 --- a/Elfo.Wardein.Core/Elfo.Wardein.Core.csproj +++ b/Elfo.Wardein.Core/Elfo.Wardein.Core.csproj @@ -24,6 +24,8 @@ + + diff --git a/Elfo.Wardein.Core/ServiceManager/HttpClientUrlResponseManager.cs b/Elfo.Wardein.Core/ServiceManager/HttpClientUrlResponseManager.cs index a8eb1b4..a141aab 100644 --- a/Elfo.Wardein.Core/ServiceManager/HttpClientUrlResponseManager.cs +++ b/Elfo.Wardein.Core/ServiceManager/HttpClientUrlResponseManager.cs @@ -1,50 +1,77 @@ -using Elfo.Wardein.Abstractions.WebWatcher; -using Elfo.Wardein.Core.ServiceManager; +using Elfo.Wardein.Abstractions.Configuration.Models; +using Elfo.Wardein.Abstractions.WebWatcher; +using Microsoft.Extensions.Configuration; +using Microsoft.Win32.SafeHandles; +using NLog; using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Security.Principal; using System.Text.RegularExpressions; using System.Threading.Tasks; +using static Elfo.Wardein.Abstractions.Configuration.Models.WebWatcherConfigurationModel; namespace Elfo.Wardein.Core.ServiceManager { public class HttpClientUrlResponseManager : IAmUrlResponseManager { + private string userNameToImpersonate; + private string domainToImpersonate; + private string userPasswordToImpersonate; - public async Task IsHealthy(bool assertWithStatusCode, string assertWithRegex, Uri url) + protected static ILogger log = LogManager.GetCurrentClassLogger(); + + public HttpClientUrlResponseManager() { - using (var handler = new HttpClientHandler()) - using (var client = new HttpClient(handler)) - { - // TODO support authentication - client.BaseAddress = url; - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); + + userNameToImpersonate = configuration["Impersonate:userNameToImpersonate"]; + domainToImpersonate = configuration["Impersonate:userDomainToImpersonate"]; + userPasswordToImpersonate = configuration["Impersonate:userPasswordToImpersonate"]; + } - var response = await client.GetAsync(url); + - if (!assertWithStatusCode) + public async Task IsHealthy(WebWatcherConfigurationModel configuration, HttpCallApiMethod method) + { + HttpResponseMessage response = null; + var apiClient = InitializeApiClient(configuration); + ImpersonateUser iu = new ImpersonateUser(); + try + { + iu.RunImpersonated(domainToImpersonate, userNameToImpersonate, userPasswordToImpersonate, + async () => response = await apiClient.GetAsync(configuration.Url.AbsoluteUri)); + } + catch (UnauthorizedAccessException ex) + { + log.Error($"Exception got while waiting response from {configuration.Url.AbsoluteUri} - {ex}"); + throw; + } + if (!configuration.AssertWithStatusCode) + { + if (!string.IsNullOrWhiteSpace(configuration.AssertWithRegex)) { - if (!string.IsNullOrWhiteSpace(assertWithRegex)) - { - return await CheckIsMatch(assertWithRegex, response); - } - else - { - return await Task.FromResult(true); - } + return await CheckIsMatch(configuration.AssertWithRegex, response); } else { - if (response.StatusCode != HttpStatusCode.OK) - { - return await Task.FromResult(false); - } - else - { - return await CheckIsMatch(assertWithRegex, response); - } + return await Task.FromResult(true); + } + } + else + { + if (response.StatusCode != HttpStatusCode.OK) + { + return await Task.FromResult(false); + } + else + { + return await CheckIsMatch(configuration.AssertWithRegex, response); } } } @@ -70,33 +97,57 @@ private async Task CheckIsMatch(string assertionRegex, HttpResponseMessage } } - public Task RestartPool(string poolName) + public async Task RestartPool(string poolName) { - new IISPoolManager(poolName).Restart(); - return Task.CompletedTask; + await new IISPoolManager(poolName).Restart(); } - //private List AuthCredentials(string username, string password) - //{ - // List result = null; + HttpClient InitializeApiClient(WebWatcherConfigurationModel configuration) + { + var client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true, PreAuthenticate = true }); + client.BaseAddress = new Uri(configuration.Url.AbsoluteUri); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + return client; + } + + public class ImpersonateUser + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, + int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken); + + // Test harness. + // If you incorporate this code into a DLL, be sure to demand FullTrust. + + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + public void RunImpersonated(string domainName, string userName, string password, Action action) + { + try + { + const int LOGON32_PROVIDER_DEFAULT = 0; + //This parameter causes LogonUser to create a primary token. + const int LOGON32_LOGON_INTERACTIVE = 2; - // CredentialCache.DefaultNetworkCredentials.UserName = username; - // CredentialCache.DefaultNetworkCredentials.Password = password; + // Call LogonUser to obtain a handle to an access token. + SafeAccessTokenHandle safeAccessTokenHandle; + bool returnValue = LogonUser(userName, domainName, password, + LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, + out safeAccessTokenHandle); - // using (var authtHandler = new HttpClientHandler { Credentials = CredentialCache.DefaultNetworkCredentials }) - // { - // using (var httpClient = new HttpClient(authtHandler)) - // { - // httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("Keep-Alive")); - // HttpResponseMessage message = httpClient.GetAsync("").Result; - // if (message.IsSuccessStatusCode) - // { - // var inter = message.Content.ReadAsStringAsync(); - // result = JsonConvert.DeserializeObject>(inter.Result); - // } - // } - // } - // return result; - //} + if (false == returnValue) + { + int ret = Marshal.GetLastWin32Error(); + throw new System.ComponentModel.Win32Exception(ret); + } + WindowsIdentity.RunImpersonated(safeAccessTokenHandle, action); + } + catch (Exception ex) + { + Console.WriteLine("Exception occurred. " + ex.Message); + } + } + } } } diff --git a/Elfo.Wardein.Watchers/WebWatcher/WebWatcher.cs b/Elfo.Wardein.Watchers/WebWatcher/WebWatcher.cs index 2053034..ad9596b 100644 --- a/Elfo.Wardein.Watchers/WebWatcher/WebWatcher.cs +++ b/Elfo.Wardein.Watchers/WebWatcher/WebWatcher.cs @@ -1,9 +1,7 @@ using Elfo.Wardein.Abstractions.Watchers; using Elfo.Wardein.Abstractions.WebWatcher; -using Elfo.Wardein.Core.ServiceManager; using Elfo.Wardein.Core; using Elfo.Wardein.Core.Helpers; -using Elfo.Wardein.Core.NotificationService; using NLog; using NLog.Fluent; using System; @@ -63,7 +61,7 @@ internal virtual async Task RunCheck() log.Info($"Starting check on {GetLoggingDisplayName}"); var notificationService = ServicesContainer.NotificationService(Config.NotificationType); - var isHealthy = await urlResponseManager.IsHealthy(Config.AssertWithStatusCode, Config.AssertWithRegex, Config.Url); + var isHealthy = await urlResponseManager.IsHealthy(Config, Config.Method); var currentStatus = await watcherPersistenceService.UpsertCurrentStatus ( watcherConfigurationId: Config.WatcherConfigurationId,