1
0

Refactoring

This commit is contained in:
2021-10-22 21:05:37 +02:00
commit ca9de13c9e
43 changed files with 5145 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<AssemblyName>AMWD.Common.AspNetCore</AssemblyName>
<RootNamespace>AMWD.Common.AspNetCore</RootNamespace>
<NrtRevisionFormat>{semvertag:master:+chash}{!:-dirty}</NrtRevisionFormat>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<BuildInParallel>false</BuildInParallel>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageId>AMWD.Common.AspNetCore</PackageId>
<Product>AM.WD Common Library for ASP.NET Core</Product>
<Description>Library with classes and extensions used frequently on AM.WD projects.</Description>
<Company>AM.WD</Company>
<Authors>Andreas Müller</Authors>
<Copyright>© {copyright:2020-} AM.WD</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="2.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="2.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
<PackageReference Include="Unclassified.DeepConvert" Version="1.3.0" />
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.Filters
{
/// <summary>
/// Custom filter attribute to use Google's reCaptcha (v3).
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
/// </summary>
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 readonly string privateKey;
/// <summary>
/// Initializes a new instance of the <see cref="GoogleReCaptchaAttribute"/> class.
/// </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>
/// <param name="configuration">The application configuration.</param>
public GoogleReCaptchaAttribute(IConfiguration configuration)
{
privateKey = configuration.GetValue<string>("Google:ReCaptcha: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)
{
await DoValidation(context);
await base.OnActionExecutionAsync(context, next);
}
private async Task DoValidation(ActionExecutingContext context)
{
if (string.IsNullOrWhiteSpace(privateKey))
return;
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);
}
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));
string json = await response.Content.ReadAsStringAsync();
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; }
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extensions for the <see cref="IApplicationBuilder"/>.
/// </summary>
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 is defined via<br/>
/// - <c>ASPNETCORE_APPL_PATH</c> environment variable (preferred)<br/>
/// - <c>AspNetCore_Appl_Path</c> in the settings file<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 no <paramref name="address"/> oder <paramref name="network"/> is set, the default IPv4 private subnets are configured:<br/>
/// - <c>10.0.0.0/8</c><br/>
/// - <c>172.16.0.0/12</c><br/>
/// - <c>192.168.0.0/16</c>
/// </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>
public static void UseProxyHosting(this IApplicationBuilder app, IPNetwork network = null, IPAddress address = null)
{
string path = Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH");
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)
{
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));
}
if (network != null)
options.KnownNetworks.Add(network);
if (address != null)
options.KnownProxies.Add(address);
app.UseForwardedHeaders(options);
}
}
}

View File

@@ -0,0 +1,80 @@
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>
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;
}
}
}

View File

@@ -0,0 +1,63 @@
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
{
/// <summary>
/// Retrieves the antiforgery token.
/// </summary>
/// <param name="httpContext">The web context.</param>
/// <returns>Name and value of the token.</returns>
public static (string Name, string Value) GetAntiforgeryToken(this HttpContext httpContext)
{
var af = httpContext.RequestServices.GetService<IAntiforgery>();
var set = af?.GetAndStoreTokens(httpContext);
return (Name: set?.FormFieldName, Value: set?.RequestToken);
}
/// <summary>
/// Returns the remote ip address.
/// </summary>
/// <param name="httpContext">The web context.</param>
/// <param name="headerName">The name of the header to resolve the <see cref="IPAddress"/> when behind a proxy (Default: X-Forwarded-For).</param>
/// <returns>The ip address of the client.</returns>
public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string headerName = "X-Forwarded-For")
{
var remote = httpContext.Connection.RemoteIpAddress;
string forwardedHeader = httpContext.Request.Headers[headerName].ToString();
if (!string.IsNullOrWhiteSpace(forwardedHeader) && IPAddress.TryParse(forwardedHeader, out var forwarded))
return forwarded;
return remote;
}
/// <summary>
/// Tries to retrieve the return url.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public static string GetReturnUrl(this HttpContext httpContext)
{
string url = httpContext.Items["OriginalRequest"]?.ToString();
if (string.IsNullOrWhiteSpace(url))
url = httpContext.Request.Query["ReturnUrl"].ToString();
return url;
}
/// <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();
}
}

View File

