diff --git a/src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs b/src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs index 77c389b..e21044f 100644 --- a/src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs +++ b/src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using DotNetCore.CAP; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder @@ -33,5 +34,24 @@ namespace Microsoft.AspNetCore.Builder bootstrapper.BootstrapAsync(); return app; } + + public static IApplicationBuilder UseCapDashboard( + this IApplicationBuilder app, + string pathMatch = "/cap") + { + if (app == null) throw new ArgumentNullException(nameof(app)); + if (pathMatch == null) throw new ArgumentNullException(nameof(pathMatch)); + + var marker = app.ApplicationServices.GetService(); + + if (marker == null) + { + throw new InvalidOperationException("Add Cap must be called on the service collection."); + } + + app.Map(new PathString(pathMatch), x => x.UseMiddleware(storage, options, routes)); + + return app; + } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/CAP.DashboardMiddleware.cs b/src/DotNetCore.CAP/CAP.DashboardMiddleware.cs new file mode 100644 index 0000000..47716bf --- /dev/null +++ b/src/DotNetCore.CAP/CAP.DashboardMiddleware.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using DotNetCore.CAP.Dashboard; +using Microsoft.AspNetCore.Http; + +namespace DotNetCore.CAP +{ + public class DashboardMiddleware + { + private readonly DashboardOptions _options; + private readonly RequestDelegate _next; + private readonly IStorage _storage; + private readonly RouteCollection _routes; + + public DashboardMiddleware(RequestDelegate next, DashboardOptions options, IStorage storage, RouteCollection routes) + { + if (next == null) throw new ArgumentNullException(nameof(next)); + if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (options == null) throw new ArgumentNullException(nameof(options)); + if (routes == null) throw new ArgumentNullException(nameof(routes)); + + _next = next; + _options = options; + _storage = storage; + _routes = routes; + } + + public Task Invoke(HttpContext httpContext) + { + var context = new CapDashboardContext(_storage, _options, httpContext); + var findResult = _routes.FindDispatcher(httpContext.Request.Path.Value); + + if (findResult == null) + { + return _next.Invoke(httpContext); + } + + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var filter in _options.Authorization) + { + if (!filter.Authorize(context)) + { + var isAuthenticated = httpContext.User?.Identity?.IsAuthenticated; + + httpContext.Response.StatusCode = isAuthenticated == true + ? (int)HttpStatusCode.Forbidden + : (int)HttpStatusCode.Unauthorized; + + return Task.FromResult(0); + } + } + + context.UriMatch = findResult.Item2; + + return findResult.Item1.Dispatch(context); + } + } +} diff --git a/src/DotNetCore.CAP/DashboardOptions.cs b/src/DotNetCore.CAP/CAP.DashboardOptions.cs similarity index 100% rename from src/DotNetCore.CAP/DashboardOptions.cs rename to src/DotNetCore.CAP/CAP.DashboardOptions.cs diff --git a/src/DotNetCore.CAP/CAP.DashboardOptionsExtensions.cs b/src/DotNetCore.CAP/CAP.DashboardOptionsExtensions.cs new file mode 100644 index 0000000..9080771 --- /dev/null +++ b/src/DotNetCore.CAP/CAP.DashboardOptionsExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetCore.CAP +{ + internal sealed class DashboardOptionsExtension : ICapOptionsExtension + { + private readonly Action _options; + + public DashboardOptionsExtension(Action option) + { + _options = option; + } + + public void AddServices(IServiceCollection services) + { + var dashboardOptions = new DashboardOptions(); + _options?.Invoke(dashboardOptions); + services.AddSingleton(dashboardOptions); + } + } + + + public static class CapOptionsExtensions + { + /// + /// Configuration to use kafka in CAP. + /// + /// Provides programmatic configuration for the kafka . + /// + public static CapOptions UseDashboard(this CapOptions capOptions, Action options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + capOptions.RegisterExtension(new DashboardOptionsExtension(options)); + + return capOptions; + } + } + +} diff --git a/src/DotNetCore.CAP/Dashboard/DashboardContext.cs b/src/DotNetCore.CAP/Dashboard/DashboardContext.cs index 21a7811..2a99fcd 100644 --- a/src/DotNetCore.CAP/Dashboard/DashboardContext.cs +++ b/src/DotNetCore.CAP/Dashboard/DashboardContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; namespace DotNetCore.CAP.Dashboard { @@ -24,4 +25,22 @@ namespace DotNetCore.CAP.Dashboard public DashboardRequest Request { get; protected set; } public DashboardResponse Response { get; protected set; } } + + public sealed class CapDashboardContext : DashboardContext + { + public CapDashboardContext( + IStorage storage, + DashboardOptions options, + HttpContext httpContext) + : base(storage, options) + { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + + HttpContext = httpContext; + Request = new CapDashboardRequest(httpContext); + Response = new CapDashboardResponse(httpContext); + } + + public HttpContext HttpContext { get; } + } } diff --git a/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs b/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs index ee04d5a..fe54cf0 100644 --- a/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs +++ b/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace DotNetCore.CAP.Dashboard { @@ -15,4 +17,28 @@ namespace DotNetCore.CAP.Dashboard public abstract string GetQuery(string key); public abstract Task> GetFormValuesAsync(string key); } + + internal sealed class CapDashboardRequest : DashboardRequest + { + private readonly HttpContext _context; + + public CapDashboardRequest(HttpContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + _context = context; + } + + public override string Method => _context.Request.Method; + public override string Path => _context.Request.Path.Value; + public override string PathBase => _context.Request.PathBase.Value; + public override string LocalIpAddress => _context.Connection.LocalIpAddress.ToString(); + public override string RemoteIpAddress => _context.Connection.RemoteIpAddress.ToString(); + public override string GetQuery(string key) => _context.Request.Query[key]; + + public override async Task> GetFormValuesAsync(string key) + { + var form = await _context.Request.ReadFormAsync(); + return form[key]; + } + } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs b/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs index 7fb9024..c3226b8 100644 --- a/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs +++ b/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs @@ -1,6 +1,8 @@ using System; +using System.Globalization; using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace DotNetCore.CAP.Dashboard { @@ -14,4 +16,39 @@ namespace DotNetCore.CAP.Dashboard public abstract void SetExpire(DateTimeOffset? value); public abstract Task WriteAsync(string text); } + + internal sealed class CapDashboardResponse : DashboardResponse + { + private readonly HttpContext _context; + + public CapDashboardResponse( HttpContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + _context = context; + } + + public override string ContentType + { + get { return _context.Response.ContentType; } + set { _context.Response.ContentType = value; } + } + + public override int StatusCode + { + get { return _context.Response.StatusCode; } + set { _context.Response.StatusCode = value; } + } + + public override Stream Body => _context.Response.Body; + + public override Task WriteAsync(string text) + { + return _context.Response.WriteAsync(text); + } + + public override void SetExpire(DateTimeOffset? value) + { + _context.Response.Headers["Expires"] = value?.ToString("r", CultureInfo.InvariantCulture); + } + } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/RouteCollection.cs b/src/DotNetCore.CAP/Dashboard/RouteCollection.cs new file mode 100644 index 0000000..c0d19f3 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/RouteCollection.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace DotNetCore.CAP.Dashboard +{ + public class RouteCollection + { + private readonly List> _dispatchers + = new List>(); + + public void Add(string pathTemplate, IDashboardDispatcher dispatcher) + { + if (pathTemplate == null) throw new ArgumentNullException(nameof(pathTemplate)); + if (dispatcher == null) throw new ArgumentNullException(nameof(dispatcher)); + + _dispatchers.Add(new Tuple(pathTemplate, dispatcher)); + } + + public Tuple FindDispatcher(string path) + { + if (path.Length == 0) path = "/"; + + foreach (var dispatcher in _dispatchers) + { + var pattern = dispatcher.Item1; + + if (!pattern.StartsWith("^", StringComparison.OrdinalIgnoreCase)) + pattern = "^" + pattern; + if (!pattern.EndsWith("$", StringComparison.OrdinalIgnoreCase)) + pattern += "$"; + + var match = Regex.Match( + path, + pattern, + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); + + if (match.Success) + { + return new Tuple(dispatcher.Item2, match); + } + } + + return null; + } + } +}