aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
blob: 15c3002b4f9a668ed3a164f2f29be8ba59c17a6e (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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

using VNLib.Hashing;
using VNLib.Net.Http;
using VNLib.Net.Sessions;
using VNLib.Utils;
using VNLib.Utils.Async;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Extensions;


namespace VNLib.Plugins.Essentials.Sessions.Memory
{

    /// <summary>
    /// An <see cref="ISessionProvider"/> for in-process-memory backed sessions
    /// </summary>
    internal sealed class MemorySessionStore : ISessionProvider
    {
        private readonly Dictionary<string, MemorySession> SessionsStore;

        internal readonly MemorySessionConfig Config;

        public MemorySessionStore(MemorySessionConfig config)
        {
            Config = config;
            SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal);
        }
        
        ///<inheritdoc/>
        public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
        {
            
            static ValueTask SessionHandleClosedAsync(ISession session, IHttpEvent ev)
            {
                return (session as MemorySession).UpdateAndRelease(true, ev);
            }
            
            //Check for previous session cookie
            if (entity.Server.RequestCookies.TryGetNonEmptyValue(Config.SessionCookieID, out string sessionId))
            {
                //Try to get the old record or evict it
                ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession session);
                if(result > 0)
                {
                    //Valid, now wait for exclusive access
                    await session.WaitOneAsync(cancellationToken);
                    return new (session, SessionHandleClosedAsync);
                }
                //Continue creating a new session
            }
            
            //Dont service non browsers for new sessions
            if (!entity.Server.IsBrowser())
            {
                return SessionHandle.Empty;
            }
            
            //try to cleanup expired records
            SessionsStore.CollectRecords();
            //Make sure there is enough room to add a new session
            if (SessionsStore.Count >= Config.MaxAllowedSessions)
            {
                entity.Server.SetNoCache();
                //Set 503 when full
                entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
                //Cannot service new session
                return new(null, FileProcessArgs.VirtualSkip, null);
            }
            //Initialze a new session
            MemorySession ms = new(entity.Server.GetTrustedIp(), this);
            //Set session cookie 
            SetSessionCookie(entity, ms);
            //Increment the semaphore
            (ms as IWaitHandle).WaitOne();
            //store the session in cache while holding semaphore, and set its expiration
            SessionsStore.StoreRecord(ms.SessionID, ms, Config.SessionTimeout);
            //Init new session handle
            return new SessionHandle(ms, SessionHandleClosedAsync);
        }

        /// <summary>
        /// Gets a new unique sessionid for sessions
        /// </summary>
        internal string NewSessionID => RandomHash.GetRandomHex((int)Config.SessionIdSizeBytes);

        internal void UpdateRecord(string newSessId, MemorySession session)
        {
            lock (SessionsStore)
            {
                //Remove old record from the store
                SessionsStore.Remove(session.SessionID);
                //Insert the new session
                SessionsStore.Add(newSessId, session);
            }
        }
        /// <summary>
        /// Sets a standard session cookie for an entity/connection
        /// </summary>
        /// <param name="entity">The entity to set the cookie on</param>
        /// <param name="session">The session attached to the </param>
        internal void SetSessionCookie(IHttpEvent entity, MemorySession session)
        {
            //Set session cookie 
            entity.Server.SetCookie(Config.SessionCookieID, session.SessionID, null, "/", Config.SessionTimeout, CookieSameSite.Lax, true, true);
        }
        /// <summary>
        /// Evicts all sessions from the current store
        /// </summary>
        public void Cleanup()
        {
            //Expire all old records to cleanup all entires
            this.SessionsStore.CollectRecords(DateTime.MaxValue);
        }
        /// <summary>
        /// Collects all expired records from the current store
        /// </summary>
        public void GC()
        {
            //collect expired records
            this.SessionsStore.CollectRecords();
        }
    }
}