diff --git a/Teknik.sln b/Teknik.sln index 425d058..a25b0be 100644 --- a/Teknik.sln +++ b/Teknik.sln @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServer", "IdentityS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContentScanningService", "ContentScanningService\ContentScanningService.csproj", "{491FE626-ABC8-4D00-8C7F-0849C357201A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebCommon", "WebCommon\WebCommon.csproj", "{32E85A7F-871A-437C-9BA3-00499AAB442C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,12 @@ Global {491FE626-ABC8-4D00-8C7F-0849C357201A}.Release|Any CPU.Build.0 = Release|Any CPU {491FE626-ABC8-4D00-8C7F-0849C357201A}.Test|Any CPU.ActiveCfg = Debug|Any CPU {491FE626-ABC8-4D00-8C7F-0849C357201A}.Test|Any CPU.Build.0 = Debug|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Release|Any CPU.Build.0 = Release|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Test|Any CPU.ActiveCfg = Debug|Any CPU + {32E85A7F-871A-437C-9BA3-00499AAB442C}.Test|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WebCommon/IErrorController.cs b/WebCommon/IErrorController.cs new file mode 100644 index 0000000..7871b5c --- /dev/null +++ b/WebCommon/IErrorController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Teknik.WebCommon +{ + public interface IErrorController + { + public ControllerContext ControllerContext { get; set; } + + public IActionResult HttpError(int statusCode, Exception exception); + } +} diff --git a/WebCommon/Middleware/BlacklistMiddleware.cs b/WebCommon/Middleware/BlacklistMiddleware.cs new file mode 100644 index 0000000..e75629d --- /dev/null +++ b/WebCommon/Middleware/BlacklistMiddleware.cs @@ -0,0 +1,128 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Teknik.Configuration; + +namespace Teknik.WebCommon.Middleware +{ + public class BlacklistMiddleware + { + private readonly RequestDelegate _next; + private readonly IMemoryCache _cache; + + public BlacklistMiddleware(RequestDelegate next, IMemoryCache cache) + { + _next = next; + _cache = cache; + } + + public async Task Invoke(HttpContext context, Config config) + { + // Beggining of Request + bool blocked = false; + string blockReason = string.Empty; + + #region Detect Blacklisted IPs + if (!blocked) + { + string IPAddr = context.Request.HttpContext.Connection.RemoteIpAddress.ToString(); + if (!string.IsNullOrEmpty(IPAddr)) + { + StringDictionary badIPs = GetFileData(context, "BlockedIPs", config.IPBlacklistFile); + + blocked |= (badIPs != null && badIPs.ContainsKey(IPAddr)); + blockReason = $"This IP address ({IPAddr}) has been blacklisted. If you feel this is in error, please contact support@teknik.io for assistance."; + } + } + #endregion + + #region Detect Blacklisted Referrers + if (!blocked) + { + string referrer = context.Request.Headers["Referer"].ToString(); + string referrerHost = referrer; + try + { + var referrerUri = new Uri(referrer); + referrerHost = referrerUri.Host; + } catch + { } + if (!string.IsNullOrEmpty(referrer)) + { + StringDictionary badReferrers = GetFileData(context, "BlockedReferrers", config.ReferrerBlacklistFile); + + if (badReferrers != null) + { + blocked |= badReferrers.ContainsKey(referrer) || badReferrers.ContainsKey(referrerHost); + blockReason = $"This referrer ({referrer}) has been blacklisted. If you feel this is in error, please contact support@teknik.io for assistance."; + } + } + } + #endregion + + if (blocked) + { + // Clear the response + context.Response.Clear(); + + string jsonResult = JsonConvert.SerializeObject(new { error = new { type = "Blacklist", message = blockReason } }); + await context.Response.WriteAsync(jsonResult); + return; + } + + await _next.Invoke(context); + + // End of request + } + + public StringDictionary GetFileData(HttpContext context, string key, string filePath) + { + StringDictionary data; + if (!_cache.TryGetValue(key, out data)) + { + data = GetFileLines(filePath); + _cache.Set(key, data); + } + + return data; + } + + public StringDictionary GetFileLines(string configPath) + { + StringDictionary retval = new StringDictionary(); + if (File.Exists(configPath)) + { + using (StreamReader sr = new StreamReader(configPath)) + { + String line; + while ((line = sr.ReadLine()) != null) + { + line = line.Trim(); + if (line.Length != 0) + { + retval.Add(line, null); + } + } + } + } + + return retval; + } + } + + public static class BlacklistMiddlewareExtensions + { + public static IApplicationBuilder UseBlacklist(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/Middleware/CORSMiddleware.cs b/WebCommon/Middleware/CORSMiddleware.cs new file mode 100644 index 0000000..5ab9c30 --- /dev/null +++ b/WebCommon/Middleware/CORSMiddleware.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Teknik.Configuration; +using Teknik.Utilities; +using Teknik.Utilities.Routing; + +namespace Teknik.WebCommon.Middleware +{ + // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project + public class CORSMiddleware + { + private readonly RequestDelegate _next; + + public CORSMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task InvokeAsync(HttpContext httpContext, Config config) + { + // Allow this domain, or everything if local + string origin = (httpContext.Request.IsLocal()) ? "*" : httpContext.Request.Headers["Origin"].ToString(); + + // Is the referrer set to the CDN and we are using a CDN? + if (config.UseCdn && !string.IsNullOrEmpty(config.CdnHost)) + { + try + { + string host = httpContext.Request.Headers["Host"]; + Uri uri = new Uri(config.CdnHost); + if (host == uri.Host) + origin = host; + } + catch { } + } + + string domain = (string.IsNullOrEmpty(origin)) ? string.Empty : origin.GetDomain(); + + if (string.IsNullOrEmpty(origin)) + { + string host = httpContext.Request.Headers["Host"]; + string sub = host.GetSubdomain(); + origin = (string.IsNullOrEmpty(sub)) ? config.Host : sub + "." + config.Host; + } + else + { + if (domain != config.Host) + { + string sub = origin.GetSubdomain(); + origin = (string.IsNullOrEmpty(sub)) ? config.Host : sub + "." + config.Host; + } + } + + httpContext.Response.Headers.Append("Access-Control-Allow-Origin", origin); + httpContext.Response.Headers.Append("Vary", "Origin"); + + return _next(httpContext); + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class CORSMiddlewareExtensions + { + public static IApplicationBuilder UseCORS(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/Middleware/ErrorHandlerMiddleware.cs b/WebCommon/Middleware/ErrorHandlerMiddleware.cs new file mode 100644 index 0000000..ee4296a --- /dev/null +++ b/WebCommon/Middleware/ErrorHandlerMiddleware.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using System; +using System.Threading.Tasks; +using Teknik.WebCommon; + +namespace Teknik.Middleware +{ + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + + public ErrorHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, IErrorController errorController) + { + var statusCodeFeature = new StatusCodePagesFeature(); + httpContext.Features.Set(statusCodeFeature); + + Exception exception = null; + try + { + await _next(httpContext); + } + catch (Exception ex) + { + exception = ex; + } + + if (!statusCodeFeature.Enabled) + { + // Check if the feature is still available because other middleware (such as a web API written in MVC) could + // have disabled the feature to prevent HTML status code responses from showing up to an API client. + return; + } + + // Do nothing if a response body has already been provided or not 404 response + if (httpContext.Response.HasStarted) + { + return; + } + + // Detect if there is a response code or exception occured + if ((httpContext.Response.StatusCode >= 400 && httpContext.Response.StatusCode <= 600) || exception != null) + { + var routeData = httpContext.GetRouteData() ?? new RouteData(); + + var context = new ControllerContext(); + context.HttpContext = httpContext; + context.RouteData = routeData; + context.ActionDescriptor = new Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor(); + + errorController.ControllerContext = context; + + await errorController.HttpError(httpContext.Response.StatusCode, exception).ExecuteResultAsync(context); + } + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class SetupErrorHandlerMiddlewareExtensions + { + public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/Middleware/PerformanceMonitorMiddleware.cs b/WebCommon/Middleware/PerformanceMonitorMiddleware.cs new file mode 100644 index 0000000..eccfaa9 --- /dev/null +++ b/WebCommon/Middleware/PerformanceMonitorMiddleware.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Teknik.Configuration; +using Teknik.Utilities; + +namespace Teknik.WebCommon.Middleware +{ + // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project + public class PerformanceMonitorMiddleware + { + private readonly RequestDelegate _next; + + public PerformanceMonitorMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, Config config) + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + + httpContext.Response.OnStarting(state => + { + var context = (HttpContext)state; + + timer.Stop(); + + double ms = (double)timer.ElapsedMilliseconds; + string result = string.Format("{0:F0}", ms); + + if (!httpContext.Response.Headers.IsReadOnly) + httpContext.Response.Headers.Add("GenerationTime", result); + + return Task.CompletedTask; + }, httpContext); + + await _next(httpContext); + + // Don't interfere with non-HTML responses + if (httpContext.Response.ContentType != null && httpContext.Response.ContentType.StartsWith("text/html") && httpContext.Response.StatusCode == 200 && !httpContext.Request.IsAjaxRequest()) + { + double ms = (double)timer.ElapsedMilliseconds; + string result = string.Format("{0:F0}", ms); + + await httpContext.Response.WriteAsync( + ""); + } + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class PerformanceMonitorMiddlewareExtensions + { + public static IApplicationBuilder UsePerformanceMonitor(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/Middleware/SecurityHeadersMiddleware.cs b/WebCommon/Middleware/SecurityHeadersMiddleware.cs new file mode 100644 index 0000000..82d6dd9 --- /dev/null +++ b/WebCommon/Middleware/SecurityHeadersMiddleware.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Teknik.Configuration; + +namespace Teknik.WebCommon.Middleware +{ + public class SecurityHeadersMiddleware + { + private readonly RequestDelegate _next; + + public SecurityHeadersMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext httpContext) + { + IHeaderDictionary headers = httpContext.Response.Headers; + + // Access Control + headers.Append("Access-Control-Allow-Credentials", "true"); + headers.Append("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS"); + headers.Append("Access-Control-Allow-Headers", "Authorization, Accept, Origin, Content-Type, X-Requested-With, Connection, Transfer-Encoding"); + + // HSTS + headers.Append("strict-transport-security", "max-age=31536000; includeSubdomains; preload"); + + // XSS Protection + headers.Append("X-XSS-Protection", "1; mode=block"); + + // Content Type Options + headers.Append("X-Content-Type-Options", "nosniff"); + + // Referrer Policy + headers.Append("Referrer-Policy", "no-referrer, strict-origin-when-cross-origin"); + + return _next(httpContext); + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class SecurityHeadersMiddlewareExtensions + { + public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/Middleware/SetupHttpContextMiddleware.cs b/WebCommon/Middleware/SetupHttpContextMiddleware.cs new file mode 100644 index 0000000..0c44549 --- /dev/null +++ b/WebCommon/Middleware/SetupHttpContextMiddleware.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Teknik.Configuration; +using Teknik.Utilities; + +namespace Teknik.WebCommon.Middleware +{ + public class SetupHttpContextMiddleware + { + private readonly RequestDelegate _next; + + public SetupHttpContextMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + // Generate the NONCE used for this request + string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringHelper.RandomString(24))); + httpContext.Items[Constants.NONCE_KEY] = nonce; + + await _next(httpContext); + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class SetupHttpContextMiddlewareExtensions + { + public static IApplicationBuilder UseHttpContextSetup(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/WebCommon/WebCommon.csproj b/WebCommon/WebCommon.csproj new file mode 100644 index 0000000..06af9c4 --- /dev/null +++ b/WebCommon/WebCommon.csproj @@ -0,0 +1,18 @@ + + + + net5.0 + Teknik.WebCommon + Teknik.WebCommon + + + + + + + + + + + +