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