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 { /// /// An for in-process-memory backed sessions /// internal sealed class MemorySessionStore : ISessionProvider { private readonly Dictionary SessionsStore; internal readonly MemorySessionConfig Config; public MemorySessionStore(MemorySessionConfig config) { Config = config; SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal); } /// public async ValueTask 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); } /// /// Gets a new unique sessionid for sessions /// 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); } } /// /// Sets a standard session cookie for an entity/connection /// /// The entity to set the cookie on /// The session attached to the internal void SetSessionCookie(IHttpEvent entity, MemorySession session) { //Set session cookie entity.Server.SetCookie(Config.SessionCookieID, session.SessionID, null, "/", Config.SessionTimeout, CookieSameSite.Lax, true, true); } /// /// Evicts all sessions from the current store /// public void Cleanup() { //Expire all old records to cleanup all entires this.SessionsStore.CollectRecords(DateTime.MaxValue); } /// /// Collects all expired records from the current store /// public void GC() { //collect expired records this.SessionsStore.CollectRecords(); } } }