@@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.Extensions.Logging
{
/// <summary>
/// Extensions for the <see cref="ILogger"/>.
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,42 @@
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="model">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>
#pragma warning disable IDE0060 // remove unused parameters
public static void AddModelError<TModel, TProperty>(this ModelStateDictionary modelState, TModel model, Expression<Func<TModel, TProperty>> keyExpression, string errorMessage)
#pragma warning restore IDE0060 // remove unused parameters
{
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);
}
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Hosting;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for the <see cref="IServiceCollection"/>.
/// </summary>
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.AddSingleton<IHostedService, BackgroundServiceStarter<TService>>();
return services;
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// Implements a basic authentication.
/// </summary>
public class BasicAuthMiddleware
{
private readonly RequestDelegate next;
private readonly string realm;
private readonly Func<string, string, bool> userPasswordAuth;
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthMiddleware"/> class.
/// </summary>
/// <param name="next">The following delegate in the process chain.</param>
/// <param name="realm">The realm to display when requesting for credentials.</param>
/// <param name="userPasswordAuth">The function <c>(user, passwd) => result</c> to validate username and password.</param>
public BasicAuthMiddleware(RequestDelegate next, string realm, Func<string, string, bool> userPasswordAuth)
{
this.next = next;
this.realm = realm;
this.userPasswordAuth = userPasswordAuth;
}
/// <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 (httpContext.Request.Headers.TryGetValue("Authorization", out var authHeader)
&& ((string)authHeader).StartsWith("Basic "))
{
string encoded = ((string)authHeader).Split(' ', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "";
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
string[] parts = decoded.Split(':');
if (parts.Length >= 2)
{
string username = parts[0].Trim().ToLower();
string password = parts[1].Trim();
if (userPasswordAuth(username, password))
{
await next.Invoke(httpContext);
return;
}
}
}
httpContext.Response.Headers["WWW-Authenticate"] = "Basic";
if (!string.IsNullOrWhiteSpace(realm))
{
httpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm}\"";
}
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}
}

View File

@@ -0,0 +1,119 @@
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>
public class CustomFloatingPointModelBinder : IModelBinder
{
private readonly NumberStyles supportedNumberStyles;
private readonly ILogger logger;
private readonly CultureInfo cultureInfo;
/// <summary>
/// Initializes a new instance of <see cref="CustomFloatingPointModelBinder"/>.
/// </summary>
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public CustomFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
{
this.cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
supportedNumberStyles = supportedStyles;
logger = loggerFactory?.CreateLogger<CustomFloatingPointModelBinder>();
}
/// <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;
}
}
}

View File

@@ -0,0 +1,58 @@
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>
public class CustomFloatingPointModelBinderProvider : 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 CustomFloatingPointModelBinder(SupportedNumberStyles, CultureInfo, loggerFactory);
}
return null;
}
}
}

View File

@@ -0,0 +1,61 @@
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>
[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.ToArray());
output.Attributes.Add("class", classes);
}
}
}
}

View File

@@ -0,0 +1,38 @@
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>
[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);
}
}
}

View File

@@ -0,0 +1,186 @@
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>
[HtmlTargetElement("input", Attributes = "asp-for")]
public class NumberInputTagHelper : InputTagHelper
{
/// <summary>
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
/// </summary>
/// <param name="generator">The HTML generator.</param>
public NumberInputTagHelper(IHtmlGenerator generator)
: base(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));
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Wrapper class to start a background service.
/// </summary>
/// <typeparam name="TService">The service type.</typeparam>
public class BackgroundServiceStarter<TService> : IHostedService
where TService : class, IHostedService
{
private readonly TService service;
/// <summary>
/// Initializes an new instance of the <see cref="BackgroundServiceStarter{TService}"/> class.
/// </summary>
/// <param name="backgroundService">The service to work in background.</param>
public BackgroundServiceStarter(TService backgroundService)
{
service = backgroundService;
}
/// <summary>
/// Starts the service.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task StartAsync(CancellationToken cancellationToken)
{
return service.StartAsync(cancellationToken);
}
/// <summary>
/// Stops the service.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task StopAsync(CancellationToken cancellationToken)
{
return service.StopAsync(cancellationToken);
}
}
}

View 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;
}
}
}
}