diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 118c4d3..90c73b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ jobs: name: Unit Tests on Linux runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.x' + dotnet-version: '8' - run: dotnet test diff --git a/Dockerfile b/Dockerfile index 58e305c..6a5aa92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env WORKDIR /build # Copy csproj and restore as distinct layers @@ -10,7 +10,7 @@ COPY sama/ ./ RUN dotnet publish -c Release -o out --no-restore /p:MvcRazorCompileOnPublish=true # Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:6.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /opt/sama COPY --from=build-env /build/out . ENTRYPOINT ["dotnet", "sama.dll"] diff --git a/Dockerfile-buildx-amd64 b/Dockerfile-buildx-amd64 index f9c7321..fe8e77e 100644 --- a/Dockerfile-buildx-amd64 +++ b/Dockerfile-buildx-amd64 @@ -1,5 +1,5 @@ # Forcing amd64 for buildx per https://github.com/dotnet/dotnet-docker/issues/1537#issuecomment-755351628 -FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim-amd64 AS build-env WORKDIR /build # Copy csproj and restore as distinct layers @@ -11,7 +11,7 @@ COPY sama/ ./ RUN dotnet publish -c Release -o out --no-restore /p:MvcRazorCompileOnPublish=true # Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:6.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /opt/sama COPY --from=build-env /build/out . ENTRYPOINT ["dotnet", "sama.dll"] diff --git a/sama/Controllers/AccountController.cs b/sama/Controllers/AccountController.cs index 63500d3..f666a82 100644 --- a/sama/Controllers/AccountController.cs +++ b/sama/Controllers/AccountController.cs @@ -173,7 +173,7 @@ public async Task Edit(Guid? id) var isRemote = (user == null && userId[0] == 0 && userId[1] == 0 && userId[2] == 0 && userId[3] == 0); if (isRemote) { - ViewData["IsCurrentUser"] = (id == Guid.Parse(_userManager.GetUserId(User))); + ViewData["IsCurrentUser"] = (id == Guid.Parse(_userManager.GetUserId(User)!)); return View("EditRemote"); } diff --git a/sama/Controllers/EndpointsController.cs b/sama/Controllers/EndpointsController.cs index 3782a8d..5cc56e0 100644 --- a/sama/Controllers/EndpointsController.cs +++ b/sama/Controllers/EndpointsController.cs @@ -10,21 +10,8 @@ namespace sama.Controllers { [Authorize] - public class EndpointsController : Controller + public class EndpointsController(ApplicationDbContext _context, StateService _stateService, UserManagementService _userService, AggregateNotificationService _notifier) : Controller { - private readonly ApplicationDbContext _context; - private readonly StateService _stateService; - private readonly UserManagementService _userService; - private readonly AggregateNotificationService _notifier; - - public EndpointsController(ApplicationDbContext context, StateService stateService, UserManagementService userService, AggregateNotificationService notifier) - { - _context = context; - _stateService = stateService; - _userService = userService; - _notifier = notifier; - } - [AllowAnonymous] public async Task IndexRedirect() { diff --git a/sama/Extensions/EndpointHttpExtensions.cs b/sama/Extensions/EndpointHttpExtensions.cs index f953db4..5b98eff 100644 --- a/sama/Extensions/EndpointHttpExtensions.cs +++ b/sama/Extensions/EndpointHttpExtensions.cs @@ -1,17 +1,13 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using sama.Models; +using sama.Models; using System; using System.Collections.Generic; -using System.Dynamic; using System.Linq; +using System.Text.Json.Nodes; namespace sama.Extensions { public static class EndpointHttpExtensions { - private static JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }; - public static string? GetHttpLocation(this Endpoint endpoint) => GetValue(endpoint, "Location"); @@ -45,12 +41,20 @@ public static void SetHttpCustomTlsCert(this Endpoint endpoint, string? pemEncod private static List? GetValueList(Endpoint endpoint, string name, List? defaultValue = null) { - var list = GetValue>(endpoint, name); - if (list == null) + EnsureHttp(endpoint); + if (string.IsNullOrWhiteSpace(endpoint.JsonConfig)) return defaultValue; + + var node = JsonNode.Parse(endpoint.JsonConfig); + var matches = node?.AsObject().Where(kvp => kvp.Key == name); + if (matches?.Any() ?? false) { - return defaultValue; + if (matches.First().Value == null) return defaultValue; + var array = matches.First().Value!.AsArray(); + if (array == null) return defaultValue; + return array.Select(n => n!.GetValue()).ToList(); } - return list.Select(o => (T)Convert.ChangeType(o, typeof(T))).ToList(); + // else + return defaultValue; } private static T? GetValue(Endpoint endpoint, string name, T? defaultValue = default(T)) @@ -58,16 +62,15 @@ public static void SetHttpCustomTlsCert(this Endpoint endpoint, string? pemEncod EnsureHttp(endpoint); if (string.IsNullOrWhiteSpace(endpoint.JsonConfig)) return defaultValue; - var obj = JsonConvert.DeserializeObject(endpoint.JsonConfig, JsonSettings) as IDictionary; - if (obj == null) + var node = JsonNode.Parse(endpoint.JsonConfig); + var matches = node?.AsObject().Where(kvp => kvp.Key == name); + if (matches?.Any() ?? false) { - throw new ArgumentException($"Unable to get value for '{name}'"); + if (matches.First().Value == null) return defaultValue; + return matches.First().Value!.GetValue(); } - if (!obj.ContainsKey(name)) - { - return defaultValue; - } - return (T)obj[name]; + // else + return defaultValue; } private static void SetValue(Endpoint endpoint, string name, T? value) @@ -75,13 +78,11 @@ private static void SetValue(Endpoint endpoint, string name, T? value) EnsureHttp(endpoint); var json = (string.IsNullOrWhiteSpace(endpoint.JsonConfig) ? "{}" : endpoint.JsonConfig); - var obj = JsonConvert.DeserializeObject(json, JsonSettings) ?? throw new ArgumentException($"Unable to deserialize '{name}'"); - obj!.Remove(name, out object _); - if (!obj.TryAdd(name, value)) - { - throw new ArgumentException($"Unable to set value for '{name}'"); - } - endpoint.JsonConfig = JsonConvert.SerializeObject(obj, JsonSettings); + + var nodeObj = JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException($"Unable to deserialize '{name}'"); + nodeObj.Remove(name); + nodeObj.Add(name, JsonValue.Create(value)); + endpoint.JsonConfig = nodeObj.ToJsonString(); } private static void EnsureHttp(Endpoint endpoint) diff --git a/sama/Extensions/EndpointIcmpExtensions.cs b/sama/Extensions/EndpointIcmpExtensions.cs index 0985721..ccbe613 100644 --- a/sama/Extensions/EndpointIcmpExtensions.cs +++ b/sama/Extensions/EndpointIcmpExtensions.cs @@ -1,21 +1,28 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using sama.Models; +using sama.Models; using System; -using System.Dynamic; +using System.Linq; +using System.Text.Json.Nodes; namespace sama.Extensions { public static class EndpointIcmpExtensions { - private static JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }; + private const string IcmpAddressKey = "Address"; public static string? GetIcmpAddress(this Endpoint endpoint) { EnsureIcmp(endpoint); if (string.IsNullOrWhiteSpace(endpoint.JsonConfig)) return null; - return JsonConvert.DeserializeObject(endpoint.JsonConfig, JsonSettings)?.Address?.ToObject(); + var node = JsonNode.Parse(endpoint.JsonConfig); + var matches = node?.AsObject().Where(kvp => kvp.Key == IcmpAddressKey); + if (matches?.Any() ?? false) + { + if (matches.First().Value == null) return null; + return matches.First().Value!.GetValue(); + } + // else + return null; } public static void SetIcmpAddress(this Endpoint endpoint, string? address) @@ -23,9 +30,11 @@ public static void SetIcmpAddress(this Endpoint endpoint, string? address) EnsureIcmp(endpoint); var json = (string.IsNullOrWhiteSpace(endpoint.JsonConfig) ? "{}" : endpoint.JsonConfig); - dynamic obj = JsonConvert.DeserializeObject(json, JsonSettings) ?? throw new ArgumentException("Unable to deserialize JSON settings for endpoint"); - obj.Address = address; - endpoint.JsonConfig = JsonConvert.SerializeObject(obj, JsonSettings); + + var nodeObj = JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException($"Unable to deserialize Address"); + nodeObj.Remove(IcmpAddressKey); + nodeObj.Add(IcmpAddressKey, JsonValue.Create(address)); + endpoint.JsonConfig = nodeObj.ToJsonString(); } private static void EnsureIcmp(Endpoint endpoint) diff --git a/sama/HttpHandlerFactory.cs b/sama/HttpHandlerFactory.cs new file mode 100644 index 0000000..e7aace4 --- /dev/null +++ b/sama/HttpHandlerFactory.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Http; +using System.Net.Security; + +namespace sama; + +public class HttpHandlerFactory +{ + public virtual HttpMessageHandler Create(bool allowAutoRedirect, SslClientAuthenticationOptions? sslOptions) + { + var handler = new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.Zero, + AllowAutoRedirect = allowAutoRedirect, + }; + if (sslOptions != null) + { + handler.SslOptions = sslOptions; + } + return handler; + } +} diff --git a/sama/Services/GraphiteNotificationService.cs b/sama/Services/GraphiteNotificationService.cs index 207f356..0457af6 100644 --- a/sama/Services/GraphiteNotificationService.cs +++ b/sama/Services/GraphiteNotificationService.cs @@ -57,7 +57,7 @@ public virtual void NotifySingleResult(Endpoint endpoint, EndpointCheckResult re } catch (Exception ex) { - _logger.LogError(0, "Unable to send Graphite notification", ex); + _logger.LogError(ex, "Unable to send Graphite notification"); } } diff --git a/sama/Services/HttpCheckService.cs b/sama/Services/HttpCheckService.cs index 56ab308..8d1938b 100644 --- a/sama/Services/HttpCheckService.cs +++ b/sama/Services/HttpCheckService.cs @@ -1,37 +1,19 @@ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using sama.Extensions; using sama.Models; using System; using System.Collections.Generic; using System.Net.Http; -using System.Reflection; +using System.Net.Security; using System.Threading.Tasks; namespace sama.Services { - public class HttpCheckService : ICheckService + public class HttpCheckService(SettingsService _settingsService, CertificateValidationService _certService, IConfiguration _configuration, HttpHandlerFactory _httpHandlerFactory) : ICheckService { - private readonly SettingsService _settingsService; - private readonly CertificateValidationService _certService; - private readonly IConfiguration _configuration; - private readonly IServiceProvider _serviceProvider; - - private readonly string _appVersion; - private Version? _defaultRequestVersion; private HttpVersionPolicy? _defaultVersionPolicy; - public HttpCheckService(SettingsService settingsService, CertificateValidationService certService, IConfiguration configuration, IServiceProvider serviceProvider) - { - _settingsService = settingsService; - _certService = certService; - _configuration = configuration; - _serviceProvider = serviceProvider; - - _appVersion = typeof(Startup).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; - } - public bool CanHandle(Endpoint endpoint) { return (endpoint.Kind == Endpoint.EndpointKind.Http); @@ -41,81 +23,80 @@ public EndpointCheckResult Check(Endpoint endpoint) { var result = new EndpointCheckResult { Start = DateTimeOffset.UtcNow }; - using (var httpHandler = _serviceProvider.GetRequiredService()) - using (var client = new HttpClient(httpHandler, false)) - using (var message = new HttpRequestMessage(HttpMethod.Get, endpoint.GetHttpLocation())) + var statusCodes = endpoint.GetHttpStatusCodes() ?? []; + var sslOptions = new SslClientAuthenticationOptions { - httpHandler.ServerCertificateCustomValidationCallback = (msg, certificate, chain, sslPolicyErrors) => + RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { _certService.ValidateHttpEndpoint(endpoint, chain, sslPolicyErrors); return true; - }; + } + }; - message.Version = GetDefaultRequestVersion(); - message.VersionPolicy = GetDefaultVersionPolicy(); + using var httpHandler = _httpHandlerFactory.Create((statusCodes.Count == 0), sslOptions); + using var client = new HttpClient(httpHandler, false); + using var message = new HttpRequestMessage(HttpMethod.Get, endpoint.GetHttpLocation()); - var statusCodes = endpoint.GetHttpStatusCodes() ?? new List(); - if (statusCodes.Count > 0) - httpHandler.AllowAutoRedirect = false; + message.Version = GetDefaultRequestVersion(); + message.VersionPolicy = GetDefaultVersionPolicy(); - message.Headers.Add("User-Agent", $"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 SAMA/{_appVersion}"); - message.Headers.Add("Accept", "text/html, application/xhtml+xml, */*"); - client.Timeout = ClientTimeout; + message.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 SAMA/1.0"); + message.Headers.Add("Accept", "text/html, application/xhtml+xml, */*"); + client.Timeout = ClientTimeout; - var task = client.SendAsync(message); - try - { - task.Wait(); - } - catch (Exception ex) - { - if (ex is AggregateException && ex.InnerException != null) - ex = ex.InnerException; - if (ex is HttpRequestException && ex.InnerException != null) - ex = ex.InnerException; - if (ex is TaskCanceledException) - ex = new Exception($"The request timed out after {ClientTimeout.TotalSeconds} sec"); - - SetFailure(result, ex); - return result; - } + var task = client.SendAsync(message); + try + { + task.Wait(); + } + catch (Exception ex) + { + if (ex is AggregateException && ex.InnerException != null) + ex = ex.InnerException; + if (ex is HttpRequestException && ex.InnerException != null) + ex = ex.InnerException; + if (ex is TaskCanceledException) + ex = new Exception($"The request timed out after {ClientTimeout.TotalSeconds} sec"); + + SetFailure(result, ex); + return result; + } - var response = task.Result; - if (!IsExpectedStatusCode(endpoint, response)) - { - SetFailure(result, new Exception($"HTTP status code is {(int)response.StatusCode}")); - return result; - } + var response = task.Result; + if (!IsExpectedStatusCode(endpoint, response)) + { + SetFailure(result, new Exception($"HTTP status code is {(int)response.StatusCode}")); + return result; + } - var responseMatch = endpoint.GetHttpResponseMatch(); - if (string.IsNullOrWhiteSpace(responseMatch)) - { - SetSuccess(result); - return result; - } + var responseMatch = endpoint.GetHttpResponseMatch(); + if (string.IsNullOrWhiteSpace(responseMatch)) + { + SetSuccess(result); + return result; + } - var contentTask = response.Content.ReadAsStringAsync(); - try - { - contentTask.Wait(); - } - catch (Exception ex) - { - SetFailure(result, new Exception($"Failed to read HTTP content: {ex.Message}", ex)); - return result; - } + var contentTask = response.Content.ReadAsStringAsync(); + try + { + contentTask.Wait(); + } + catch (Exception ex) + { + SetFailure(result, new Exception($"Failed to read HTTP content: {ex.Message}", ex)); + return result; + } - var index = contentTask.Result.IndexOf(responseMatch); - if (index < 0) - { - SetFailure(result, new Exception("The keyword match was not found")); - return result; - } - else - { - SetSuccess(result); - return result; - } + var index = contentTask.Result.IndexOf(responseMatch); + if (index < 0) + { + SetFailure(result, new Exception("The keyword match was not found")); + return result; + } + else + { + SetSuccess(result); + return result; } } diff --git a/sama/Services/SettingsService.cs b/sama/Services/SettingsService.cs index afbd907..f4eb8cf 100644 --- a/sama/Services/SettingsService.cs +++ b/sama/Services/SettingsService.cs @@ -163,7 +163,7 @@ private void SetSetting(string section, string name, T value) if (string.IsNullOrWhiteSpace(valueString)) { - _cache.TryRemove(section + CACHE_NAME_SEPARATOR + name, out object _); + _cache.TryRemove(section + CACHE_NAME_SEPARATOR + name, out object? _); } else { diff --git a/sama/Services/SlackNotificationService.cs b/sama/Services/SlackNotificationService.cs index fa4e489..8db6e7d 100644 --- a/sama/Services/SlackNotificationService.cs +++ b/sama/Services/SlackNotificationService.cs @@ -1,38 +1,21 @@ using Humanizer; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using sama.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; namespace sama.Services { - public class SlackNotificationService : INotificationService + public class SlackNotificationService(ILogger _logger, SettingsService _settings, HttpHandlerFactory _httpHandlerFactory, BackgroundExecutionWrapper _bgExec) : INotificationService { private const int NOTIFY_UP_QUEUE_DELAY_MILLISECONDS = 2500; - private readonly ILogger _logger; - private readonly SettingsService _settings; - private readonly IServiceProvider _serviceProvider; - private readonly BackgroundExecutionWrapper _bgExec; - - private readonly List _delayNotifyUpEndpoints; - private readonly List _delayTasks; - - public SlackNotificationService(ILogger logger, SettingsService settings, IServiceProvider serviceProvider, BackgroundExecutionWrapper bgExec) - { - _logger = logger; - _settings = settings; - _serviceProvider = serviceProvider; - _bgExec = bgExec; - - _delayNotifyUpEndpoints = new List(); - _delayTasks = new List(); - } + private readonly List _delayNotifyUpEndpoints = new List(); + private readonly List _delayTasks = new List(); public virtual void NotifyMisc(Endpoint endpoint, NotificationType type) { @@ -101,20 +84,20 @@ protected virtual void SendNotification(string message) try { - using var httpHandler = _serviceProvider.GetRequiredService(); + using var httpHandler = _httpHandlerFactory.Create(true, null); using var client = new HttpClient(httpHandler, false); - var data = JsonConvert.SerializeObject(new { text = message }); + var data = JsonSerializer.Serialize(new { text = message }).Replace("\\u0060", "`"); var content = new StringContent(data); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); using var response = client.PostAsync(url, content).Result; if (!response.IsSuccessStatusCode) { - _logger.LogError(0, $"Unable to send Slack notification: HTTP {(int)response.StatusCode}", new Exception(response.Content.ReadAsStringAsync().Result)); + _logger.LogError(new Exception(response.Content.ReadAsStringAsync().Result), $"Unable to send Slack notification: HTTP {(int)response.StatusCode}"); } } catch (Exception ex) { - _logger.LogError(0, "Unable to send Slack notification", ex); + _logger.LogError(ex, "Unable to send Slack notification"); } } diff --git a/sama/Services/SqlServerNotificationService.cs b/sama/Services/SqlServerNotificationService.cs index 2a7979b..bebdf39 100644 --- a/sama/Services/SqlServerNotificationService.cs +++ b/sama/Services/SqlServerNotificationService.cs @@ -1,8 +1,6 @@ -using Dapper; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using sama.Models; using System; -using System.Data.Common; namespace sama.Services { diff --git a/sama/Services/StateService.cs b/sama/Services/StateService.cs index 54c3829..d660d14 100644 --- a/sama/Services/StateService.cs +++ b/sama/Services/StateService.cs @@ -73,7 +73,7 @@ public virtual void AddEndpointCheckResult(int endpointId, EndpointCheckResult r public virtual void RemoveStatus(int id) { - _endpointStates.TryRemove(id, out EndpointStatus _); + _endpointStates.TryRemove(id, out EndpointStatus? _); } private void PostProcessStatus(int endpointId, EndpointStatus status, bool finalizeResults) diff --git a/sama/Services/UserManagementService.cs b/sama/Services/UserManagementService.cs index cac01c9..dcba5bb 100644 --- a/sama/Services/UserManagementService.cs +++ b/sama/Services/UserManagementService.cs @@ -2,48 +2,34 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using sama.Models; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; namespace sama.Services { - public class UserManagementService : IUserStore, IRoleStore, IDisposable + public class UserManagementService(ILogger _logger, DbContextOptions _dbContextOptions) : IUserStore, IRoleStore, IDisposable { - private readonly ILogger _logger; - private readonly DbContextOptions _dbContextOptions; - - public UserManagementService(ILogger logger, DbContextOptions dbContextOptions) - { - _logger = logger; - _dbContextOptions = dbContextOptions; - } - - public void Dispose() - { - } + private bool _disposedValue; public virtual async Task HasAccounts() { - using(var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - return await dbContext.Users.AsQueryable().AnyAsync(); - } + using var dbContext = new ApplicationDbContext(_dbContextOptions); + return await dbContext.Users.AsQueryable().AnyAsync(); } public virtual async Task FindUserByUsername(string username) { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - return await dbContext.Users.AsAsyncEnumerable().FirstOrDefaultAsync(u => u.UserName?.ToLowerInvariant() == username.Trim().ToLowerInvariant()); - } - } + using var dbContext = new ApplicationDbContext(_dbContextOptions); + return await dbContext.Users.AsAsyncEnumerable().FirstOrDefaultAsync(u => u.UserName?.ToLowerInvariant() == username.Trim().ToLowerInvariant()); + } public virtual Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) { @@ -62,24 +48,8 @@ public virtual bool ValidateCredentials(ApplicationUser user, string password) public virtual async Task CreateInitial(string username, string password) { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - if (!await dbContext.Users.AsQueryable().AnyAsync()) - { - CreatePasswordHash(password, out string hash, out string metadata); - var user = new ApplicationUser { Id = Guid.NewGuid(), UserName = username.Trim(), PasswordHash = hash, PasswordHashMetadata = metadata }; - dbContext.Users.Add(user); - await dbContext.SaveChangesAsync(); - return user; - } - - return null; - } - } - - public virtual async Task Create(string username, string password) - { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) + using var dbContext = new ApplicationDbContext(_dbContextOptions); + if (!await dbContext.Users.AsQueryable().AnyAsync()) { CreatePasswordHash(password, out string hash, out string metadata); var user = new ApplicationUser { Id = Guid.NewGuid(), UserName = username.Trim(), PasswordHash = hash, PasswordHashMetadata = metadata }; @@ -87,68 +57,72 @@ public virtual async Task Create(string username, string passwo await dbContext.SaveChangesAsync(); return user; } + + return null; } - public virtual async Task> ListUsers() + public virtual async Task Create(string username, string password) { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - return await dbContext.Users.AsQueryable().ToListAsync(); - } + using var dbContext = new ApplicationDbContext(_dbContextOptions); + CreatePasswordHash(password, out string hash, out string metadata); + var user = new ApplicationUser { Id = Guid.NewGuid(), UserName = username.Trim(), PasswordHash = hash, PasswordHashMetadata = metadata }; + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(); + return user; } - public virtual async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + public virtual async Task> ListUsers() { using var dbContext = new ApplicationDbContext(_dbContextOptions); + return await dbContext.Users.AsQueryable().ToListAsync(); + } - // The base method appears to be marked incorrectly for nullability. It should be returning Task. - -#pragma warning disable CS8603 // Possible null reference return. - return await dbContext.Users.AsAsyncEnumerable().FirstOrDefaultAsync(u => u.Id.ToString("D") == userId); -#pragma warning restore CS8603 // Possible null reference return. + public virtual async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + using var dbContext = new ApplicationDbContext(_dbContextOptions); + return await dbContext.Users.AsAsyncEnumerable().FirstOrDefaultAsync(u => u.Id.ToString("D") == userId, cancellationToken: cancellationToken); } public virtual async Task ResetUserPassword(Guid id, string password) { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - var user = await dbContext.Users.AsQueryable().FirstAsync(u => u.Id == id); - CreatePasswordHash(password, out string hash, out string metadata); - user.PasswordHash = hash; - user.PasswordHashMetadata = metadata; - dbContext.Update(user); - await dbContext.SaveChangesAsync(); - } + using var dbContext = new ApplicationDbContext(_dbContextOptions); + var user = await dbContext.Users.AsQueryable().FirstAsync(u => u.Id == id); + CreatePasswordHash(password, out string hash, out string metadata); + user.PasswordHash = hash; + user.PasswordHashMetadata = metadata; + dbContext.Update(user); + await dbContext.SaveChangesAsync(); } public virtual async Task DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) { - using (var dbContext = new ApplicationDbContext(_dbContextOptions)) - { - dbContext.Users.Remove(user); - await dbContext.SaveChangesAsync(); + using var dbContext = new ApplicationDbContext(_dbContextOptions); + dbContext.Users.Remove(user); + await dbContext.SaveChangesAsync(cancellationToken); - return IdentityResult.Success; - } + return IdentityResult.Success; } private bool VerifyPasswordHash(string password, string storedHash, string metadata) { try { - var obj = JsonConvert.DeserializeObject(metadata); - if (obj == null) return false; + var node = JsonNode.Parse(metadata)?.AsObject(); + if (node == null) return false; - dynamic metadataObject = obj; - if (metadataObject.HashType != "Argon2d") return false; + if ((string?)node["HashType"] != "Argon2d") return false; + var degreeOfParallelism = (int)node["DegreeOfParallelism"]!; + var memorySize = (int)node["MemorySize"]!; + var iterations = (int)node["Iterations"]!; + var salt = Convert.FromBase64String((string)node["Salt"]!); var passwordBytes = Encoding.UTF8.GetBytes(password); var argon = new Argon2d(passwordBytes) { - DegreeOfParallelism = metadataObject.DegreeOfParallelism, - MemorySize = metadataObject.MemorySize, - Iterations = metadataObject.Iterations, - Salt = metadataObject.Salt + DegreeOfParallelism = degreeOfParallelism, + MemorySize = memorySize, + Iterations = iterations, + Salt = salt, }; var hashBytes = argon.GetBytes(64); @@ -157,11 +131,12 @@ private bool VerifyPasswordHash(string password, string storedHash, string metad } catch (Exception) { + _logger.LogWarning("Password hash verification failed!"); return false; } } - private void CreatePasswordHash(string password, out string hash, out string metadata) + private static void CreatePasswordHash(string password, out string hash, out string metadata) { byte[] salt = new byte[32]; using (var rng = RandomNumberGenerator.Create()) @@ -175,9 +150,9 @@ private void CreatePasswordHash(string password, out string hash, out string met DegreeOfParallelism = 2, MemorySize = 65536, Iterations = 10, - Salt = salt + Salt = Convert.ToBase64String(salt), }; - metadata = JsonConvert.SerializeObject(metadataObject); + metadata = JsonSerializer.Serialize(metadataObject); var passwordBytes = Encoding.UTF8.GetBytes(password); var argon = new Argon2d(passwordBytes) @@ -185,14 +160,14 @@ private void CreatePasswordHash(string password, out string hash, out string met DegreeOfParallelism = metadataObject.DegreeOfParallelism, MemorySize = metadataObject.MemorySize, Iterations = metadataObject.Iterations, - Salt = metadataObject.Salt + Salt = salt, }; var hashBytes = argon.GetBytes(64); hash = Convert.ToBase64String(hashBytes); } - private bool CompareSlowly(byte[] b1, byte[] b2) + private static bool CompareSlowly(byte[] b1, byte[] b2) { if (b1 == null || b2 == null) return (b1 == b2); @@ -205,8 +180,32 @@ private bool CompareSlowly(byte[] b1, byte[] b2) return (val == 0); } + #region IDisposable pattern + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion + #region unused - public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -216,17 +215,17 @@ public Task CreateAsync(ApplicationUser user, CancellationToken throw new NotImplementedException(); } - public Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + public Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken) + public Task SetNormalizedUserNameAsync(ApplicationUser user, string? normalizedName, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken) + public Task SetUserNameAsync(ApplicationUser user, string? userName, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -256,35 +255,35 @@ Task IRoleStore.GetRoleIdAsync(IdentityRole role, Cancella throw new NotImplementedException(); } - Task IRoleStore.GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) + Task IRoleStore.GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) { throw new NotImplementedException(); } - Task IRoleStore.SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken) + Task IRoleStore.SetRoleNameAsync(IdentityRole role, string? roleName, CancellationToken cancellationToken) { throw new NotImplementedException(); } - Task IRoleStore.GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) + Task IRoleStore.GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) { throw new NotImplementedException(); } - Task IRoleStore.SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken) + Task IRoleStore.SetNormalizedRoleNameAsync(IdentityRole role, string? normalizedName, CancellationToken cancellationToken) { throw new NotImplementedException(); } - Task IRoleStore.FindByIdAsync(string roleId, CancellationToken cancellationToken) + Task IRoleStore.FindByIdAsync(string roleId, CancellationToken cancellationToken) { throw new NotImplementedException(); } - Task IRoleStore.FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + Task IRoleStore.FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) { throw new NotImplementedException(); } -#endregion + #endregion } } diff --git a/sama/Startup.cs b/sama/Startup.cs index 30ef954..6081086 100644 --- a/sama/Startup.cs +++ b/sama/Startup.cs @@ -10,113 +10,112 @@ using sama.Services; using System; -namespace sama +namespace sama; + +public class Startup { - public class Startup + public Startup(IWebHostEnvironment env) { - public Startup(IWebHostEnvironment env) + Env = env; + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + if (env.EnvironmentName == "Docker") { - Env = env; - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); - if (env.EnvironmentName == "Docker") - { - builder.AddJsonFile("/opt/sama-docker/appsettings.json", optional: true); - } - Configuration = builder.Build(); + builder.AddJsonFile("/opt/sama-docker/appsettings.json", optional: true); } + Configuration = builder.Build(); + } - public IWebHostEnvironment Env { get; } + public IWebHostEnvironment Env { get; } - public IConfigurationRoot Configuration { get; } + public IConfigurationRoot Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddLogging(builder => { - services.AddLogging(builder => - { - builder.AddConfiguration(Configuration.GetSection("Logging")); - builder.AddConsole(); - builder.AddDebug(); - }); - - var mvcBuilder = services.AddControllersWithViews(); - if (Env.IsDevelopment()) - { - mvcBuilder.AddRazorRuntimeCompilation(); - } - - // Adds a default in-memory implementation of IDistributedCache. - services.AddDistributedMemoryCache(); - services.AddSession(); - - services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); - - services.AddIdentity() - .AddDefaultTokenProviders(); - services.AddTransient, UserManagementService>(); - services.AddTransient, UserManagementService>(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddSingleton(Configuration); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - } + builder.AddConfiguration(Configuration.GetSection("Logging")); + builder.AddConsole(); + builder.AddDebug(); + }); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger, IConfiguration configuration) + var mvcBuilder = services.AddControllersWithViews(); + if (Env.IsDevelopment()) { - logger.LogInformation("SAMA is being configured..."); + mvcBuilder.AddRazorRuntimeCompilation(); + } - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - } + // Adds a default in-memory implementation of IDistributedCache. + services.AddDistributedMemoryCache(); + services.AddSession(); + + services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); + + services.AddIdentity() + .AddDefaultTokenProviders(); + services.AddTransient, UserManagementService>(); + services.AddTransient, UserManagementService>(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddSingleton(Configuration); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + } - app.UseStaticFiles(); + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger, IConfiguration configuration) + { + logger.LogInformation("SAMA is being configured..."); - app.UseRouting(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } - app.UseSession(new SessionOptions { IdleTimeout = TimeSpan.FromMinutes(30) }); + app.UseStaticFiles(); - app.UseAuthentication(); + app.UseRouting(); - app.UseAuthorization(); + app.UseSession(new SessionOptions { IdleTimeout = TimeSpan.FromMinutes(30) }); - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute("default", "{controller=Endpoints}/{action=IndexRedirect}/{id?}"); - }); + app.UseAuthentication(); - var httpRequestVersion = Utility.GetConfiguredHttpRequestVersion(configuration["DefaultHttpVersion"]); - var httpRequestPolicy = Utility.GetConfiguredHttpVersionPolicy(configuration["DefaultHttpVersion"]); - logger.LogInformation($"SAMA configuration is complete. HTTP requests are set to use version {httpRequestVersion} with policy {httpRequestPolicy}."); - } + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Endpoints}/{action=IndexRedirect}/{id?}"); + }); + + var httpRequestVersion = Utility.GetConfiguredHttpRequestVersion(configuration["DefaultHttpVersion"]); + var httpRequestPolicy = Utility.GetConfiguredHttpVersionPolicy(configuration["DefaultHttpVersion"]); + logger.LogInformation($"SAMA configuration is complete. HTTP requests are set to use version {httpRequestVersion} with policy {httpRequestPolicy}."); } } diff --git a/sama/sama.csproj b/sama/sama.csproj index a8f7838..fa7f916 100644 --- a/sama/sama.csproj +++ b/sama/sama.csproj @@ -2,31 +2,30 @@ Exe - net6.0 + net8.0 enable win-x64;linux-arm;linux-x64;linux-arm64;win-arm64 true $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; aspnet-WebApplication1-1C1369D5-B3C7-4414-ADD2-629BA88766C5 - 1.3.0 - 1.3.0 + 1.4.0 + 1.4.0 $([System.DateTime]::UtcNow.ToString(`yyyyMMdd-HHmm`)) + false - + - - - - - - + + + + + - - + diff --git a/unit-tests/Services/HttpCheckServiceTests.cs b/unit-tests/Services/HttpCheckServiceTests.cs index 10f245d..fabc7a7 100644 --- a/unit-tests/Services/HttpCheckServiceTests.cs +++ b/unit-tests/Services/HttpCheckServiceTests.cs @@ -1,12 +1,13 @@ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; +using sama; using sama.Models; using sama.Services; using System; using System.Collections.Generic; using System.Net.Http; +using System.Net.Security; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ public class HttpCheckServiceTests private SettingsService _settingsService; private IConfiguration _configuration; private CertificateValidationService _certService; - private IServiceProvider _serviceProvider; + private HttpHandlerFactory _httpHandlerFactory; private TestHttpHandler _httpHandler; private HttpCheckService _service; @@ -29,10 +30,11 @@ public void Setup() _settingsService = Substitute.For((IServiceProvider)null); _configuration = Substitute.For(); _certService = Substitute.For(_settingsService); - _serviceProvider = TestUtility.InitDI(); - _httpHandler = (TestHttpHandler)_serviceProvider.GetRequiredService(); + _httpHandlerFactory = Substitute.For(); + _httpHandler = Substitute.ForPartsOf(); + _httpHandlerFactory.Create(true, null).ReturnsForAnyArgs(_httpHandler); - _service = new HttpCheckService(_settingsService, _certService, _configuration, _serviceProvider); + _service = new HttpCheckService(_settingsService, _certService, _configuration, _httpHandlerFactory); _settingsService.Monitor_RequestTimeoutSeconds.Returns(1); } @@ -86,21 +88,19 @@ public void CheckShouldSucceedWhenCustomStatusCodesSet() [TestMethod] public void CheckShouldSetAllowAutoRedirectForDefaultStatusCodes() { - Assert.IsTrue(_httpHandler.AllowAutoRedirect); - _service.Check(TestUtility.CreateHttpEndpoint("A", httpLocation: "http://asdf.example.com/fdsa", httpStatusCodes: new List())); - Assert.IsTrue(_httpHandler.AllowAutoRedirect); + _httpHandlerFactory.Received(0).Create(false, Arg.Any()); + _httpHandlerFactory.Received(1).Create(true, Arg.Any()); } [TestMethod] public void CheckShouldDisableAllowAutoRedirectForSpecifiedStatusCodes() { - Assert.IsTrue(_httpHandler.AllowAutoRedirect); - _service.Check(TestUtility.CreateHttpEndpoint("A", httpLocation: "http://asdf.example.com/fdsa", httpStatusCodes: new List { 403 })); - Assert.IsFalse(_httpHandler.AllowAutoRedirect); + _httpHandlerFactory.Received(1).Create(false, Arg.Any()); + _httpHandlerFactory.Received(0).Create(true, Arg.Any()); } } } diff --git a/unit-tests/Services/SlackNotificationServiceTests.cs b/unit-tests/Services/SlackNotificationServiceTests.cs index 2d3587d..99d6526 100644 --- a/unit-tests/Services/SlackNotificationServiceTests.cs +++ b/unit-tests/Services/SlackNotificationServiceTests.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; +using sama; using sama.Models; using sama.Services; using System; @@ -16,22 +16,23 @@ namespace TestSama.Services public class SlackNotificationServiceTests { private SlackNotificationService _service; - private IServiceProvider _provider; private ILogger _logger; private SettingsService _settings; + private HttpHandlerFactory _httpHandlerFactory; private TestHttpHandler _httpHandler; private BackgroundExecutionWrapper _bgExec; [TestInitialize] public void Setup() { - _provider = TestUtility.InitDI(); _logger = Substitute.For>(); _settings = Substitute.For((IServiceProvider)null); - _httpHandler = _provider.GetRequiredService() as TestHttpHandler; + _httpHandlerFactory = Substitute.For(); + _httpHandler = Substitute.ForPartsOf(); + _httpHandlerFactory.Create(true, null).ReturnsForAnyArgs(_httpHandler); _bgExec = Substitute.For(); - _service = new SlackNotificationService(_logger, _settings, _provider, _bgExec); + _service = new SlackNotificationService(_logger, _settings, _httpHandlerFactory, _bgExec); _settings.Notifications_Slack_WebHook.Returns("https://webhook.example.com/hook"); } diff --git a/unit-tests/TestUtility.cs b/unit-tests/TestUtility.cs index 2d1c27e..09bb001 100644 --- a/unit-tests/TestUtility.cs +++ b/unit-tests/TestUtility.cs @@ -7,70 +7,68 @@ using sama.Services; using System; using System.Collections.Generic; -using System.Net.Http; -namespace TestSama +namespace TestSama; + +public static class TestUtility { - public static class TestUtility + public static IServiceProvider InitDI() { - public static IServiceProvider InitDI() - { - var collection = new ServiceCollection(); - - var sqliteConnection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source=file:testdb_{Guid.NewGuid().ToString("N")}.db;Mode=Memory;Cache=Shared"); - sqliteConnection.Open(); + var collection = new ServiceCollection(); - collection.AddDbContext(options => - { - options.UseSqlite(sqliteConnection); - }); + var sqliteConnection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source=file:testdb_{Guid.NewGuid().ToString("N")}.db;Mode=Memory;Cache=Shared"); + sqliteConnection.Open(); - collection.AddSingleton(Substitute.ForPartsOf()); - collection.AddSingleton(Substitute.For(null, null)); + collection.AddDbContext(options => + { + options.UseSqlite(sqliteConnection); + }); - var provider = collection.BuildServiceProvider(true); + collection.AddSingleton(Substitute.For()); + collection.AddSingleton(Substitute.For(null, null)); - using (var scope = provider.CreateScope()) - { - var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Database.OpenConnection(); - dbContext.Database.Migrate(); - dbContext.SaveChanges(); - } + var provider = collection.BuildServiceProvider(true); - return provider; + using (var scope = provider.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.OpenConnection(); + dbContext.Database.Migrate(); + dbContext.SaveChanges(); } - public static Endpoint CreateHttpEndpoint(string name, bool enabled = true, int id = 0, string httpLocation = null, string httpResponseMatch = null, List httpStatusCodes = null) + return provider; + } + + public static Endpoint CreateHttpEndpoint(string name, bool enabled = true, int id = 0, string httpLocation = null, string httpResponseMatch = null, List httpStatusCodes = null) + { + var endpoint = new Endpoint { - var endpoint = new Endpoint - { - Id = id, - Name = name, - Enabled = enabled, - Kind = Endpoint.EndpointKind.Http - }; + Id = id, + Name = name, + Enabled = enabled, + Kind = Endpoint.EndpointKind.Http + }; - if (httpLocation != null) endpoint.SetHttpLocation(httpLocation); - if (httpResponseMatch != null) endpoint.SetHttpResponseMatch(httpResponseMatch); - if (httpStatusCodes != null) endpoint.SetHttpStatusCodes(httpStatusCodes); + if (httpLocation != null) endpoint.SetHttpLocation(httpLocation); + if (httpResponseMatch != null) endpoint.SetHttpResponseMatch(httpResponseMatch); + if (httpStatusCodes != null) endpoint.SetHttpStatusCodes(httpStatusCodes); - return endpoint; - } + return endpoint; + } - public static Endpoint CreateIcmpEndpoint(string name, bool enabled = true, int id = 0, string icmpAddress = null) + public static Endpoint CreateIcmpEndpoint(string name, bool enabled = true, int id = 0, string icmpAddress = null) + { + var endpoint = new Endpoint { - var endpoint = new Endpoint - { - Id = id, - Name = name, - Enabled = enabled, - Kind = Endpoint.EndpointKind.Icmp - }; + Id = id, + Name = name, + Enabled = enabled, + Kind = Endpoint.EndpointKind.Icmp + }; - if (icmpAddress != null) endpoint.SetIcmpAddress(icmpAddress); + if (icmpAddress != null) endpoint.SetIcmpAddress(icmpAddress); - return endpoint; - } + return endpoint; } } diff --git a/unit-tests/unit-tests.csproj b/unit-tests/unit-tests.csproj index ff1257e..099a1c5 100644 --- a/unit-tests/unit-tests.csproj +++ b/unit-tests/unit-tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -9,11 +9,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive