Files
xIdentityService/Middlewares/XProxyMiddleware.cs
T
2024-01-25 04:44:02 +03:30

827 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using IdentityModel;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using xCommons.Extensions;
using xExceptions.Constants;
using xIdentityService.Configuration;
using xIdentityService.Constants;
using xIdentityService.Interfaces;
using xIdentityService.Providers;
using xModels.Base;
namespace xIdentityService.Middlewares {
/// <summary>
/// Proxy Server Middleware ...
/// </summary>
public class XProxyMiddleware : XBaseClass {
//
#region Pros ...
private readonly HttpClient HTTP_CLIENT;
private readonly RequestDelegate NEXT_MIDDLEWARE;
private readonly XProxyConfiguration CONFIGURATION;
#endregion
//
#region Constructor ...
public XProxyMiddleware (
ILoggerFactory loggerFactory,
RequestDelegate nextMiddleware,
XProxyConfiguration configuration,
IHttpClientFactory httpClientFactory
) : base (loggerFactory) {
//
this.CONFIGURATION = configuration;
this.NEXT_MIDDLEWARE = nextMiddleware;
//
// Create Http Client ...
this.HTTP_CLIENT = httpClientFactory
.CreateClient (XProxyConstants.XPROXY_HTTP_CLIENT);
//
// Logging Server Configuration ...
LogXProxyServerConfig ();
}
#endregion
//
#region Public ...
public async Task InvokeAsync (HttpContext context) {
//
#region Bussiness Logic ...
//
var routeDescriptor = GetRouteDescriptor (context.Request);
if (!routeDescriptor.IsNull ()) {
//
#region Handle Authorization ...
//
try {
//
// Handle XPowered ...
if (routeDescriptor.EnableXPoweredByAthorization) {
//
var isPoweredByPass = context.Request.Headers
.Any (h => h.Key
.ToNormalString () == xCommons.Constants.XAuthorization.XPoweredBy
.ToNormalString () &&
h.Value
.ToString ()
.ToNormalString () == CONFIGURATION.XPoweredValue
.ToNormalString ()
);
//
if (!isPoweredByPass) {
ThrowNotAuthorizedException ();
}
}
//
// Handle EndableAuthorization ...
if (routeDescriptor.EnableAuthorization) {
//
// Retrieve User Info for Authentication ...
var userName = context.User.Identity.Name;
var isUserAuthenticated = !userName.IsNull () &&
context.User.Identity.IsAuthenticated;
if (!isUserAuthenticated &&
!routeDescriptor.AllowedScopes.HasChild ()
) {
ThrowNotAuthorizedException ();
}
//
// Check Allowed Scopes Authorization ...
if (routeDescriptor.AllowedScopes.HasChild ()) {
//
var scopeClaims = context.User.Claims
.Where (c => c.Type == JwtClaimTypes.Scope)
.Select (c => c.Value);
var isScopePassed = scopeClaims.HasChild () &&
scopeClaims.Any (c => routeDescriptor.AllowedScopes
.Any (allowedScope => allowedScope
.ToNormalString () == c
.ToNormalString ()
)
);
if (!isScopePassed) {
ThrowNotAuthorizedException ();
}
}
//
// Check Allowed Roles Authorization ...
if (
isUserAuthenticated &&
routeDescriptor.AllowedRoles.HasChild ()
) {
//
var roleClaim = context.User.Claims
.FirstOrDefault (c => c.Type == JwtClaimTypes.Role);
var isRolePassed = !roleClaim.IsNull () &&
routeDescriptor.AllowedRoles
.Any (role => role
.ToNormalString () == roleClaim.Value
.ToNormalString ());
if (!isRolePassed) {
ThrowNotAuthorizedException ();
}
}
}
} catch {
context.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return;
}
#endregion
//
// Extract Request Uri from Received Request ...
var requestUri = GetUri (context.Request);
if (!requestUri.IsNull ()) {
//
// Retrieve Request based on current ...
var request = GetRequestMessage (
uri: requestUri,
context: context
);
//
SendLog ($"Send Request to Recieve Response ...", LogLevel.Information);
//
// Send Request and Retrieve Response ...
try {
//
var response = await HTTP_CLIENT.SendAsync (
request,
HttpCompletionOption.ResponseContentRead
);
//
// Set Current Response Status Code ...
context.Response.StatusCode = (int) response.StatusCode;
//
// Retrieve Response Headers and Set To Current Response ...
HandleResponseHeaders (context, response);
//
// Processing Response Content ...
await ProcessResponseContent (context, response);
} catch (Exception ex) {
//
LogMessage ($"Exception: {ex.Message}", LogLevel.Error);
//
context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
}
//
return;
}
}
#endregion
//
await NEXT_MIDDLEWARE (context);
}
#endregion
//
#region Private ...
/// <summary>
/// Throw Required Exception ...
/// </summary>
private void ThrowNotAuthorizedException () {
throw XException.NotAuthorized.ToException ();
}
/// <summary>
/// Show propper logs if Enabled ...
/// </summary>
/// <param name="message"></param>
/// <param name="logLevel"></param>
private void SendLog (string message, LogLevel logLevel = LogLevel.Information) {
//
if (!CONFIGURATION.EnableLogging) {
return;
}
//
LogMessage (
message: message,
logLevel: logLevel
);
}
/// <summary>
/// Log Proxy Server Configuration ...
/// </summary>
private void LogXProxyServerConfig () {
//
// Retrieve XProxyConfiguration class from Configurations and Validate them ...
if (CONFIGURATION.IsNull ()) {
throw XException.InvalidConfiguration.ToException ();
}
//
// Try to Log Proxy Configuration ...
ConsoleMessage ("===============================");
ConsoleMessage ("= Proxy Server Configurations: ");
ConsoleMessage ("===============================");
//
ConsoleMessage ($"Host: {CONFIGURATION.Host}");
ConsoleMessage ($"XPoweredValue: {CONFIGURATION.XPoweredValue}");
ConsoleMessage ($"EnableLogging: {CONFIGURATION.EnableLogging}");
ConsoleMessage ($"DisableSSLCheck: {CONFIGURATION.DisableSSLCheck}");
ConsoleMessage ($"AllowAutoRedirect: {CONFIGURATION.AllowAutoRedirect}");
ConsoleMessage ($" ");
ConsoleMessage ($"Routes: ");
ConsoleMessage ($" ");
//
CONFIGURATION.Routes
.ToList ()
.ForEach (routeDescriptor => {
ConsoleMessage ("===============================");
ConsoleMessage ($"= Rote => {routeDescriptor.Route}");
ConsoleMessage ("===============================");
ConsoleMessage ($"AllowedRoles: {routeDescriptor.AllowedRoles.ToJSON()}");
ConsoleMessage ($"AllowedScopes: {routeDescriptor.AllowedScopes.ToJSON()}");
ConsoleMessage ($"EnableAuthorization: {routeDescriptor.EnableAuthorization}");
ConsoleMessage ($"EnableXPoweredByAthorization: {routeDescriptor.EnableXPoweredByAthorization}");
ConsoleMessage ($" ");
});
}
/// <summary>
/// Retrieve Destination Request Path based on Current Request ...
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private Uri GetUri (HttpRequest request) {
//
// Create temp result ...
Uri result = null;
var requestPath = "";
//
SendLog ($"Receive Request: {request.Path} ...", LogLevel.Information);
//
// Try to Findout Route Descriptor ...
var routeDescriptor = GetRouteDescriptor (request);
//
// Route Descriptor Exists && Path for Delegating ...
if (!routeDescriptor.IsNull () &&
request.Path
.StartsWithSegments (routeDescriptor.Route)
) {
//
// Prepare Request Path by Cleaning Starter Path Segment ...
requestPath = request.Path
.ToString ()
.Replace (routeDescriptor.Route, "");
if (requestPath.StartsWith ("/")) {
requestPath = requestPath.Substring (1);
}
//
// Decoding URL ...
requestPath = HttpUtility.UrlDecode (requestPath);
//
var colonIndex = requestPath.IndexOf (":");
if (colonIndex > -1) {
//
var doubleSlash = requestPath.Substring (colonIndex + 1, 2);
var isDoubleSlash = doubleSlash == "//";
//
if (!isDoubleSlash) {
//
var protocol = requestPath
.Substring (0, colonIndex);
var section = requestPath
.Substring (colonIndex + 2, requestPath.Length - colonIndex - 2);
//
requestPath = $"{protocol}://{section}";
}
}
}
//
// Validate Request Path ...
if (!requestPath.IsNullOrEmpty ()) {
//
result = new Uri (requestPath);
//
SendLog ($"Process Request: {requestPath} ...", LogLevel.Information);
}
//
return result;
}
/// <summary>
/// Retrieve Propper Request Message based on HttpContext and uri ...F
/// </summary>
/// <param name="context"></param>
/// <param name="uri"></param>
/// <returns></returns>
private HttpRequestMessage GetRequestMessage (
HttpContext context,
Uri uri
) {
//
var result = new HttpRequestMessage ();
FillRequest (context, result);
//
// Handle Queries ...
SendLog ($"Start Processing Queries: ");
foreach (var query in context.Request.Query) {
//
var key = query.Key;
var value = HttpUtility.UrlDecode (query.Value);
//
uri = new Uri (
QueryHelpers.AddQueryString (
uri.OriginalString,
new Dictionary<string, string> {
[key] = value
}
)
);
//
SendLog ($"Query : {key}:{value} was Processed ...");
}
//
result.RequestUri = uri;
result.Headers.Host = uri.Host;
//
// Retrieve Request Method ...
result.Method = GetMethod (context.Request.Method);
//
return result;
}
/// <summary>
/// Fill Request Message by provide HttpContext ...
/// </summary>
/// <param name="context"></param>
/// <param name="message"></param>
private void FillRequest (
HttpContext context,
HttpRequestMessage message
) {
//
var requestMethod = context.Request.Method;
//
if (!HttpMethods.IsGet (requestMethod) &&
!HttpMethods.IsHead (requestMethod) &&
!HttpMethods.IsTrace (requestMethod) &&
!HttpMethods.IsDelete (requestMethod)
) {
//
var streamContent = new StreamContent (context.Request.Body);
message.Content = streamContent;
}
//
// Handle Base Headers ...
var regularHeaders = context.Request.Headers
.Where (hr => hr.Key != XProxyConstants.XPROXY_ACCEPTED_HEADERS &&
!hr.Key
.Contains (
XProxyConstants.XPROXY_FORWARD_HEADER
)
);
//
// Handle Aceepted Headers ...
IEnumerable<string> acceptedHeaders = null;
var acceptedHeadersKeyValue = context.Request.Headers
.FirstOrDefault (hr =>
hr.Key == XProxyConstants.XPROXY_ACCEPTED_HEADERS
);
if (!acceptedHeadersKeyValue.IsNull ()) {
//
acceptedHeaders = acceptedHeadersKeyValue.Value
.ToString ()
.ParseListString<string> ();
//
// Filter Regular Headers ...
if (
regularHeaders.HasChild () &&
acceptedHeaders.HasChild ()
) {
//
regularHeaders = regularHeaders
.Where (rhr => acceptedHeaders
.Contains (rhr.Key)
);
}
}
foreach (var header in regularHeaders) {
//
SendLog ($"Adding Header: {header.Key}:{header.Value} ...");
//
message.Headers.Add (
header.Key,
header.Value
.ToString ()
);
}
//
// Handle ForWarded Headers ...
// Since Forward Headers is Custom kind, this means user actually need this
// Header, so there is no need to put them on AcceptedHeaders ...
var forwardHeaders = context.Request.Headers
.Where (hr => hr.Key
.Contains (
XProxyConstants.XPROXY_FORWARD_HEADER
)
);
foreach (var header in forwardHeaders) {
//
var key = header.Key
.Replace (
XProxyConstants.XPROXY_FORWARD_HEADER,
string.Empty
);
//
SendLog ($"Forwarding Header: {header.Key} to {key} ...");
//
message.Headers
.Add (
key,
header.Value
.ToString ()
);
}
}
/// <summary>
/// Retreive and Extract Method Type of HttpRequest ...
/// </summary>
/// <param name="method"></param>
/// <returns></returns>
private static HttpMethod GetMethod (string method) {
//
if (HttpMethods.IsDelete (method)) {
return HttpMethod.Delete;
} else if (HttpMethods.IsGet (method)) {
return HttpMethod.Get;
} else if (HttpMethods.IsHead (method)) {
return HttpMethod.Head;
} else if (HttpMethods.IsOptions (method)) {
return HttpMethod.Options;
} else if (HttpMethods.IsPost (method)) {
return HttpMethod.Post;
} else if (HttpMethods.IsPut (method)) {
return HttpMethod.Put;
} else if (HttpMethods.IsTrace (method)) {
return HttpMethod.Trace;
}
//
return new HttpMethod (method);
}
/// <summary>
/// add all headers exists in HttpResponse MEssage to Current Context Response Headers ...
/// </summary>
/// <param name="context"></param>
/// <param name="message"></param>
private void HandleResponseHeaders (
HttpContext context,
HttpResponseMessage message
) {
//
// Handle Message Headers ...
foreach (var header in message.Headers) {
context.Response.Headers[header.Key] = header.Value
.ToArray ();
}
//
// Handle Message Content Headers ...
foreach (var header in message.Content.Headers) {
context.Response.Headers[header.Key] = header.Value
.ToArray ();
}
//
context.Response.Headers.Remove ("transfer-encoding");
}
/// <summary>
/// Processing Response Content and Handle it on HttpContextResponse ...
/// </summary>
/// <param name="context"></param>
/// <param name="response"></param>
/// <returns></returns>
private async Task ProcessResponseContent (
HttpContext context,
HttpResponseMessage message
) {
//
// Reading Content as Byte Array ...
var contentBytes = await message.Content
.ReadAsByteArrayAsync ();
//
// Check ContentType of Response ...
if (
IsContentOfType (message, XResponseContentTypes.HTML) ||
IsContentOfType (message, XResponseContentTypes.JSON) ||
IsContentOfType (message, XResponseContentTypes.JavaScript)
) {
//
// If their String Content ...
var stringContent = Encoding.UTF8.GetString (contentBytes);
//
// TODO: here we can change String Contents ...
//
// Writing new Content to Response ...
await context.Response
.WriteAsync (
stringContent,
Encoding.UTF8
);
} else {
//
// if they have non string content ...
await context.Response.Body
.WriteAsync (
buffer: contentBytes,
offset: 0,
count: contentBytes.Length
);
}
//
SendLog ($"Response Content Processed ...", LogLevel.Information);
}
/// <summary>
/// Check Content Type of Specific Response ...
/// </summary>
/// <param name="response"></param>
/// <param name="type"></param>
/// <returns></returns>
private bool IsContentOfType (
HttpResponseMessage response,
string type
) {
//
// Create EMpty Result ...
var result = false;
//
// Retrieve and Check Content Type of Response ...
if (response.Content?.Headers?.ContentType != null) {
result = response.Content.Headers.ContentType.MediaType == type;
}
//
return result;
}
/// <summary>
/// Retrieve and Extract Route Descriptor ...
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private XProxyRouteDescriptor GetRouteDescriptor (HttpRequest request) {
//
// Validate Args ...
if (
CONFIGURATION.IsNull () ||
!CONFIGURATION.Routes.HasChild ()
) {
return null;
}
//
var result = CONFIGURATION.Routes
.FirstOrDefault (rd => !rd.Route.IsNullOrEmpty () &&
request.Path
.StartsWithSegments (rd.Route
.ToNormalString ()
)
);
//
return result;
}
#endregion
}
/// <summary>
/// Provide all requirements for Dependency Injection Handler ...
/// </summary>
public static class XDIHelperExtension {
/// <summary>
/// Retrieve XProxy Middelware Configuration from Configuration ...
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
public static XProxyConfiguration GetXProxyConfiguration (this IConfiguration config) {
//
var xProxyConfiguration = config.GetSection (ConfigurationNodeNames.XPROXY_CONFIGURATION_NODE_NAME);
return xProxyConfiguration.Get<XProxyConfiguration> ();
}
/// <summary>
/// Register HTTP Handler for XProxy Server ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="httpHandler"></param>
public static void AddXProxyHttpHandler (
this IServiceCollection services,
IConfiguration configuration,
HttpClientHandler httpHandler = null
) {
//
var proxyConfig = configuration.GetXProxyConfiguration ();
if (proxyConfig.IsNull ()) {
throw XException.InvalidConfiguration.ToException ();
}
//
services.AddXProxyHttpHandler (
httpHandler: httpHandler,
configuration: proxyConfig
);
}
/// <summary>
/// Register HTTP Handler for XProxy Server ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="httpHandler"></param>
public static void AddXProxyHttpHandler (
this IServiceCollection services,
XProxyConfiguration configuration,
HttpClientHandler httpHandler = null
) {
//
if (configuration.IsNull ()) {
throw XException.InvalidConfiguration.ToException ();
}
//
if (httpHandler.IsNull ()) {
httpHandler = new HttpClientHandler ();
}
//
httpHandler.AllowAutoRedirect = configuration.AllowAutoRedirect;
//
if (configuration.DisableSSLCheck) {
httpHandler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
}
//
services
.AddHttpClient (XProxyConstants.XPROXY_HTTP_CLIENT)
.ConfigurePrimaryHttpMessageHandler (() => {
return httpHandler;
});
}
/// <summary>
/// Register XProxy On Server ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="httpHandler"></param>
public static void AddXProxyServer (
this IServiceCollection services,
IConfiguration configuration,
HttpClientHandler httpHandler = null
) {
//
var proxyConfig = configuration.GetXProxyConfiguration ();
//
services.AddXProxyServer (
httpHandler: httpHandler,
configuration: proxyConfig
);
}
/// <summary>
/// Register XProxy On Server ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <param name="httpHandler"></param>
public static void AddXProxyServer (
this IServiceCollection services,
XProxyConfiguration configuration,
HttpClientHandler httpHandler = null
) {
//
// Validate Args ...
if (configuration.IsNull ()) {
throw XException.InvalidConfiguration.ToException ();
}
//
// Register Proxy Configuration ...
services.AddSingleton<XProxyConfiguration> (configuration);
//
// Regiter Proxy Handler ...
services.AddXProxyHttpHandler (
httpHandler: httpHandler,
configuration: configuration
);
}
/// <summary>
/// Register XProxy On Server ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddXProxyClient (
this IServiceCollection services,
IConfiguration configuration
) {
//
var proxyConfig = configuration.GetXProxyConfiguration ();
//
services.AddXProxyClient (
configuration: proxyConfig
);
}
/// <summary>
/// Register XProxy On Client ...
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddXProxyClient (
this IServiceCollection services,
XProxyConfiguration configuration
) {
//
// Validate Args ...
if (configuration.IsNull ()) {
throw XException.InvalidConfiguration.ToException ();
}
//
// Register Proxy Configuration ...
services.AddSingleton<XProxyConfiguration> (configuration);
//
// Register IXProxyHelper ...
services.AddSingleton<IXProxyHelper, XProxyHelper> ();
}
/// <summary>
/// Adding XProxy Middleware to Applications ...
/// </summary>
/// <param name="app"></param>
public static void UseXProxy (this IApplicationBuilder app) {
app.UseMiddleware<XProxyMiddleware> ();
}
}
}