diff options
author | vman <public@vaughnnugent.com> | 2022-11-18 16:08:51 -0500 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-11-18 16:08:51 -0500 |
commit | 526c2364b9ad685d1c000fc8a168bf1305aaa8b7 (patch) | |
tree | a2bc01607320a6a75e1a869d5bd34e79fd63c595 /VNLib.Plugins.Essentials.Content.Routing/Router.cs | |
parent | 2080400119be00bdc354f3121d84ec2f89606ac7 (diff) |
Add project files.
Diffstat (limited to 'VNLib.Plugins.Essentials.Content.Routing/Router.cs')
-rw-r--r-- | VNLib.Plugins.Essentials.Content.Routing/Router.cs | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/VNLib.Plugins.Essentials.Content.Routing/Router.cs b/VNLib.Plugins.Essentials.Content.Routing/Router.cs new file mode 100644 index 0000000..7c67f4f --- /dev/null +++ b/VNLib.Plugins.Essentials.Content.Routing/Router.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; + +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Loading.Events; +using VNLib.Plugins.Essentials.Content.Routing.Model; +using static VNLib.Plugins.Essentials.Accounts.AccountManager; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Content.Routing +{ + internal class Router : IPageRouter, IIntervalScheduleable + { + private static readonly RouteComparer Comparer = new(); + + private readonly RouteStore Store; + + private readonly ConcurrentDictionary<IWebRoot, Task<ReadOnlyCollection<Route>>> RouteTable; + + public Router(PluginBase plugin) + { + Store = new(plugin.GetContextOptions()); + _ = plugin.ScheduleInterval(this, TimeSpan.FromSeconds(30)); + RouteTable = new(); + } + + ///<inheritdoc/> + public async ValueTask<FileProcessArgs> RouteAsync(HttpEntity entity) + { + ulong privilage = READ_MSK; + //Only select privilages for logged-in users + if (entity.Session.IsSet && entity.LoginCookieMatches() || entity.TokenMatches()) + { + privilage = entity.Session.Privilages; + } + //Get the routing table for the current host + ReadOnlyCollection<Route> routes = await RouteTable.GetOrAdd(entity.RequestedRoot, LoadRoutesAsync); + //Find the proper routine for the connection + return FindArgs(routes, entity.RequestedRoot.Hostname, entity.Server.Path, privilage); + } + + /// <summary> + /// Clears all cached routines from the database + /// </summary> + public void ResetRoutes() => RouteTable.Clear(); + + private async Task<ReadOnlyCollection<Route>> LoadRoutesAsync(IWebRoot root) + { + List<Route> collection = new(); + //Load all routes + _ = await Store.GetPageAsync(collection, 0, int.MaxValue); + //Select only exact match routes, or wildcard routes + return (from r in collection + where r.Hostname.EndsWith(root.Hostname, StringComparison.OrdinalIgnoreCase) || r.Hostname == "*" + //Orderby path "specificity" longer pathts are generally more specific, so filter order + orderby r.MatchPath.Length ascending + select r) + .ToList() + .AsReadOnly(); + } + + + private static FileProcessArgs FindArgs(ReadOnlyCollection<Route> routes, string hostname, string path, ulong privilages) + { + //Rent an array to sort routes for the current user + Route[] matchArray = ArrayPool<Route>.Shared.Rent(routes.Count); + int count = 0; + //Search for routes that match + for(int i = 0; i < routes.Count; i++) + { + if(Matches(routes[i], hostname, path, privilages)) + { + //Add to sort array + matchArray[count++] = routes[i]; + } + } + //If no matches are found, return continue routine + if (count == 0) + { + //Return the array to the pool + ArrayPool<Route>.Shared.Return(matchArray); + return FileProcessArgs.Continue; + } + //Get sorting span for matches + Span<Route> found = matchArray.AsSpan(0, count); + //Sort the found rules + found.Sort(Comparer); + //Select the last element + Route selected = found[^1]; + //Return array to pool + ArrayPool<Route>.Shared.Return(matchArray); + return selected.MatchArgs; + } + + /// <summary> + /// Determines if a route can be matched to a hostname, resource path, and a + /// privilage level + /// </summary> + /// <param name="route">The route to test against</param> + /// <param name="hostname">The hostname to test</param> + /// <param name="path">The resource path to test</param> + /// <param name="privilages">The privialge level to search for</param> + /// <returns>True if the route can be matched to the resource and the privialge level</returns> + private static bool Matches(Route route, ReadOnlySpan<char> hostname, ReadOnlySpan<char> path, ulong privilages) + { + //Get span of hostname to stop string heap allocations during comparisons + ReadOnlySpan<char> routineHost = route.Hostname; + ReadOnlySpan<char> routinePath = route.MatchPath; + //Test if hostname hostname matches exactly (may be wildcard) or hostname begins with a wildcard and ends with the request hostname + bool hostMatch = routineHost.SequenceEqual(hostname) || (routineHost.Length > 1 && routineHost[0] == '*' && hostname.EndsWith(routineHost[1..])); + if (!hostMatch) + { + return false; + } + //Test if path is a wildcard, matches exactly, or if the path is a wildcard path, that the begining of the reqest path matches the routine path + bool pathMatch = routinePath == "*" || routinePath.SequenceEqual(path) || (routinePath.Length > 1 && routinePath[^1] == '*' && path.StartsWith(routinePath[..^1])); + if (!pathMatch) + { + return false; + } + //Test if the level and group privilages match for the current routine + return (privilages & LEVEL_MSK) >= (route.Privilage & LEVEL_MSK) && (route.Privilage & GROUP_MSK) == (privilages & GROUP_MSK); + } + + Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} |