aboutsummaryrefslogtreecommitdiff
path: root/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProvider.cs
blob: 705661ce94dcf4b9d70bd46190ded72fdf035ae5 (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
/*
* Copyright (c) 2022 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Sessions.VNCache
* File: WebSessionProvider.cs 
*
* WebSessionProvider.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials.Sessions.VNCache 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.Sessions.VNCache 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.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

using VNLib.Net.Http;
using VNLib.Plugins.Essentials;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Sessions.Cache.Client;

namespace VNLib.Plugins.Sessions.VNCache
{
    /// <summary>
    /// The implementation of a VNCache web based session
    /// </summary>
    [ConfigurationName("web")]
    internal sealed class WebSessionProvider : SessionCacheClient, ISessionProvider
    {
        static readonly TimeSpan BackgroundUpdateTimeout = TimeSpan.FromSeconds(10);

        private readonly IWebSessionIdFactory factory;
        private readonly uint MaxConnections;

        /// <summary>
        /// Initializes a new <see cref="WebSessionProvider"/> 
        /// </summary>
        /// <param name="client">The cache client to make cache operations against</param>
        /// <param name="maxCacheItems">The max number of items to store in cache</param>
        /// <param name="maxWaiting">The maxium number of waiting session events before 503s are sent</param>
        /// <param name="factory">The session-id factory</param>
        public WebSessionProvider(IRemoteCacheStore client, int maxCacheItems, uint maxWaiting, IWebSessionIdFactory factory) : base(client, maxCacheItems)
        {
            this.factory = factory;
            MaxConnections = maxWaiting;
        }

        private string UpdateSessionId(IHttpEvent entity, string oldId)
        {
            //Generate and set a new sessionid
            string newid = factory.GenerateSessionId(entity);
            //Aquire lock on cache
            lock (CacheLock)
            {
                //Change the cache lookup id
                if (CacheTable.Remove(oldId, out RemoteSession? session))
                {
                    CacheTable.Add(newid, session);
                }
            }
            return newid;
        }
        
        protected override RemoteSession SessionCtor(string sessionId) => new WebSession(sessionId, Store, BackgroundUpdateTimeout, UpdateSessionId);

        public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
        {
            //Callback to close the session when the handle is closeed
            static ValueTask HandleClosedAsync(ISession session, IHttpEvent entity)
            {
                return (session as SessionBase)!.UpdateAndRelease(true, entity);
            }
            
            try
            {
                //Get session id
                if (!factory.TryGetSessionId(entity, out string? sessionId))
                {
                    //Id not allowed/found, so do not attach a session
                    return SessionHandle.Empty;
                }

                //Limit max number of waiting clients
                if (WaitingConnections > MaxConnections)
                {
                    //Set 503 for temporary unavail
                    entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
                    return new SessionHandle(null, FileProcessArgs.VirtualSkip, null);
                }

                //Get session
                RemoteSession session = await GetSessionAsync(entity, sessionId, cancellationToken);

                //If the session is new (not in cache), then overwrite the session id with a new one as user may have specified their own
                if (session.IsNew)
                {
                    session.RegenID();
                }

                //Make sure the session has not expired yet
                if (session.Created.Add(factory.ValidFor) < DateTimeOffset.UtcNow)
                {
                    //Invalidate the session, so its technically valid for this request, but will be cleared on this handle close cycle
                    session.Invalidate();
                    //Clear basic login status
                    session.Token = null;
                    session.UserID = null;
                    session.Privilages = 0;
                    session.SetLoginToken(null);
                }
                
                return new SessionHandle(session, HandleClosedAsync);
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (SessionException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new SessionException("Exception raised while retreiving or loading Web session", ex);
            }
        }
    }
}