diff --git a/src/Electricity.Api/AllowedAccessMiddleware.cs b/src/Electricity.Api/AllowedAccessMiddleware.cs new file mode 100644 index 0000000..90f0146 --- /dev/null +++ b/src/Electricity.Api/AllowedAccessMiddleware.cs @@ -0,0 +1,72 @@ +using System.Net; +using Electricity.Api.Options; + +namespace Electricity.Api; + +internal class AllowedAccessMiddleware( + RequestDelegate next, + ILogger logger, + AllowedAccessOptions options +) +{ + private readonly RequestDelegate next = next; + private readonly ILogger logger = logger; + private readonly AllowedAccessOptions options = options; + + public async Task Invoke(HttpContext context) + { + if ( + !context.Request.Headers.TryGetValue( + options.HttpIpAddressHeaderName, + out var headerValue + ) + ) + { + logger.LogDebug( + "Rejecting request missing the expected HTTP header {HeaderName}", + options.HttpIpAddressHeaderName + ); + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } + + if (headerValue.Count != 1) + { + logger.LogDebug( + "Rejecting request with malformed HTTP header {HeaderName}", + options.HttpIpAddressHeaderName + ); + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } + + if (string.IsNullOrWhiteSpace(headerValue[0])) + { + logger.LogDebug( + "Rejecting request with malformed value {HeaderValue} in HTTP header {HeaderName}", + headerValue[0], + options.HttpIpAddressHeaderName + ); + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } + + if (!options.HttpIpAddressHeaderValues.Contains(headerValue[0])) + { + logger.LogInformation( + "Rejecting request with disallowed header value {HeaderValue} in HTTP header {HeaderName}", + headerValue[0], + options.HttpIpAddressHeaderName + ); + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } + + logger.LogDebug( + "Accepting request with allowed value {HeaderValue} in HTTP header {HeaderName}", + headerValue[0], + options.HttpIpAddressHeaderName + ); + await next.Invoke(context); + } +} diff --git a/src/Electricity.Api/Options/AllowedAccessOptions.cs b/src/Electricity.Api/Options/AllowedAccessOptions.cs new file mode 100644 index 0000000..692cae8 --- /dev/null +++ b/src/Electricity.Api/Options/AllowedAccessOptions.cs @@ -0,0 +1,47 @@ +namespace Electricity.Api.Options; + +internal class AllowedAccessOptions(IConfiguration configuration) +{ + public string HttpIpAddressHeaderName { get; } = ResolveHttpIpAddressHeaderName(configuration); + public string[] HttpIpAddressHeaderValues { get; } = + ResolveHttpIpAddressHeaderValues(configuration); + + private static string ResolveHttpIpAddressHeaderName(IConfiguration configuration) + { + const string configurationKey = "AllowedAccess:HttpIpAddressHeaderName"; + var value = configuration["AllowedAccess:HttpIpAddressHeaderName"]; + if (string.IsNullOrWhiteSpace(value)) + { + throw new NotSupportedException($"The app setting {configurationKey} cannot be empty"); + } + + return value; + } + + private static string[] ResolveHttpIpAddressHeaderValues(IConfiguration configuration) + { + const string configurationKey = "AllowedAccess:HttpIpAddressHeaderValues"; + var values = configuration.GetSection(configurationKey).Get>(); + if (values == null || values.Count == 0) + { + throw new NotSupportedException( + $"The app setting {configurationKey} must be an string array with at least 1 value" + ); + } + + List validatedValues = []; + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new NotSupportedException( + $"The value \"{value}\" from app setting {configurationKey} cannot be empty" + ); + } + + validatedValues.Add(value); + } + + return validatedValues.ToArray(); + } +} diff --git a/src/Electricity.Api/Program.cs b/src/Electricity.Api/Program.cs index 500a6d0..78b0e9d 100644 --- a/src/Electricity.Api/Program.cs +++ b/src/Electricity.Api/Program.cs @@ -1,5 +1,6 @@ using System.CommandLine; using Electricity.Api; +using Electricity.Api.Options; using Electricity.Api.Services; using Microsoft.EntityFrameworkCore; @@ -67,6 +68,11 @@ internal class Program config.JsonSerializerOptions.WriteIndented = true; }); builder.Services.AddSwaggerGen(); + + builder.Services.AddSingleton(sp => new AllowedAccessOptions( + sp.GetRequiredService() + )); + builder.Services.AddScoped(); var app = builder.Build(); @@ -79,6 +85,9 @@ internal class Program app.UseCors("default"); } + app.UseMiddleware( + app.Services.GetRequiredService() + ); app.MapControllers(); app.Run(); diff --git a/src/Electricity.Api/appsettings.Development.json b/src/Electricity.Api/appsettings.Development.json index ff66ba6..fb8cd2d 100644 --- a/src/Electricity.Api/appsettings.Development.json +++ b/src/Electricity.Api/appsettings.Development.json @@ -1,7 +1,11 @@ { + "AllowedAccess": { + "HttpIpAddressHeaderName": "Host", + "HttpIpAddressHeaderValues": ["localhost:7290"] + }, "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Trace", "Microsoft.AspNetCore": "Warning" } } diff --git a/src/Electricity.Api/appsettings.json b/src/Electricity.Api/appsettings.json index 3865fae..655a2c6 100644 --- a/src/Electricity.Api/appsettings.json +++ b/src/Electricity.Api/appsettings.json @@ -1,5 +1,9 @@ { "AllowedHosts": "*", + "AllowedAccess": { + "HttpIpAddressHeaderName": "X-Real-IP", + "HttpIpAddressHeaderValues": ["213.233.220.64", "86.83.136.215"] + }, "Logging": { "LogLevel": { "Default": "Information",