aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
blob: 4dc320a8d88516126cd8602428369e5a33e4800a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
* Copyright (c) 2022 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Content.Routing
* File: Router.cs 
*
* Router.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials.Content.Routing is free software: you can redistribute it and/or modify 
* it under the terms of the GNU Affero General Public License as 
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Essentials.Content.Routing is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see https://www.gnu.org/licenses/.
*/

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;

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;
        }
    }
}