diff options
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions')
4 files changed, 111 insertions, 70 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs index 35e2fea..4e09b04 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs @@ -16,15 +16,16 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory { private readonly Dictionary<string, string> DataStorage; - private readonly MemorySessionStore SessionStore; + private readonly Func<IHttpEvent, string, string> OnSessionUpdate; - public MemorySession(IPAddress ipAddress, MemorySessionStore SessionStore) + public MemorySession(string sessionId, IPAddress ipAddress, Func<IHttpEvent, string, string> onSessionUpdate) { //Set the initial is-new flag DataStorage = new Dictionary<string, string>(10); - this.SessionStore = SessionStore; + + OnSessionUpdate = onSessionUpdate; //Get new session id - SessionID = SessionStore.NewSessionID; + SessionID = sessionId; UserIP = ipAddress; SessionType = SessionType.Web; Created = DateTimeOffset.UtcNow; @@ -45,7 +46,8 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory { //Clear storage, and regenerate the sessionid DataStorage.Clear(); - RegenId(state); + //store new sessionid + SessionID = OnSessionUpdate(state, SessionID); //Reset ip-address UserIP = state.Server.GetTrustedIp(); //Update created-time @@ -58,26 +60,14 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory else if (Flags.IsSet(REGEN_ID_MSK)) { //Regen id without modifying the data store - RegenId(state); + SessionID = OnSessionUpdate(state, SessionID); } //Clear flags Flags.ClearAll(); //Memory session always completes return ValueTask.FromResult<Task?>(null); } - - private void RegenId(IHttpEvent entity) - { - //Get a new session-id - string newId = SessionStore.NewSessionID; - //Update the cache entry - SessionStore.UpdateRecord(newId, this); - //store new sessionid - SessionID = newId; - //set cookie - SessionStore.SetSessionCookie(entity, this); - } - + protected override Task OnEvictedAsync() { //Clear all session data diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs index f41d384..df5dd59 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs @@ -52,6 +52,9 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory //Schedule garbage collector _ = plugin.ScheduleInterval(this, TimeSpan.FromMinutes(1)); + + //Call cleanup on exit + _ = plugin.UnloadToken.RegisterUnobserved(_sessions.Cleanup); } Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs index 15c3002..388f998 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs @@ -3,7 +3,6 @@ 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; @@ -11,10 +10,10 @@ using VNLib.Utils.Async; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Extensions; +#nullable enable namespace VNLib.Plugins.Essentials.Sessions.Memory { - /// <summary> /// An <see cref="ISessionProvider"/> for in-process-memory backed sessions /// </summary> @@ -23,11 +22,13 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory private readonly Dictionary<string, MemorySession> SessionsStore; internal readonly MemorySessionConfig Config; + internal readonly SessionIdFactory IdFactory; public MemorySessionStore(MemorySessionConfig config) { Config = config; SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal); + IdFactory = new(config.SessionIdSizeBytes, config.SessionCookieID, config.SessionTimeout); } ///<inheritdoc/> @@ -36,11 +37,11 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory static ValueTask SessionHandleClosedAsync(ISession session, IHttpEvent ev) { - return (session as MemorySession).UpdateAndRelease(true, 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 id for the session + if (IdFactory.TryGetSessionId(entity, out string? sessionId)) { //Try to get the old record or evict it ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession session); @@ -50,63 +51,51 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory await session.WaitOneAsync(cancellationToken); return new (session, SessionHandleClosedAsync); } - //Continue creating a new session + else + { + //try to cleanup expired records + GC(); + //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 + session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId); + //Increment the semaphore + (session as IWaitHandle).WaitOne(); + //store the session in cache while holding semaphore, and set its expiration + SessionsStore.StoreRecord(session.SessionID, session, Config.SessionTimeout); + //Init new session handle + return new (session, SessionHandleClosedAsync); + } } - - //Dont service non browsers for new sessions - if (!entity.Server.IsBrowser()) + else { 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) + private string UpdateSessionId(IHttpEvent entity, string oldId) { + //Generate and set a new sessionid + string newid = IdFactory.GenerateSessionId(entity); + //Aquire lock on cache lock (SessionsStore) { - //Remove old record from the store - SessionsStore.Remove(session.SessionID); - //Insert the new session - SessionsStore.Add(newSessId, session); + //Change the cache lookup id + if (SessionsStore.Remove(oldId, out MemorySession? session)) + { + SessionsStore.Add(newid, session); + } } + return newid; } - /// <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> diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs new file mode 100644 index 0000000..ff0608e --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Hashing; +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Extensions; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Sessions.Memory +{ + internal sealed class SessionIdFactory : ISessionIdFactory + { + private readonly int IdSize; + private readonly string cookieName; + private readonly TimeSpan ValidFor; + + public SessionIdFactory(uint idSize, string cookieName, TimeSpan validFor) + { + IdSize = (int)idSize; + this.cookieName = cookieName; + ValidFor = validFor; + } + + public string GenerateSessionId(IHttpEvent entity) + { + //Random hex hash + string cookie = RandomHash.GetRandomBase32(IdSize); + + //Set the session id cookie + entity.Server.SetCookie(cookieName, cookie, ValidFor, secure: true, httpOnly: true); + + //return session-id value from cookie value + return cookie; + } + + public bool TryGetSessionId(IHttpEvent entity, [NotNullWhen(true)] out string? sessionId) + { + //Get authorization token and make sure its not too large to cause a buffer overflow + if (entity.Server.GetCookie(cookieName, out sessionId)) + { + return true; + } + //Only add sessions for user-agents + else if (entity.Server.IsBrowser()) + { + //Get a new session id + sessionId = GenerateSessionId(entity); + + return true; + } + else + { + sessionId = null; + return false; + } + } + } +} |