aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Sessions
diff options
context:
space:
mode:
authorLibravatar vman <public@vaughnnugent.com>2022-10-30 02:28:12 -0400
committerLibravatar vman <public@vaughnnugent.com>2022-10-30 02:28:12 -0400
commita8510fb835dcc5e1142d700164ce5a4bd44e1a25 (patch)
tree28caab320f777a384cb6883b68dd999cdc8c0a3f /Libs/VNLib.Plugins.Essentials.Sessions
Add project files.
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs107
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionConfig.cs33
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs64
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs127
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj43
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.xml75
6 files changed, 449 insertions, 0 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs
new file mode 100644
index 0000000..35e2fea
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Extensions;
+
+using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Sessions.Memory
+{
+ internal class MemorySession : SessionBase
+ {
+ private readonly Dictionary<string, string> DataStorage;
+
+ private readonly MemorySessionStore SessionStore;
+
+ public MemorySession(IPAddress ipAddress, MemorySessionStore SessionStore)
+ {
+ //Set the initial is-new flag
+ DataStorage = new Dictionary<string, string>(10);
+ this.SessionStore = SessionStore;
+ //Get new session id
+ SessionID = SessionStore.NewSessionID;
+ UserIP = ipAddress;
+ SessionType = SessionType.Web;
+ Created = DateTimeOffset.UtcNow;
+ //Init
+ IsNew = true;
+ }
+ //Store in memory directly
+ public override IPAddress UserIP { get; protected set; }
+
+ //Session type has no backing store, so safe to hard-code it's always web
+
+ public override SessionType SessionType => SessionType.Web;
+
+ protected override ValueTask<Task?> UpdateResource(bool isAsync, IHttpEvent state)
+ {
+ //if invalid is set, invalide the current session
+ if (Flags.IsSet(INVALID_MSK))
+ {
+ //Clear storage, and regenerate the sessionid
+ DataStorage.Clear();
+ RegenId(state);
+ //Reset ip-address
+ UserIP = state.Server.GetTrustedIp();
+ //Update created-time
+ Created = DateTimeOffset.UtcNow;
+ //Re-initialize the session to the state of the current connection
+ this.InitNewSession(state.Server);
+ //Modified flag doesnt matter since there is no write-back
+
+ }
+ else if (Flags.IsSet(REGEN_ID_MSK))
+ {
+ //Regen id without modifying the data store
+ RegenId(state);
+ }
+ //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
+ DataStorage.Clear();
+ return Task.CompletedTask;
+ }
+
+ protected override string IndexerGet(string key)
+ {
+ return DataStorage.GetValueOrDefault(key, string.Empty);
+ }
+
+ protected override void IndexerSet(string key, string value)
+ {
+ //Check for special keys
+ switch (key)
+ {
+ //For tokens/login hashes, we can set the upgrade flag
+ case TOKEN_ENTRY:
+ case LOGIN_TOKEN_ENTRY:
+ Flags.Set(REGEN_ID_MSK);
+ break;
+ }
+ DataStorage[key] = value;
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionConfig.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionConfig.cs
new file mode 100644
index 0000000..cbbaf53
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionConfig.cs
@@ -0,0 +1,33 @@
+using System;
+using VNLib.Utils.Logging;
+
+namespace VNLib.Net.Sessions
+{
+ /// <summary>
+ /// Represents configration variables used to create and operate http sessions.
+ /// </summary>
+ public readonly struct MemorySessionConfig
+ {
+ /// <summary>
+ /// The name of the cookie to use for matching sessions
+ /// </summary>
+ public string SessionCookieID { get; init; }
+ /// <summary>
+ /// The size (in bytes) of the genreated SessionIds
+ /// </summary>
+ public uint SessionIdSizeBytes { get; init; }
+ /// <summary>
+ /// The amount of time a session is valid (within the backing store)
+ /// </summary>
+ public TimeSpan SessionTimeout { get; init; }
+ /// <summary>
+ /// The log for which all errors within the <see cref="SessionProvider"/> instance will be written to.
+ /// </summary>
+ public ILogProvider SessionLog { get; init; }
+ /// <summary>
+ /// The maximum number of sessions allowed to be cached in memory. If this value is exceed requests to this
+ /// server will be denied with a 503 error code
+ /// </summary>
+ public int MaxAllowedSessions { get; init; }
+ }
+} \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs
new file mode 100644
index 0000000..f41d384
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+
+using VNLib.Net.Http;
+using VNLib.Net.Sessions;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Extensions.Loading.Configuration;
+using VNLib.Plugins.Extensions.Loading.Events;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Sessions.Memory
+{
+ public sealed class MemorySessionEntrypoint : IRuntimeSessionProvider, IIntervalScheduleable
+ {
+ const string WEB_SESSION_CONFIG = "web";
+
+ private MemorySessionStore? _sessions;
+
+ bool IRuntimeSessionProvider.CanProcess(IHttpEvent entity)
+ {
+ //Web sessions can always be provided
+ return _sessions != null;
+ }
+
+ public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ return _sessions!.GetSessionAsync(entity, cancellationToken);
+ }
+
+ void IRuntimeSessionProvider.Load(PluginBase plugin, ILogProvider localized)
+ {
+ //Get websessions config element
+
+ IReadOnlyDictionary<string, JsonElement> webSessionConfig = plugin.GetConfig(WEB_SESSION_CONFIG);
+
+ MemorySessionConfig config = new()
+ {
+ SessionLog = localized,
+ MaxAllowedSessions = webSessionConfig["cache_size"].GetInt32(),
+ SessionIdSizeBytes = webSessionConfig["cookie_size"].GetUInt32(),
+ SessionTimeout = webSessionConfig["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds),
+ SessionCookieID = webSessionConfig["cookie_name"].GetString() ?? throw new KeyNotFoundException($"Missing required element 'cookie_name' for config '{WEB_SESSION_CONFIG}'"),
+ };
+
+ _sessions = new(config);
+
+ //Schedule garbage collector
+ _ = plugin.ScheduleInterval(this, TimeSpan.FromMinutes(1));
+ }
+
+ Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
+ {
+ //Cleanup expired sessions on interval
+ _sessions?.GC();
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
new file mode 100644
index 0000000..15c3002
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
@@ -0,0 +1,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();
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj
new file mode 100644
index 0000000..0d21b31
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj
@@ -0,0 +1,43 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Platforms>AnyCPU;x64</Platforms>
+
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <AssemblyName>VNLib.Plugins.Essentials.Sessions.Memory</AssemblyName>
+ <RootNamespace>VNLib.Plugins.Essentials.Sessions.Memory</RootNamespace>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+ <DocumentationFile></DocumentationFile>
+ </PropertyGroup>
+ <ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions>
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\VNLib\Essentials\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\..\VNLib\Http\VNLib.Net.Http.csproj" />
+ <ProjectReference Include="..\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" />
+ </ItemGroup>
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\Web Plugins\DevPlugins\SessionProviders\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+
+</Project>
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.xml b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.xml
new file mode 100644
index 0000000..9c596c3
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<doc>
+ <assembly>
+ <name>VNLib.Plugins.Essentials.Sessions.Memory</name>
+ </assembly>
+ <members>
+ <member name="T:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore">
+ <summary>
+ An <see cref="T:VNLib.Plugins.Essentials.Sessions.ISessionProvider"/> for in-process-memory backed sessions
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore.GetSessionAsync(VNLib.Net.Http.HttpEvent,System.Threading.CancellationToken)">
+ <inheritdoc/>
+ </member>
+ <member name="P:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore.NewSessionID">
+ <summary>
+ Gets a new unique sessionid for sessions
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore.SetSessionCookie(VNLib.Net.Http.HttpEvent,VNLib.Plugins.Essentials.Sessions.Memory.MemorySession)">
+ <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>
+ </member>
+ <member name="M:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore.Cleanup">
+ <summary>
+ Evicts all sessions from the current store
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Essentials.Sessions.Memory.MemorySessionStore.GC">
+ <summary>
+ Collects all expired records from the current store
+ </summary>
+ </member>
+ <member name="T:VNLib.Plugins.Essentials.Sessions.Memory.MemSessionHandle">
+ <summary>
+ Provides a one-time-use handle (similar to asyncReleaser, or openHandle)
+ that holds exclusive access to a session until it is released
+ </summary>
+ </member>
+ <member name="T:VNLib.Net.Sessions.MemorySessionConfig">
+ <summary>
+ Represents configration variables used to create and operate http sessions.
+ </summary>
+ </member>
+ <member name="P:VNLib.Net.Sessions.MemorySessionConfig.SessionCookieID">
+ <summary>
+ The name of the cookie to use for matching sessions
+ </summary>
+ </member>
+ <member name="P:VNLib.Net.Sessions.MemorySessionConfig.SessionIdSizeBytes">
+ <summary>
+ The size (in bytes) of the genreated SessionIds
+ </summary>
+ </member>
+ <member name="P:VNLib.Net.Sessions.MemorySessionConfig.SessionTimeout">
+ <summary>
+ The amount of time a session is valid (within the backing store)
+ </summary>
+ </member>
+ <member name="P:VNLib.Net.Sessions.MemorySessionConfig.SessionLog">
+ <summary>
+ The log for which all errors within the <see cref="!:SessionProvider"/> instance will be written to.
+ </summary>
+ </member>
+ <member name="P:VNLib.Net.Sessions.MemorySessionConfig.MaxAllowedSessions">
+ <summary>
+ The maximum number of sessions allowed to be cached in memory. If this value is exceed requests to this
+ server will be denied with a 503 error code
+ </summary>
+ </member>
+ </members>
+</doc>