diff --git a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
index 8aeae5d..e36b394 100644
--- a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
+++ b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
@@ -38,11 +38,11 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
\ No newline at end of file
diff --git a/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs
new file mode 100644
index 0000000..9dbd87a
--- /dev/null
+++ b/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs
@@ -0,0 +1,103 @@
+using System.Linq;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AMWD.Common.AspNetCore.Attributes
+{
+ ///
+ /// Implements an IP filter. The defined addresses are blocked.
+ ///
+ public class IPBlacklistAttribute : ActionFilterAttribute
+ {
+ ///
+ /// Gets or sets a value indicating whether local (localhost) access is blocked (Default: false).
+ ///
+ public bool RestrictLocalAccess { get; set; }
+
+ ///
+ /// Gets or sets a configuration key where the blocked IP addresses are defined.
+ ///
+ ///
+ /// JSON configuration example:
+ /// {
+ /// "ConfigurationKey": [
+ /// "10.0.0.0/8",
+ /// "172.16.0.0/12",
+ /// "fd00:123:abc::13"
+ /// ]
+ /// }
+ ///
+ public string ConfigurationKey { get; set; }
+
+ ///
+ /// Gets or sets a comma separated list of blocked IP addresses.
+ ///
+ public string RestrictedIpAddresses { get; set; }
+
+ ///
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ base.OnActionExecuting(context);
+
+ if (!RestrictLocalAccess && context.HttpContext.IsLocalRequest())
+ return;
+
+ var remoteIpAddress = context.HttpContext.GetRemoteIpAddress();
+ if (!string.IsNullOrWhiteSpace(RestrictedIpAddresses))
+ {
+ string[] ipAddresses = RestrictedIpAddresses.Split(',');
+ foreach (string ipAddress in ipAddresses)
+ {
+ if (string.IsNullOrWhiteSpace(ipAddress))
+ continue;
+
+ if (MatchesIpAddress(ipAddress, remoteIpAddress))
+ {
+ context.Result = new ForbidResult();
+ return;
+ }
+ }
+ }
+
+ var configuration = context.HttpContext.RequestServices.GetService();
+ if (!string.IsNullOrWhiteSpace(ConfigurationKey) && configuration != null)
+ {
+ var section = configuration.GetSection(ConfigurationKey);
+ if (!section.Exists())
+ return;
+
+ foreach (var child in section.GetChildren())
+ {
+ if (string.IsNullOrWhiteSpace(child.Value))
+ continue;
+
+ if (MatchesIpAddress(child.Value, remoteIpAddress))
+ {
+ context.Result = new ForbidResult();
+ return;
+ }
+ }
+ }
+ }
+
+ private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress)
+ {
+ if (configIpAddress.Contains('/'))
+ {
+ string[] ipNetworkParts = configIpAddress.Split('/');
+ var ip = IPAddress.Parse(ipNetworkParts.First());
+ int prefix = int.Parse(ipNetworkParts.Last());
+
+ var net = new IPNetwork(ip, prefix);
+ return net.Contains(remoteIpAddress);
+ }
+
+ return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress);
+ }
+ }
+}
diff --git a/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs
new file mode 100644
index 0000000..568fe6d
--- /dev/null
+++ b/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs
@@ -0,0 +1,102 @@
+using System.Linq;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AMWD.Common.AspNetCore.Attributes
+{
+ ///
+ /// Implements an IP filter. Only defined addresses are allowed to access.
+ ///
+ public class IPWhitelistAttribute : ActionFilterAttribute
+ {
+ ///
+ /// Gets or sets a value indicating whether local (localhost) access is granted (Default: true).
+ ///
+ public bool AllowLocalAccess { get; set; } = true;
+
+ ///
+ /// Gets or sets a configuration key where the allowed IP addresses are allowed.
+ ///
+ ///
+ /// JSON configuration example:
+ /// {
+ /// "ConfigurationKey": [
+ /// "10.0.0.0/8",
+ /// "172.16.0.0/12",
+ /// "fd00:123:abc::13"
+ /// ]
+ /// }
+ ///
+ public string ConfigurationKey { get; set; }
+
+ ///
+ /// Gets or sets a comma separated list of allowed IP addresses.
+ ///
+ public string AllowedIpAddresses { get; set; }
+
+ ///
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ base.OnActionExecuting(context);
+
+ if (AllowLocalAccess && context.HttpContext.IsLocalRequest())
+ return;
+
+ var remoteIpAddress = context.HttpContext.GetRemoteIpAddress();
+ if (!string.IsNullOrWhiteSpace(AllowedIpAddresses))
+ {
+ string[] ipAddresses = AllowedIpAddresses.Split(',');
+ foreach (string ipAddress in ipAddresses)
+ {
+ if (string.IsNullOrWhiteSpace(ipAddress))
+ continue;
+
+ if (MatchesIpAddress(ipAddress, remoteIpAddress))
+ return;
+ }
+ }
+
+ var configuration = context.HttpContext.RequestServices.GetService();
+ if (!string.IsNullOrWhiteSpace(ConfigurationKey) && configuration != null)
+ {
+ var section = configuration.GetSection(ConfigurationKey);
+ if (!section.Exists())
+ {
+ context.Result = new ForbidResult();
+ return;
+ }
+
+ foreach (var child in section.GetChildren())
+ {
+ if (string.IsNullOrWhiteSpace(child.Value))
+ continue;
+
+ if (MatchesIpAddress(child.Value, remoteIpAddress))
+ return;
+ }
+ }
+
+ context.Result = new ForbidResult();
+ }
+
+ private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress)
+ {
+ if (configIpAddress.Contains('/'))
+ {
+ string[] ipNetworkParts = configIpAddress.Split('/');
+ var ip = IPAddress.Parse(ipNetworkParts.First());
+ int prefix = int.Parse(ipNetworkParts.Last());
+
+ var net = new IPNetwork(ip, prefix);
+ return net.Contains(remoteIpAddress);
+ }
+
+ return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress);
+ }
+ }
+}
diff --git a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
index 0f0c011..1e593c7 100644
--- a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
+++ b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http
///
/// Retrieves the antiforgery token.
///
- /// The web context.
+ /// The current .
/// Name and value of the token.
public static (string Name, string Value) GetAntiforgeryToken(this HttpContext httpContext)
{
@@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Http
///
/// Returns the remote ip address.
///
- /// The web context.
+ /// The current .
/// The name of the header to resolve the when behind a proxy (Default: X-Forwarded-For).
/// The ip address of the client.
public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string headerName = "X-Forwarded-For")
@@ -39,10 +39,22 @@ namespace Microsoft.AspNetCore.Http
return remote;
}
+ ///
+ /// Returns whether the request was made locally.
+ ///
+ /// The current .
+ /// The name of the header to resolve the when behind a proxy (Default: X-Forwarded-For).
+ ///
+ public static bool IsLocalRequest(this HttpContext httpContext, string headerName = "X-Forwarded-For")
+ {
+ var remoteIpAddress = httpContext.GetRemoteIpAddress(headerName);
+ return httpContext.Connection.LocalIpAddress.Equals(remoteIpAddress);
+ }
+
///
/// Tries to retrieve the return url.
///
- ///
+ /// The current .
///
public static string GetReturnUrl(this HttpContext httpContext)
{
diff --git a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj
index 31fb867..28866d4 100644
--- a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj
+++ b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj
@@ -56,7 +56,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs
new file mode 100644
index 0000000..460e6a7
--- /dev/null
+++ b/AMWD.Common.EntityFrameworkCore/Converters/DateOnlyConverter.cs
@@ -0,0 +1,28 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AMWD.Common.EntityFrameworkCore.Converters
+{
+ ///
+ /// Defines the conversion from a object to a which can be handled by the database engine.
+ ///
+ ///
+ /// As of 2022-06-04 only required for Microsoft SQL server on .NET 6.0.
+ ///
+ public class DateOnlyConverter : ValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DateOnlyConverter()
+ : base(
+ d => d.ToDateTime(TimeOnly.MinValue),
+ d => DateOnly.FromDateTime(d)
+ )
+ { }
+ }
+}
+
+#endif
diff --git a/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs
new file mode 100644
index 0000000..2587912
--- /dev/null
+++ b/AMWD.Common.EntityFrameworkCore/Converters/NullableDateOnlyConverter.cs
@@ -0,0 +1,28 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AMWD.Common.EntityFrameworkCore.Converters
+{
+ ///
+ /// Defines the conversion from a nullable object to a nullable which can be handled by the database engine.
+ ///
+ ///
+ /// As of 2022-06-04 only required for Microsoft SQL server on .NET 6.0.
+ ///
+ public class NullableDateOnlyConverter : ValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NullableDateOnlyConverter()
+ : base(
+ d => d == null ? null : new DateTime?(d.Value.ToDateTime(TimeOnly.MinValue)),
+ d => d == null ? null : new DateOnly?(DateOnly.FromDateTime(d.Value))
+ )
+ { }
+ }
+}
+
+#endif
diff --git a/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs
new file mode 100644
index 0000000..bafc8ca
--- /dev/null
+++ b/AMWD.Common.EntityFrameworkCore/Converters/NullableTimeOnlyConverter.cs
@@ -0,0 +1,28 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AMWD.Common.EntityFrameworkCore.Converters
+{
+ ///
+ /// Defines the conversion from a nullable object to a nullable which can be handled by the database engine.
+ ///
+ ///
+ /// As of 2022-06-04 only required for Microsoft SQL server on .NET 6.0.
+ ///
+ public class NullableTimeOnlyConverter : ValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NullableTimeOnlyConverter()
+ : base(
+ d => d == null ? null : new TimeSpan?(d.Value.ToTimeSpan()),
+ d => d == null ? null : new TimeOnly?(TimeOnly.FromTimeSpan(d.Value))
+ )
+ { }
+ }
+}
+
+#endif
diff --git a/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs b/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs
new file mode 100644
index 0000000..acb4ccc
--- /dev/null
+++ b/AMWD.Common.EntityFrameworkCore/Converters/TimeOnlyConverter.cs
@@ -0,0 +1,28 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AMWD.Common.EntityFrameworkCore.Converters
+{
+ ///
+ /// Defines the conversion from a object to a which can be handled by the database engine.
+ ///
+ ///
+ /// As of 2022-06-04 only required for Microsoft SQL server on .NET 6.0.
+ ///
+ public class TimeOnlyConverter : ValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TimeOnlyConverter()
+ : base(
+ t => t.ToTimeSpan(),
+ t => TimeOnly.FromTimeSpan(t)
+ )
+ { }
+ }
+}
+
+#endif
diff --git a/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs b/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs
new file mode 100644
index 0000000..8c8f8b1
--- /dev/null
+++ b/AMWD.Common.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs
@@ -0,0 +1,43 @@
+#if NET6_0_OR_GREATER
+
+using System;
+using AMWD.Common.EntityFrameworkCore.Converters;
+using Microsoft.EntityFrameworkCore;
+
+namespace AMWD.Common.EntityFrameworkCore.Extensions
+{
+ ///
+ /// Extensions for the of entity framework core.
+ ///
+ public static class ModelConfigurationBuilderExtensions
+ {
+ ///
+ /// Adds converters for the and datatypes introduced with .NET 6.0.
+ ///
+ ///
+ /// As of 2022-06-04 only required for Microsoft SQL server on .NET 6.0.
+ ///
+ /// The instance.
+ /// The instance after applying the converters.
+ public static ModelConfigurationBuilder AddDateOnlyTimeOnlyConverters(this ModelConfigurationBuilder builder)
+ {
+ builder.Properties()
+ .HaveConversion()
+ .HaveColumnType("date");
+ builder.Properties()
+ .HaveConversion()
+ .HaveColumnType("date");
+
+ builder.Properties()
+ .HaveConversion()
+ .HaveColumnType("time");
+ builder.Properties()
+ .HaveConversion()
+ .HaveColumnType("time");
+
+ return builder;
+ }
+ }
+}
+
+#endif
diff --git a/AMWD.Common.Moq/AMWD.Common.Moq.csproj b/AMWD.Common.Moq/AMWD.Common.Moq.csproj
index b0afde3..e4e6c8f 100644
--- a/AMWD.Common.Moq/AMWD.Common.Moq.csproj
+++ b/AMWD.Common.Moq/AMWD.Common.Moq.csproj
@@ -36,8 +36,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/AMWD.Common.Moq/TcpClientMoq.cs b/AMWD.Common.Moq/TcpClientMoq.cs
new file mode 100644
index 0000000..cc1eced
--- /dev/null
+++ b/AMWD.Common.Moq/TcpClientMoq.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+
+namespace AMWD.Common.Moq
+{
+ ///
+ /// Wrapps the including the setup.
+ ///
+ public class TcpClientMoq
+ {
+ private readonly Mock streamMock;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TcpClientMoq()
+ {
+ Callbacks = new();
+ Response = new byte[0];
+
+ streamMock = new();
+ streamMock
+ .Setup(s => s.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((buffer, offset, count, _) =>
+ {
+ var callback = new TcpClientCallback
+ {
+ Buffer = new byte[count],
+ Offset = offset,
+ Count = count,
+ Type = TcpClientCallback.WriteType.Asynchronous
+ };
+ Array.Copy(buffer, offset, callback.Buffer, 0, count);
+
+ Callbacks.Add(callback);
+ })
+ .Returns(Task.CompletedTask);
+ streamMock
+ .Setup(s => s.Write(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((buffer, offset, count) =>
+ {
+ var callback = new TcpClientCallback
+ {
+ Buffer = new byte[count],
+ Offset = offset,
+ Count = count,
+ Type = TcpClientCallback.WriteType.Synchronous
+ };
+ Array.Copy(buffer, offset, callback.Buffer, 0, count);
+
+ Callbacks.Add(callback);
+ });
+
+ streamMock
+ .Setup(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((buffer, offset, count, _) =>
+ {
+ byte[] bytes = Response ?? new byte[0];
+ Array.Copy(bytes, 0, buffer, offset, Math.Min(bytes.Length, count));
+ })
+ .ReturnsAsync(Response?.Length ?? 0);
+ streamMock
+ .Setup(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((buffer, offset, count) =>
+ {
+ byte[] bytes = Response ?? new byte[0];
+ Array.Copy(bytes, 0, buffer, offset, Math.Min(bytes.Length, count));
+ })
+ .Returns(Response?.Length ?? 0);
+
+ Mock = new();
+ Mock
+ .Setup(c => c.GetStream())
+ .Returns(streamMock.Object);
+ }
+
+ ///
+ /// Gets the mocked .
+ ///
+ public Mock Mock { get; }
+
+ ///
+ /// Gets the placed request.
+ ///
+ public List Callbacks { get; }
+
+ ///
+ /// Gets the byte response, that should be "sent".
+ ///
+ public byte[] Response { get; set; }
+
+ ///
+ /// Resets the and .
+ ///
+ public void Reset()
+ {
+ Response = new byte[0];
+ Callbacks.Clear();
+ }
+
+ ///
+ /// Verifies the number of calls writing asynchronous to the stream.
+ ///
+ /// Number of calls.
+ public void VerifyWriteAsync(Times times)
+ => streamMock.Verify(s => s.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), times);
+
+ ///
+ /// Verifies the number of calls writing synchronous to the stream.
+ ///
+ /// Number of calls.
+ public void VerifyWriteSync(Times times)
+ => streamMock.Verify(s => s.Write(It.IsAny(), It.IsAny(), It.IsAny()), times);
+
+ ///
+ /// Verifies the number of calls reading asynchronous from the stream.
+ ///
+ /// Number of calls.
+ public void VerifyReadAsync(Times times)
+ => streamMock.Verify(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), times);
+
+ ///
+ /// Verifies the number of calls reading synchronous from the stream.
+ ///
+ /// Number of calls.
+ public void VerifyReadSync(Times times)
+ => streamMock.Verify(s => s.Read(It.IsAny(), It.IsAny(), It.IsAny()), times);
+
+ ///
+ /// Represents the placed TCP request.
+ ///
+ public class TcpClientCallback
+ {
+ ///
+ /// Gets or sets the type (a/synchronous call).
+ ///
+ public WriteType Type { get; set; }
+
+ ///
+ /// Gets or sets the buffer content.
+ ///
+ public byte[] Buffer { get; set; }
+
+ ///
+ /// Gets or sets the offset.
+ ///
+ public int Offset { get; set; }
+
+ ///
+ /// Gets or sets the byte count.
+ ///
+ public int Count { get; set; }
+
+ ///
+ /// Lists the possible request types.
+ ///
+ public enum WriteType
+ {
+ ///
+ /// The request was synchronous.
+ ///
+ Synchronous = 1,
+
+ ///
+ /// The request was asynchronous.
+ ///
+ Asynchronous = 2
+ }
+ }
+ }
+}
diff --git a/AMWD.Common.Tests/AMWD.Common.Tests.csproj b/AMWD.Common.Tests/AMWD.Common.Tests.csproj
index 5e7b74c..bf1fc7d 100644
--- a/AMWD.Common.Tests/AMWD.Common.Tests.csproj
+++ b/AMWD.Common.Tests/AMWD.Common.Tests.csproj
@@ -7,13 +7,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
diff --git a/AMWD.Common/AMWD.Common.csproj b/AMWD.Common/AMWD.Common.csproj
index aaf036e..3122dbd 100644
--- a/AMWD.Common/AMWD.Common.csproj
+++ b/AMWD.Common/AMWD.Common.csproj
@@ -38,8 +38,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39ffcbb..a8798d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.4.3...master) - 0000-00-00
+## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.5.0...master) - 0000-00-00
_nothing changed yet_
+## [v1.5.0](https://git.am-wd.de/AM.WD/common/compare/v1.4.3...v1.5.0) - 2022-06-15
+### Added
+- `TcpClientMoq` to test communication via a `TcpClient`
+- EntityFramework Core Converters for new `DateOnly` and `TimeOnly` datatypes when using SQL Server on .NET 6.0 (Bug on Microsoft's EntityFramework)
+- `HttpContext.IsLocalRequest()` to determine whether the request was from local or remote.
+- `IPWhitelistAttribute` and `IPBlacklistAttribute` to allow/restrict access on specific controllers/actions.
+
## [v1.4.3](https://git.am-wd.de/AM.WD/common/compare/v1.4.2...v1.4.3) - 2022-05-12
### Fixed