Solution restructured to use multiple test projects
This commit is contained in:
20
src/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
Normal file
20
src/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
|
||||
|
||||
<AssemblyName>amwd-common-aspnetcore</AssemblyName>
|
||||
<RootNamespace>AMWD.Common.AspNetCore</RootNamespace>
|
||||
|
||||
<NrtTagMatch>asp/v[0-9]*</NrtTagMatch>
|
||||
<PackageId>AMWD.Common.AspNetCore</PackageId>
|
||||
<Product>AM.WD Common Library for ASP.NET Core</Product>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Unclassified.DeepConvert" Version="1.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.AspNetCore.Security.BasicAuthentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Authorization
|
||||
{
|
||||
/// <summary>
|
||||
/// A basic authentication as attribute to use for specific actions.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public class BasicAuthenticationAttribute : Attribute, IAsyncAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a username to validate.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a password to validate.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a realm used on authentication header.
|
||||
/// </summary>
|
||||
public string Realm { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetService<ILogger<BasicAuthenticationAttribute>>();
|
||||
try
|
||||
{
|
||||
var validatorResult = await TrySetHttpUser(context).ConfigureAwait(false);
|
||||
bool isAllowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
|
||||
if (isAllowAnonymous)
|
||||
return;
|
||||
|
||||
if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeaderValue))
|
||||
{
|
||||
SetAuthenticateRequest(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue);
|
||||
byte[] decoded = Convert.FromBase64String(authHeader.Parameter);
|
||||
string plain = Encoding.UTF8.GetString(decoded);
|
||||
|
||||
// See: https://www.rfc-editor.org/rfc/rfc2617, page 6
|
||||
string username = plain.Split(':').First();
|
||||
string password = plain[(username.Length + 1)..];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
if (Username == username && Password == password)
|
||||
return;
|
||||
}
|
||||
|
||||
if (validatorResult == null)
|
||||
SetAuthenticateRequest(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, $"Failed to execute the basic authentication attribute: {ex.InnerException?.Message ?? ex.Message}");
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAuthenticateRequest(AuthorizationFilterContext context)
|
||||
{
|
||||
var validator = context.HttpContext.RequestServices.GetService<IBasicAuthenticationValidator>();
|
||||
string realm = string.IsNullOrWhiteSpace(Realm)
|
||||
? string.IsNullOrWhiteSpace(validator?.Realm)
|
||||
? null
|
||||
: validator.Realm
|
||||
: Realm;
|
||||
|
||||
context.HttpContext.Response.Headers.WWWAuthenticate = "Basic";
|
||||
if (!string.IsNullOrWhiteSpace(realm))
|
||||
context.HttpContext.Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm.Trim().Replace("\"", "")}\"";
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
|
||||
private static async Task<ClaimsPrincipal> TrySetHttpUser(AuthorizationFilterContext context)
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetService<ILogger<BasicAuthenticationAttribute>>();
|
||||
try
|
||||
{
|
||||
if (context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeaderValue))
|
||||
{
|
||||
var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue);
|
||||
byte[] decoded = Convert.FromBase64String(authHeader.Parameter);
|
||||
string plain = Encoding.UTF8.GetString(decoded);
|
||||
|
||||
// See: https://www.rfc-editor.org/rfc/rfc2617, page 6
|
||||
string username = plain.Split(':').First();
|
||||
string password = plain[(username.Length + 1)..];
|
||||
|
||||
var validator = context.HttpContext.RequestServices.GetService<IBasicAuthenticationValidator>();
|
||||
if (validator == null)
|
||||
return null;
|
||||
|
||||
var result = await validator.ValidateAsync(username, password, context.HttpContext.GetRemoteIpAddress(), context.HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
context.HttpContext.User = result;
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, $"Using validator to get HTTP user failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom filter attribute to use Google's reCaptcha (v3).
|
||||
/// <br/>
|
||||
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// appsettings.json:
|
||||
/// <br/>
|
||||
/// <code>
|
||||
/// {<br/>
|
||||
/// [...]<br/>
|
||||
/// "Google": {<br/>
|
||||
/// "ReCaptcha": {<br/>
|
||||
/// "PrivateKey": "__private reCaptcha key__",<br/>
|
||||
/// "PublicKey": "__public reCaptcha key__"<br/>
|
||||
/// }<br/>
|
||||
/// }<br/>
|
||||
/// }
|
||||
/// </code>
|
||||
/// <br/>
|
||||
/// The score from google can be found on HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey].
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class GoogleReCaptchaAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The error key used in <see cref="ActionContext.ModelState"/>.
|
||||
/// </summary>
|
||||
public const string ErrorKey = "GoogleReCaptcha";
|
||||
|
||||
/// <summary>
|
||||
/// The key used in forms submitted to the backend.
|
||||
/// </summary>
|
||||
public const string ResponseTokenKey = "g-recaptcha-response";
|
||||
|
||||
/// <summary>
|
||||
/// The key used in <see cref="Http.HttpContext.Items"/> to transport the score (0 - bot, 1 - human).
|
||||
/// </summary>
|
||||
public const string ScoreKey = "GoogleReCaptchaScore";
|
||||
|
||||
private const string VerificationUrl = "https://www.google.com/recaptcha/api/siteverify";
|
||||
|
||||
private string _privateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the validattion in background.
|
||||
/// </summary>
|
||||
/// <param name="context">The action context.</param>
|
||||
/// <param name="next">The following action delegate.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
|
||||
_privateKey = configuration?.GetValue<string>("Google:ReCaptcha:PrivateKey");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_privateKey))
|
||||
return;
|
||||
|
||||
await DoValidation(context).ConfigureAwait(false);
|
||||
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task DoValidation(ActionExecutingContext context)
|
||||
{
|
||||
if (!context.HttpContext.Request.HasFormContentType)
|
||||
return;
|
||||
|
||||
var token = context.HttpContext.Request.Form[ResponseTokenKey];
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
context.ModelState.TryAddModelError(ErrorKey, "No token to validate Google reCaptcha");
|
||||
return;
|
||||
}
|
||||
|
||||
await Validate(context, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Validate(ActionExecutingContext context, string token)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var param = new Dictionary<string, string>
|
||||
{
|
||||
{ "secret", _privateKey },
|
||||
{ "response", token }
|
||||
};
|
||||
var response = await httpClient.PostAsync(VerificationUrl, new FormUrlEncodedContent(param)).ConfigureAwait(false);
|
||||
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
var result = JsonConvert.DeserializeObject<Response>(json);
|
||||
if (result?.Success != true)
|
||||
{
|
||||
context.ModelState.TryAddModelError(ErrorKey, "Google reCaptcha verification failed");
|
||||
context.HttpContext.Items[ScoreKey] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.HttpContext.Items[ScoreKey] = result.Score;
|
||||
}
|
||||
}
|
||||
|
||||
private class Response
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public decimal Score { get; set; }
|
||||
|
||||
public string Action { get; set; }
|
||||
|
||||
[JsonProperty("challenge_ts")]
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
public string Hostname { get; set; }
|
||||
|
||||
[JsonProperty("error-codes")]
|
||||
public List<string> ErrorCodes { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/AMWD.Common.AspNetCore/Attributes/IPAllowListAttribute.cs
Normal file
108
src/AMWD.Common.AspNetCore/Attributes/IPAllowListAttribute.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements an IP filter. Only defined addresses are allowed to access.
|
||||
/// </summary>
|
||||
public class IPAllowListAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether local (localhost) access is granted (Default: true).
|
||||
/// </summary>
|
||||
public bool AllowLocalAccess { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a configuration key where the allowed IP addresses are allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// JSON configuration example:<br/>
|
||||
/// {<br/>
|
||||
/// "ConfigurationKey": [<br/>
|
||||
/// "10.0.0.0/8",<br/>
|
||||
/// "172.16.0.0/12",<br/>
|
||||
/// "fd00:123:abc::13"<br/>
|
||||
/// ]<br/>
|
||||
/// }
|
||||
/// </remarks>
|
||||
public string ConfigurationKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma separated list of allowed IP addresses.
|
||||
/// </summary>
|
||||
public string AllowedIpAddresses { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
base.OnActionExecuting(context);
|
||||
context.HttpContext.Items["RemoteAddress"] = context.HttpContext.GetRemoteIpAddress();
|
||||
|
||||
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<IConfiguration>();
|
||||
if (!string.IsNullOrWhiteSpace(ConfigurationKey) && configuration != null)
|
||||
{
|
||||
var section = configuration.GetSection(ConfigurationKey);
|
||||
if (!section.Exists())
|
||||
{
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var child in section.GetChildren())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(child.Value))
|
||||
continue;
|
||||
|
||||
if (MatchesIpAddress(child.Value, remoteIpAddress))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/AMWD.Common.AspNetCore/Attributes/IPBlockListAttribute.cs
Normal file
109
src/AMWD.Common.AspNetCore/Attributes/IPBlockListAttribute.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements an IP filter. The defined addresses are blocked.
|
||||
/// </summary>
|
||||
public class IPBlockListAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether local (localhost) access is blocked (Default: false).
|
||||
/// </summary>
|
||||
public bool BlockLocalAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a configuration key where the blocked IP addresses are defined.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// JSON configuration example:<br/>
|
||||
/// {<br/>
|
||||
/// "ConfigurationKey": [<br/>
|
||||
/// "10.0.0.0/8",<br/>
|
||||
/// "172.16.0.0/12",<br/>
|
||||
/// "fd00:123:abc::13"<br/>
|
||||
/// ]<br/>
|
||||
/// }
|
||||
/// </remarks>
|
||||
public string ConfigurationKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma separated list of blocked IP addresses.
|
||||
/// </summary>
|
||||
public string BlockedIpAddresses { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
base.OnActionExecuting(context);
|
||||
context.HttpContext.Items["RemoteAddress"] = context.HttpContext.GetRemoteIpAddress();
|
||||
|
||||
if (!BlockLocalAccess && context.HttpContext.IsLocalRequest())
|
||||
return;
|
||||
|
||||
var remoteIpAddress = context.HttpContext.GetRemoteIpAddress();
|
||||
if (!string.IsNullOrWhiteSpace(BlockedIpAddresses))
|
||||
{
|
||||
string[] ipAddresses = BlockedIpAddresses.Split(',');
|
||||
foreach (string ipAddress in ipAddresses)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
continue;
|
||||
|
||||
if (MatchesIpAddress(ipAddress, remoteIpAddress))
|
||||
{
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
|
||||
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 StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IApplicationBuilder"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds settings to run behind a reverse proxy (e.g. NginX).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A base path (e.g. running in a sub-directory /app) for the application can be defined via <c>ASPNETCORE_APPL_PATH</c> environment variable.
|
||||
/// <br/>
|
||||
/// <br/>
|
||||
/// Additionally you can specify the proxy server by using <paramref name="address"/> or a <paramref name="network"/> when there are multiple proxy servers.
|
||||
/// <br/>
|
||||
/// When neither <paramref name="address"/> nor <paramref name="network"/> is set, the default subnets are configured:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>127.0.0.0/8</c></item>
|
||||
/// <item><c>::1/128</c></item>
|
||||
///
|
||||
/// <item><c>10.0.0.0/8</c></item>
|
||||
/// <item><c>172.16.0.0/12</c></item>
|
||||
/// <item><c>192.168.0.0/16</c></item>
|
||||
/// <item><c>fd00::/8</c></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="app">The application builder.</param>
|
||||
/// <param name="network">The <see cref="IPNetwork"/> where proxy requests are received from (optional).</param>
|
||||
/// <param name="address">The <see cref="IPAddress"/> where proxy requests are received from (optional).</param>
|
||||
/// <param name="basePath">A custom base path (optional, <c>ASPNETCORE_APPL_PATH</c> is prefererred).</param>
|
||||
public static IApplicationBuilder UseProxyHosting(this IApplicationBuilder app, IPNetwork network = null, IPAddress address = null, string basePath = null)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
path = basePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
app.UsePathBase(new PathString(path));
|
||||
|
||||
var options = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||
options.KnownProxies.Clear();
|
||||
options.KnownNetworks.Clear();
|
||||
|
||||
if (network == null && address == null)
|
||||
{
|
||||
// localhost
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Loopback, 8));
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.IPv6Loopback, 128));
|
||||
|
||||
// private IPv4 networks
|
||||
// see https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
|
||||
|
||||
// private IPv6 networks
|
||||
// see https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fd00::"), 8));
|
||||
}
|
||||
|
||||
if (network != null)
|
||||
options.KnownNetworks.Add(network);
|
||||
|
||||
if (address != null)
|
||||
options.KnownProxies.Add(address);
|
||||
|
||||
app.UseForwardedHeaders(options);
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs
Normal file
81
src/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the HTML (e.g. <see cref="IHtmlHelper"/>).
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static class HtmlExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix used to identify JavaScript parts.
|
||||
/// </summary>
|
||||
public static string JSPrefix { get; set; } = "_JS_";
|
||||
|
||||
/// <summary>
|
||||
/// The prefix used to identify CascadingStyleSheet parts.
|
||||
/// </summary>
|
||||
public static string CSSPrefix { get; set; } = "_CSS_";
|
||||
|
||||
/// <summary>
|
||||
/// Add a js snippet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <param name="template">The template to use to add the snippet.</param>
|
||||
/// <returns></returns>
|
||||
public static T AddJS<T>(this IHtmlHelper<T> htmlHelper, Func<object, HelperResult> template)
|
||||
{
|
||||
htmlHelper.ViewContext.HttpContext.Items[$"{JSPrefix}{Guid.NewGuid()}"] = template;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the js snippets into the view.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <returns></returns>
|
||||
public static T RenderJS<T>(this IHtmlHelper<T> htmlHelper)
|
||||
{
|
||||
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
|
||||
{
|
||||
if (key.ToString().StartsWith(JSPrefix) && htmlHelper.ViewContext.HttpContext.Items[key] is Func<object, HelperResult> template)
|
||||
htmlHelper.ViewContext.Writer.WriteLine(template(null));
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a css snippet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <param name="template">The template to use to add the snippet.</param>
|
||||
/// <returns></returns>
|
||||
public static T AddCSS<T>(this IHtmlHelper<T> htmlHelper, Func<object, HelperResult> template)
|
||||
{
|
||||
htmlHelper.ViewContext.HttpContext.Items[$"{CSSPrefix}{Guid.NewGuid()}"] = template;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the css snippets into the view.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <returns></returns>
|
||||
public static T RenderCSS<T>(this IHtmlHelper<T> htmlHelper)
|
||||
{
|
||||
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
|
||||
{
|
||||
if (key.ToString().StartsWith(CSSPrefix) && htmlHelper.ViewContext.HttpContext.Items[key] is Func<object, HelperResult> template)
|
||||
htmlHelper.ViewContext.Writer.WriteLine(template(null));
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
Normal file
124
src/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="HttpContext"/>.
|
||||
/// </summary>
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
// Search these additional headers for a remote client ip address.
|
||||
private static readonly string[] _defaultIpHeaderNames =
|
||||
[
|
||||
"Cf-Connecting-Ip", // set by Cloudflare
|
||||
"X-Real-IP", // wide-spread alternative to X-Forwarded-For
|
||||
"X-Forwarded-For", // commonly used on all known proxies
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the antiforgery token.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
/// <returns>FormName, HeaderName and Value of the antiforgery token.</returns>
|
||||
public static (string FormName, string HeaderName, string Value) GetAntiforgeryToken(this HttpContext httpContext)
|
||||
{
|
||||
var antiforgery = httpContext.RequestServices.GetService<IAntiforgery>();
|
||||
var tokenSet = antiforgery?.GetAndStoreTokens(httpContext);
|
||||
|
||||
return (tokenSet?.FormFieldName, tokenSet?.HeaderName, tokenSet?.RequestToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the remote ip address.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Searches for additional headers in the following order:
|
||||
/// <list type="number">
|
||||
/// <item>Cf-Connecting-Ip</item>
|
||||
/// <item>X-Real-IP</item>
|
||||
/// <item>X-Forwarded-For</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
/// <param name="ipHeaderName">The name of the header to resolve the <see cref="IPAddress"/> when behind a proxy.</param>
|
||||
/// <returns>The ip address of the client.</returns>
|
||||
public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string ipHeaderName = null)
|
||||
{
|
||||
string forwardedForAddress = null;
|
||||
|
||||
var headerNames = string.IsNullOrWhiteSpace(ipHeaderName)
|
||||
? _defaultIpHeaderNames
|
||||
: new[] { ipHeaderName }.Concat(_defaultIpHeaderNames);
|
||||
foreach (string headerName in headerNames)
|
||||
{
|
||||
if (!httpContext.Request.Headers.ContainsKey(headerName))
|
||||
continue;
|
||||
|
||||
// X-Forwarded-For can contain multiple comma-separated addresses.
|
||||
forwardedForAddress = httpContext.Request.Headers[headerName].ToString()
|
||||
.Split(',', StringSplitOptions.TrimEntries)
|
||||
.First();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(forwardedForAddress) && IPAddress.TryParse(forwardedForAddress, out var remoteAddress))
|
||||
{
|
||||
return remoteAddress.IsIPv4MappedToIPv6
|
||||
? remoteAddress.MapToIPv4()
|
||||
: remoteAddress;
|
||||
}
|
||||
|
||||
return httpContext.Connection.RemoteIpAddress.IsIPv4MappedToIPv6
|
||||
? httpContext.Connection.RemoteIpAddress.MapToIPv4()
|
||||
: httpContext.Connection.RemoteIpAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the request was made locally.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Searches for additional headers in the following order:
|
||||
/// <list type="number">
|
||||
/// <item>Cf-Connecting-Ip</item>
|
||||
/// <item>X-Real-IP</item>
|
||||
/// <item>X-Forwarded-For</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
/// <param name="ipHeaderName">The name of the header to resolve the <see cref="IPAddress"/> when behind a proxy.</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsLocalRequest(this HttpContext httpContext, string ipHeaderName = null)
|
||||
{
|
||||
var remoteIpAddress = httpContext.GetRemoteIpAddress(ipHeaderName);
|
||||
return httpContext.Connection.LocalIpAddress.Equals(remoteIpAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the return url.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static string GetReturnUrl(this HttpContext httpContext)
|
||||
{
|
||||
if (httpContext.Items.ContainsKey("OriginalRequest"))
|
||||
return httpContext.Items["OriginalRequest"].ToString();
|
||||
|
||||
if (httpContext.Request.Query.ContainsKey("ReturnUrl"))
|
||||
return httpContext.Request.Query["ReturnUrl"].ToString();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a session when available.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
public static void ClearSession(this HttpContext httpContext)
|
||||
=> httpContext.Session?.Clear();
|
||||
}
|
||||
}
|
||||
99
src/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs
Normal file
99
src/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal static class LoggerExtensions
|
||||
{
|
||||
// Found here:
|
||||
// https://github.com/dotnet/aspnetcore/blob/a4c45262fb8549bdb4f5e4f76b16f98a795211ae/src/Mvc/Mvc.Core/src/MvcCoreLoggerExtensions.cs
|
||||
|
||||
public static void AttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(44, "AttemptingToBindParameterModel"),
|
||||
$"Attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(13, "AttemptingToBindPropertyModel"),
|
||||
$"Attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(24, "AttemptingToBindModel"),
|
||||
$"Attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FoundNoValueInRequest(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(16, "FoundNoValueForParameterInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding parameter '{modelMetadata.ParameterName}' of typ//(('{bindingContext.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(15, "FoundNoValueForPropertyInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{bindingContext.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(46, "FoundNoValueInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' of type '{bindingContext.ModelType}'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DoneAttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(45, "DoneAttemptingToBindParameterModel"),
|
||||
$"Done attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(14, "DoneAttemptingToBindPropertyModel"),
|
||||
$"Done attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(25, "DoneAttemptingToBindModel"),
|
||||
$"Done attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for the ASP.NET Core application.
|
||||
/// </summary>
|
||||
public static class ModelStateDictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/>
|
||||
/// instance that is associated with the key specified as a <see cref="MemberExpression"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of the model.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <param name="modelState">The <see cref="ModelStateDictionary"/> instance.</param>
|
||||
/// <param name="_">The model. Only used to infer the model type.</param>
|
||||
/// <param name="keyExpression">The <see cref="MemberExpression"/> that specifies the property.</param>
|
||||
/// <param name="errorMessage">The error message to add.</param>
|
||||
/// <exception cref="InvalidOperationException">No member expression provided.</exception>
|
||||
public static void AddModelError<TModel, TProperty>(this ModelStateDictionary modelState, TModel _, Expression<Func<TModel, TProperty>> keyExpression, string errorMessage)
|
||||
{
|
||||
if (modelState is null)
|
||||
throw new ArgumentNullException(nameof(modelState));
|
||||
|
||||
string key = "";
|
||||
var expr = keyExpression.Body as MemberExpression;
|
||||
while (expr != null)
|
||||
{
|
||||
key = expr.Member.Name + (key != "" ? "." + key : "");
|
||||
expr = expr.Expression as MemberExpression;
|
||||
}
|
||||
if (key == "")
|
||||
throw new InvalidOperationException("No member expression provided.");
|
||||
|
||||
modelState.AddModelError(key, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a hosted service that is instanciated only once.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The type of the service to add.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddSingletonHostedService<TService>(this IServiceCollection services)
|
||||
where TService : class, IHostedService
|
||||
{
|
||||
services.AddSingleton<TService>();
|
||||
services.AddHostedService(serviceProvider => serviceProvider.GetRequiredService<TService>());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hosted service that is instanciated only once.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The type of the service to add.</typeparam>
|
||||
/// <typeparam name="TImplementation">The type of the implementation of the service to add.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddSingletonHostedService<TService, TImplementation>(this IServiceCollection services)
|
||||
where TService : class, IHostedService
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
services.AddSingleton<TService, TImplementation>();
|
||||
services.AddHostedService(serviceProvider => serviceProvider.GetRequiredService<TService>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/AMWD.Common.AspNetCore/Extensions/SessionExtensions.cs
Normal file
56
src/AMWD.Common.AspNetCore/Extensions/SessionExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="ISession"/> object.
|
||||
/// </summary>
|
||||
public static class SessionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a strong typed value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public static void SetValue<T>(this ISession session, string key, T value)
|
||||
=> session.SetString(key, JsonConvert.SerializeObject(value));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strong typed value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public static T GetValue<T>(this ISession session, string key)
|
||||
=> session.HasKey(key) ? JsonConvert.DeserializeObject<T>(session.GetString(key)) : default;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strong typed value or the fallback value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="fallback">A fallback value when the key is not present.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public static T GetValue<T>(this ISession session, string key, T fallback)
|
||||
{
|
||||
if (session.HasKey(key))
|
||||
return session.GetValue<T>(key);
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the session has the key available.
|
||||
/// </summary>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns><c>true</c> when the key was found, otherwise <c>false</c>.</returns>
|
||||
public static bool HasKey(this ISession session, string key)
|
||||
=> session.Keys.Contains(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their <see href="https://github.com/dotnet/aspnetcore/issues/6566">issue</see> with other cultures than en-US.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of <see cref="InvariantFloatingPointModelBinder"/>.
|
||||
/// </remarks>
|
||||
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
|
||||
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
|
||||
: IModelBinder
|
||||
{
|
||||
private readonly NumberStyles _supportedNumberStyles = supportedStyles;
|
||||
private readonly ILogger _logger = loggerFactory?.CreateLogger<InvariantFloatingPointModelBinder>();
|
||||
private readonly CultureInfo _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
|
||||
_logger?.AttemptingToBindModel(bindingContext);
|
||||
string modelName = bindingContext.ModelName;
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
|
||||
if (valueProviderResult == ValueProviderResult.None)
|
||||
{
|
||||
_logger?.FoundNoValueInRequest(bindingContext);
|
||||
|
||||
// no entry
|
||||
_logger?.DoneAttemptingToBindModel(bindingContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var modelState = bindingContext.ModelState;
|
||||
modelState.SetModelValue(modelName, valueProviderResult);
|
||||
|
||||
var metadata = bindingContext.ModelMetadata;
|
||||
var type = metadata.UnderlyingOrModelType;
|
||||
try
|
||||
{
|
||||
string value = valueProviderResult.FirstValue;
|
||||
var culture = _cultureInfo ?? valueProviderResult.Culture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
|
||||
model = null;
|
||||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
model = float.Parse(value, _supportedNumberStyles, culture);
|
||||
}
|
||||
else if (type == typeof(double))
|
||||
{
|
||||
model = double.Parse(value, _supportedNumberStyles, culture);
|
||||
}
|
||||
else if (type == typeof(decimal))
|
||||
{
|
||||
model = decimal.Parse(value, _supportedNumberStyles, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// unreachable
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// When converting value, a null model may indicate a failed conversion for an otherwise required
|
||||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
|
||||
// current bindingContext. If not, an error is logged.
|
||||
if (model == null && !metadata.IsReferenceOrNullableType)
|
||||
{
|
||||
modelState.TryAddModelError(
|
||||
modelName,
|
||||
metadata
|
||||
.ModelBindingMessageProvider
|
||||
.ValueMustNotBeNullAccessor(valueProviderResult.ToString())
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
bool isFormatException = exception is FormatException;
|
||||
if (!isFormatException && exception.InnerException != null)
|
||||
{
|
||||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
|
||||
// this code in case a cursory review of the CoreFx code missed something.
|
||||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
|
||||
}
|
||||
|
||||
modelState.TryAddModelError(modelName, exception, metadata);
|
||||
// Conversion failed.
|
||||
}
|
||||
|
||||
_logger?.DoneAttemptingToBindModel(bindingContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IModelBinderProvider"/> for binding <see cref="decimal"/>, <see cref="double"/>,
|
||||
/// <see cref="float"/>, and their <see cref="Nullable{T}"/> wrappers.
|
||||
/// Modified to set <see cref="NumberStyles"/> and <see cref="System.Globalization.CultureInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To use this provider, insert it at the beginning of the providers list:<br/>
|
||||
/// <code>
|
||||
/// services.AddControllersWithViews(options =><br/>
|
||||
/// {<br/>
|
||||
/// options.ModelBinderProviders.Insert(0, new CustomFloatingPointModelBinderProvider());<br/>
|
||||
/// });</code>
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class InvariantFloatingPointModelBinderProvider : IModelBinderProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the supported <see cref="NumberStyles"/> globally.
|
||||
/// Default: <see cref="NumberStyles.Float"/> and <see cref="NumberStyles.AllowThousands"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="SimpleTypeModelBinder"/> uses <see cref="DecimalConverter"/> and similar. Those <see cref="TypeConverter"/>s default to <see cref="NumberStyles.Float"/>.
|
||||
/// </remarks>
|
||||
public static NumberStyles SupportedNumberStyles { get; set; } = NumberStyles.Float | NumberStyles.AllowThousands;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="System.Globalization.CultureInfo"/> to use while parsing globally.
|
||||
/// Default: <see cref="CultureInfo.InvariantCulture"/>.
|
||||
/// </summary>
|
||||
public static CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
|
||||
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||
var modelType = context.Metadata.UnderlyingOrModelType;
|
||||
if (modelType == typeof(decimal) ||
|
||||
modelType == typeof(double) ||
|
||||
modelType == typeof(float))
|
||||
{
|
||||
return new InvariantFloatingPointModelBinder(SupportedNumberStyles, CultureInfo, loggerFactory);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="options" > The monitor for the options instance.</param>
|
||||
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
|
||||
/// <param name="validator">An basic autentication validator implementation.</param>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
#else
|
||||
/// <summary>
|
||||
/// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="options" > The monitor for the options instance.</param>
|
||||
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
|
||||
/// <param name="clock">The <see cref="ISystemClock"/>.</param>
|
||||
/// <param name="validator">An basic autentication validator implementation.</param>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)
|
||||
#endif
|
||||
{
|
||||
private readonly ILogger _logger = logger.CreateLogger<BasicAuthenticationHandler>();
|
||||
private readonly IBasicAuthenticationValidator _validator = validator;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var endpoint = Context.GetEndpoint();
|
||||
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValue))
|
||||
return AuthenticateResult.Fail("Authorization header missing");
|
||||
|
||||
ClaimsPrincipal principal;
|
||||
try
|
||||
{
|
||||
var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue);
|
||||
string plain = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter));
|
||||
|
||||
// See: https://www.rfc-editor.org/rfc/rfc2617, page 6
|
||||
string username = plain.Split(':').First();
|
||||
string password = plain[(username.Length + 1)..];
|
||||
|
||||
var ipAddress = Context.GetRemoteIpAddress();
|
||||
principal = await _validator.ValidateAsync(username, password, ipAddress, Context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Handling the Basic Authentication failed: {ex.Message}");
|
||||
return AuthenticateResult.Fail("Authorization header invalid");
|
||||
}
|
||||
|
||||
if (principal == null)
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a basic authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="BasicAuthenticationMiddleware"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="next">The following delegate in the process chain.</param>
|
||||
/// <param name="validator">A basic authentication validator.</param>
|
||||
public class BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator)
|
||||
{
|
||||
private readonly RequestDelegate _next = next;
|
||||
private readonly IBasicAuthenticationValidator _validator = validator;
|
||||
|
||||
/// <summary>
|
||||
/// The delegate invokation.
|
||||
/// Performs the authentication check.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The corresponding HTTP context.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
if (!httpContext.Request.Headers.TryGetValue("Authorization", out var authHeaderValue))
|
||||
{
|
||||
SetAuthenticateRequest(httpContext, _validator.Realm);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (!httpContext.Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
SetAuthenticateRequest(httpContext, _validator.Realm);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue);
|
||||
#else
|
||||
var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]);
|
||||
#endif
|
||||
byte[] decoded = Convert.FromBase64String(authHeader.Parameter);
|
||||
string plain = Encoding.UTF8.GetString(decoded);
|
||||
|
||||
// See: https://www.rfc-editor.org/rfc/rfc2617, page 6
|
||||
string username = plain.Split(':').First();
|
||||
string password = plain[(username.Length + 1)..];
|
||||
|
||||
var principal = await _validator.ValidateAsync(username, password, httpContext.GetRemoteIpAddress(), httpContext.RequestAborted).ConfigureAwait(false);
|
||||
if (principal == null)
|
||||
{
|
||||
SetAuthenticateRequest(httpContext, _validator.Realm);
|
||||
return;
|
||||
}
|
||||
|
||||
await _next.Invoke(httpContext).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = (ILogger<BasicAuthenticationMiddleware>)httpContext.RequestServices.GetService(typeof(ILogger<BasicAuthenticationMiddleware>));
|
||||
logger?.LogError(ex, $"Falied to execute basic authentication middleware: {ex.InnerException?.Message ?? ex.Message}");
|
||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetAuthenticateRequest(HttpContext httpContext, string realm)
|
||||
{
|
||||
httpContext.Response.Headers.WWWAuthenticate = "Basic";
|
||||
if (!string.IsNullOrWhiteSpace(realm))
|
||||
httpContext.Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm.Replace("\"", "")}\"";
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface representing the validation of a basic authentication.
|
||||
/// </summary>
|
||||
public interface IBasicAuthenticationValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the realm to use when requesting authentication.
|
||||
/// </summary>
|
||||
string Realm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates a username and password for Basic Authentication.
|
||||
/// </summary>
|
||||
/// <param name="username">A username.</param>
|
||||
/// <param name="password">A password.</param>
|
||||
/// <param name="remoteAddress">The remote client's IP address.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>The validated user principal or <c>null</c>.</returns>
|
||||
Task<ClaimsPrincipal> ValidateAsync(string username, string password, IPAddress remoteAddress, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for <see cref="IApplicationBuilder"/> to enable folder protection.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static class ProtectedPathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide protected paths even for static files.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="options">The <see cref="ProtectedPathOptions"/> with path and policy name.</param>
|
||||
public static IApplicationBuilder UseProtectedPath(this IApplicationBuilder app, ProtectedPathOptions options)
|
||||
=> app.UseMiddleware<ProtectedPathMiddleware>(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a check to provide protected paths.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ProtectedPathExtensions"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="next">The following delegate in the process chain.</param>
|
||||
/// <param name="options">The options to configure the middleware.</param>
|
||||
public class ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options)
|
||||
{
|
||||
private readonly RequestDelegate _next = next;
|
||||
private readonly PathString _path = options.Path;
|
||||
private readonly string _policyName = options.PolicyName;
|
||||
|
||||
/// <summary>
|
||||
/// The delegate invokation.
|
||||
/// Performs the protection check.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The corresponding HTTP context.</param>
|
||||
/// <param name="authorizationService">The <see cref="IAuthorizationService"/>.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public async Task InvokeAsync(HttpContext httpContext, IAuthorizationService authorizationService)
|
||||
{
|
||||
if (httpContext.Request.Path.StartsWithSegments(_path))
|
||||
{
|
||||
var result = await authorizationService.AuthorizeAsync(httpContext.User, null, _policyName).ConfigureAwait(false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await httpContext.ChallengeAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _next.Invoke(httpContext).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Options to define which folder should be protected.
|
||||
/// </summary>
|
||||
public class ProtectedPathOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the protected folder.
|
||||
/// </summary>
|
||||
public PathString Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy name to use.
|
||||
/// </summary>
|
||||
public string PolicyName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelpers
|
||||
{
|
||||
// Source: https://stackoverflow.com/a/42385059
|
||||
|
||||
/// <summary>
|
||||
/// A tag helper that adds a CSS class attribute based on a condition.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
[HtmlTargetElement(Attributes = ClassPrefix + "*")]
|
||||
public class ConditionClassTagHelper : TagHelper
|
||||
{
|
||||
private const string ClassPrefix = "condition-class-";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unconditional CSS class attribute value of the element.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("class")]
|
||||
public string CssClass { get; set; }
|
||||
|
||||
private IDictionary<string, bool> _classValues;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a dictionary containing all conditional class names and a boolean condition
|
||||
/// value indicating whether the class should be added to the element.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("", DictionaryAttributePrefix = ClassPrefix)]
|
||||
public IDictionary<string, bool> ClassValues
|
||||
{
|
||||
get
|
||||
{
|
||||
return _classValues ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
set
|
||||
{
|
||||
_classValues = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously executes the <see cref="T:Microsoft.AspNetCore.Razor.TagHelpers.TagHelper"/>
|
||||
/// with the given <paramref name="context"/> and <paramref name="output"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains information associated with the current HTML tag.</param>
|
||||
/// <param name="output">A stateful HTML element used to generate an HTML tag.</param>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var items = _classValues.Where(e => e.Value).Select(e => e.Key).ToList();
|
||||
if (!string.IsNullOrEmpty(CssClass))
|
||||
items.Insert(0, CssClass);
|
||||
|
||||
if (items.Any())
|
||||
{
|
||||
string classes = string.Join(" ", [.. items]);
|
||||
output.Attributes.Add("class", classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs
Normal file
39
src/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A tag helper to create a obfuscated email link.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]
|
||||
public class EmailTagHelper : TagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The e-mail address.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-address")]
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Processes the element.
|
||||
/// </summary>
|
||||
/// <param name="context">The tag helper context.</param>
|
||||
/// <param name="output">The tag helper output.</param>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"mailto:{Address}"));
|
||||
string reversed = new(Address.Reverse().ToArray());
|
||||
|
||||
output.TagName = "a";
|
||||
output.TagMode = TagMode.StartTagAndEndTag;
|
||||
output.Attributes.SetAttribute("href", new HtmlString($"javascript:window.location.href=atob('{base64}')"));
|
||||
output.Attributes.SetAttribute("style", "unicode-bidi: bidi-override; direction: rtl;");
|
||||
output.Attributes.RemoveAll("asp-address");
|
||||
output.Content.SetContent(reversed);
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs
Normal file
158
src/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A tag helper to dynamically create integrity checks for linked sources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="IntegrityHashTagHelper"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="env">The web host environment.</param>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
[HtmlTargetElement("link")]
|
||||
[HtmlTargetElement("script")]
|
||||
public class IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration)
|
||||
: TagHelper
|
||||
{
|
||||
private readonly IWebHostEnvironment _env = env;
|
||||
private readonly string _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the integrity should be calculated.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-integrity")]
|
||||
public bool IsIntegrityEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hash strength to use.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-integrity-strength")]
|
||||
public int IntegrityStrength { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (context.AllAttributes.Where(a => a.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)).Any())
|
||||
return;
|
||||
|
||||
if (!IsIntegrityEnabled)
|
||||
return;
|
||||
|
||||
string source = null;
|
||||
switch (context.TagName.ToLower())
|
||||
{
|
||||
case "link":
|
||||
var rel = context.AllAttributes.Where(a => a.Name.ToLower() == "rel").FirstOrDefault();
|
||||
if (rel == null || rel.Value.ToString().ToLower() == "stylesheet")
|
||||
{
|
||||
var href = context.AllAttributes.Where(a => a.Name.ToLower() == "href").FirstOrDefault();
|
||||
source = href?.Value?.ToString().Trim();
|
||||
}
|
||||
break;
|
||||
case "script":
|
||||
var src = context.AllAttributes.Where(a => a.Name.ToLower() == "src").FirstOrDefault();
|
||||
source = src?.Value?.ToString().Trim();
|
||||
break;
|
||||
}
|
||||
|
||||
// no source given, no hash to calculate.
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
return;
|
||||
|
||||
byte[] fileBytes = null;
|
||||
if (source.StartsWith("http") || source.StartsWith("//"))
|
||||
{
|
||||
if (source.StartsWith("//"))
|
||||
source = $"http:{source}";
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_hostUrl))
|
||||
client.DefaultRequestHeaders.Referrer = new Uri(_hostUrl);
|
||||
|
||||
var response = await client.GetAsync(source).ConfigureAwait(false);
|
||||
fileBytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (source.StartsWith("~"))
|
||||
source = source[1..];
|
||||
|
||||
if (source.StartsWith("/"))
|
||||
source = source[1..];
|
||||
|
||||
if (source.Contains('?'))
|
||||
source = source[..source.IndexOf("?")];
|
||||
|
||||
try
|
||||
{
|
||||
string path = Path.Combine(_env.WebRootPath, source);
|
||||
fileBytes = await File.ReadAllBytesAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string type;
|
||||
byte[] hashBytes = [];
|
||||
switch (IntegrityStrength)
|
||||
{
|
||||
case 512:
|
||||
type = "sha512";
|
||||
using (var sha = SHA512.Create())
|
||||
{
|
||||
hashBytes = sha.ComputeHash(fileBytes);
|
||||
}
|
||||
break;
|
||||
case 384:
|
||||
type = "sha384";
|
||||
using (var sha = SHA384.Create())
|
||||
{
|
||||
hashBytes = sha.ComputeHash(fileBytes);
|
||||
}
|
||||
break;
|
||||
default: // 256
|
||||
type = "sha256";
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
hashBytes = sha.ComputeHash(fileBytes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
string hash = Convert.ToBase64String(hashBytes);
|
||||
|
||||
output.Attributes.RemoveAll("integrity");
|
||||
output.Attributes.RemoveAll("crossorigin");
|
||||
|
||||
output.Attributes.Add(new TagHelperAttribute("integrity", new HtmlString($"{type}-{hash}")));
|
||||
output.Attributes.Add(new TagHelperAttribute("crossorigin", new HtmlString("anonymous")));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
// ensure leaving context to prevent a deadlock.
|
||||
var task = Task.Run(() => ProcessAsync(context, output));
|
||||
task.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs
Normal file
185
src/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds additional behavior to the modelbinding for numeric properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="generator">The HTML generator.</param>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
[HtmlTargetElement("input", Attributes = "asp-for")]
|
||||
public class NumberInputTagHelper(IHtmlGenerator generator)
|
||||
: InputTagHelper(generator)
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
base.Process(context, output);
|
||||
|
||||
var types = new[] {
|
||||
typeof(byte), typeof(sbyte),
|
||||
typeof(ushort), typeof(short),
|
||||
typeof(uint), typeof(int),
|
||||
typeof(ulong), typeof(long),
|
||||
typeof(float),
|
||||
typeof(double),
|
||||
typeof(decimal)
|
||||
};
|
||||
|
||||
var typeAttributes = output.Attributes
|
||||
.Where(a => a.Name == "type")
|
||||
.ToList();
|
||||
string typeAttributeValue = typeAttributes.First().Value as string;
|
||||
|
||||
Type modelType = For.ModelExplorer.ModelType;
|
||||
Type nullableType = Nullable.GetUnderlyingType(modelType);
|
||||
|
||||
// the type itself or its nullable wrapper matching and
|
||||
// the type attribute is number or there is only one type attribute
|
||||
// IMPORTANT TO KNOW: if the type attribute is set in the view, there are two attributes with same value.
|
||||
if ((types.Contains(modelType) || types.Contains(nullableType)) && (typeAttributeValue == "number" || typeAttributes.Count == 1))
|
||||
{
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
string min = "";
|
||||
string max = "";
|
||||
string step = "";
|
||||
string value = "";
|
||||
|
||||
if (modelType == typeof(byte) || nullableType == typeof(byte))
|
||||
{
|
||||
min = byte.MinValue.ToString(culture);
|
||||
max = byte.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
byte val = (byte)For.Model;
|
||||
value = value.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(sbyte) || nullableType == typeof(sbyte))
|
||||
{
|
||||
min = sbyte.MinValue.ToString(culture);
|
||||
max = sbyte.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
sbyte val = (sbyte)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(ushort) || nullableType == typeof(ushort))
|
||||
{
|
||||
min = ushort.MinValue.ToString(culture);
|
||||
max = ushort.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
ushort val = (ushort)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(short) || nullableType == typeof(short))
|
||||
{
|
||||
min = short.MinValue.ToString(culture);
|
||||
max = short.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
short val = (short)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(uint) || nullableType == typeof(uint))
|
||||
{
|
||||
min = uint.MinValue.ToString(culture);
|
||||
max = uint.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
uint val = (uint)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(int) || nullableType == typeof(int))
|
||||
{
|
||||
min = int.MinValue.ToString(culture);
|
||||
max = int.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
int val = (int)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(ulong) || nullableType == typeof(ulong))
|
||||
{
|
||||
min = ulong.MinValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
ulong val = (ulong)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(long) || nullableType == typeof(long))
|
||||
{
|
||||
if (For.Model != null)
|
||||
{
|
||||
long val = (long)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(float) || nullableType == typeof(float))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
float val = (float)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(double) || nullableType == typeof(double))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
double val = (double)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(decimal) || nullableType == typeof(decimal))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
decimal val = (decimal)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("type", "number"));
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("value", value));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(min) && !output.Attributes.ContainsName("min"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("min", min));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(max) && !output.Attributes.ContainsName("max"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("max", max));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(step) && !output.Attributes.ContainsName("step"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("step", step));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs
Normal file
55
src/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides helpers for webpages.
|
||||
/// </summary>
|
||||
public static class HtmlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a color is considered as dark.
|
||||
/// </summary>
|
||||
/// <param name="color">The color (hex or rgb - as defined in CSS)</param>
|
||||
/// <returns><c>true</c> when the color is dark otherwise <c>false</c>.</returns>
|
||||
public static bool IsDarkColor(string color)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(color))
|
||||
throw new ArgumentNullException(nameof(color));
|
||||
|
||||
int r, g, b;
|
||||
|
||||
var rgbMatch = Regex.Match(color, @"^rgba?\(([0-9]+), ?([0-9]+), ?([0-9]+)");
|
||||
var hexMatchFull = Regex.Match(color, @"^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$");
|
||||
var hexMatchLite = Regex.Match(color, @"^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$");
|
||||
|
||||
if (rgbMatch.Success)
|
||||
{
|
||||
r = Convert.ToInt32(rgbMatch.Groups[1].Value, 10);
|
||||
g = Convert.ToInt32(rgbMatch.Groups[2].Value, 10);
|
||||
b = Convert.ToInt32(rgbMatch.Groups[3].Value, 10);
|
||||
}
|
||||
else if (hexMatchFull.Success)
|
||||
{
|
||||
r = Convert.ToInt32(hexMatchFull.Groups[1].Value, 16);
|
||||
g = Convert.ToInt32(hexMatchFull.Groups[2].Value, 16);
|
||||
b = Convert.ToInt32(hexMatchFull.Groups[3].Value, 16);
|
||||
}
|
||||
else if (hexMatchLite.Success)
|
||||
{
|
||||
r = Convert.ToInt32(new string(hexMatchLite.Groups[1].Value.First(), 2), 16);
|
||||
g = Convert.ToInt32(new string(hexMatchLite.Groups[2].Value.First(), 2), 16);
|
||||
b = Convert.ToInt32(new string(hexMatchLite.Groups[3].Value.First(), 2), 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Unknown color value '{color}'");
|
||||
}
|
||||
|
||||
double luminance = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
|
||||
return luminance <= 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/AMWD.Common.AspNetCore/Utilities/PasswordHelper.cs
Normal file
52
src/AMWD.Common.AspNetCore/Utilities/PasswordHelper.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace Microsoft.AspNetCore.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides password hashing and verification methods.
|
||||
/// </summary>
|
||||
public static class PasswordHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Hashes a password.
|
||||
/// </summary>
|
||||
/// <param name="plainPassword">The plain password.</param>
|
||||
/// <returns></returns>
|
||||
public static string HashPassword(string plainPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(plainPassword))
|
||||
return plainPassword?.Trim();
|
||||
|
||||
var ph = new PasswordHasher<object>();
|
||||
return ph.HashPassword(null, plainPassword.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a password with a hashed version.
|
||||
/// </summary>
|
||||
/// <param name="plainPassword">The plain password.</param>
|
||||
/// <param name="hashedPassword">The password hash.</param>
|
||||
/// <param name="rehashNeeded">A value indicating whether the password needs a rehash.</param>
|
||||
/// <returns></returns>
|
||||
public static bool VerifyPassword(string plainPassword, string hashedPassword, out bool rehashNeeded)
|
||||
{
|
||||
rehashNeeded = false;
|
||||
if (string.IsNullOrWhiteSpace(plainPassword) || string.IsNullOrWhiteSpace(hashedPassword))
|
||||
return false;
|
||||
|
||||
var ph = new PasswordHasher<object>();
|
||||
var result = ph.VerifyHashedPassword(null, hashedPassword, plainPassword);
|
||||
switch (result)
|
||||
{
|
||||
case PasswordVerificationResult.Success:
|
||||
return true;
|
||||
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
rehashNeeded = true;
|
||||
return true;
|
||||
|
||||
case PasswordVerificationResult.Failed:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user