aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Sessions.VNCache
diff options
context:
space:
mode:
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.VNCache')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/IWebSessionIdFactory.cs26
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj47
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSession.cs103
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionIdFactoryImpl.cs96
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs122
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs101
6 files changed, 495 insertions, 0 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/IWebSessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/IWebSessionIdFactory.cs
new file mode 100644
index 0000000..81f44b8
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/IWebSessionIdFactory.cs
@@ -0,0 +1,26 @@
+
+using VNLib.Net.Http;
+
+namespace VNLib.Plugins.Essentials.Sessions.VNCache
+{
+ /// <summary>
+ /// Id factory for <see cref="WebSessionProvider"/>
+ /// </summary>
+ internal interface IWebSessionIdFactory: ISessionIdFactory
+ {
+ /// <summary>
+ /// The maxium amount of time a session is valid for. Sessions will be invalidated
+ /// after this time
+ /// </summary>
+ TimeSpan ValidFor { get; }
+
+ /// <summary>
+ /// Gets a new session-id for the connection and manipulates the entity as necessary
+ /// </summary>
+ /// <param name="entity">The connection to generate the new session for</param>
+ /// <returns>The new session-id</returns>
+ string GenerateSessionId(IHttpEvent entity);
+ }
+
+
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj
new file mode 100644
index 0000000..ecb80b1
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <Platforms>AnyCPU;x64</Platforms>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Version>1.0.0.1</Version>
+ <PlatformTarget>x64</PlatformTarget>
+ <SignAssembly>False</SignAssembly>
+ <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl>
+
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>True</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+ <Deterministic>True</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" />
+ <ProjectReference Include="..\VNLib.Plugins.Sessions.Cache.Client\VNLib.Plugins.Sessions.Cache.Client.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Endpoints\" />
+ </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.VNCache/WebSession.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSession.cs
new file mode 100644
index 0000000..a7a6f5e
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSession.cs
@@ -0,0 +1,103 @@
+using VNLib.Net.Http;
+using VNLib.Data.Caching;
+using VNLib.Net.Messaging.FBM.Client;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Sessions.Cache.Client;
+using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions;
+
+
+namespace VNLib.Plugins.Essentials.Sessions.VNCache
+{
+ internal class WebSession : RemoteSession
+ {
+ protected const ulong UPGRADE_MSK = 0b0000000000010000UL;
+
+ protected readonly Func<IHttpEvent, string, string> UpdateId;
+ private string? _oldId;
+
+ public WebSession(string sessionId, FBMClient client, TimeSpan backgroundTimeOut, Func<IHttpEvent, string, string> UpdateId)
+ : base(sessionId, client, backgroundTimeOut)
+ {
+ this.UpdateId = UpdateId;
+ }
+
+ protected override void IndexerSet(string key, string value)
+ {
+ //Set value
+ base.IndexerSet(key, value);
+ switch (key)
+ {
+ //Set the upgrade flag when token data is modified
+ case LOGIN_TOKEN_ENTRY:
+ case TOKEN_ENTRY:
+ Flags.Set(UPGRADE_MSK);
+ break;
+ }
+ }
+
+ public override async Task WaitAndLoadAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ //Wait for the session to load
+ await base.WaitAndLoadAsync(entity, cancellationToken);
+ //If the session is new, set to web mode
+ if (IsNew)
+ {
+ SessionType = SessionType.Web;
+ }
+ }
+
+ private async Task ProcessUpgradeAsync()
+ {
+ //Setup timeout cancellation for the update, to cancel it
+ using CancellationTokenSource cts = new(UpdateTimeout);
+ await Client.AddOrUpdateObjectAsync(_oldId!, SessionID, DataStore, cts.Token);
+ _oldId = null;
+ }
+
+ protected override ValueTask<Task?> UpdateResource(bool isAsync, IHttpEvent state)
+ {
+ Task? result = null;
+ //Check flags in priority level, Invalid is highest state priority
+ if (Flags.IsSet(INVALID_MSK))
+ {
+ //Clear all stored values
+ DataStore!.Clear();
+ //Reset ip-address
+ UserIP = state.Server.GetTrustedIp();
+ //Update created time
+ Created = DateTimeOffset.UtcNow;
+ //Init the new session-data
+ this.InitNewSession(state.Server);
+ //Restore session type
+ SessionType = SessionType.Web;
+ //generate new session-id and update the record in the store
+ _oldId = SessionID;
+ //Update the session-id
+ SessionID = UpdateId(state, _oldId);
+ //write update to server
+ result = Task.Run(ProcessUpgradeAsync);
+ }
+ else if (Flags.IsSet(UPGRADE_MSK | REGEN_ID_MSK))
+ {
+ //generate new session-id and update the record in the store
+ _oldId = SessionID;
+ //Update the session-id
+ SessionID = UpdateId(state, _oldId);
+ //Update created time
+ Created = DateTimeOffset.UtcNow;
+ //write update to server
+ result = Task.Run(ProcessUpgradeAsync);
+ }
+ else if (Flags.IsSet(MODIFIED_MSK))
+ {
+ //Send update to server
+ result = Task.Run(ProcessUpdateAsync);
+ }
+
+ //Clear all flags
+ Flags.ClearAll();
+
+ return ValueTask.FromResult<Task?>(null);
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionIdFactoryImpl.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionIdFactoryImpl.cs
new file mode 100644
index 0000000..001723a
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionIdFactoryImpl.cs
@@ -0,0 +1,96 @@
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Hashing;
+using VNLib.Net.Http;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Extensions;
+
+
+namespace VNLib.Plugins.Essentials.Sessions.VNCache
+{
+ /// <summary>
+ /// <see cref="IWebSessionIdFactory"/> implementation, using
+ /// http cookies as session id storage
+ /// </summary>
+ internal sealed class WebSessionIdFactoryImpl : IWebSessionIdFactory
+ {
+ public TimeSpan ValidFor { get; }
+
+ public string GenerateSessionId(IHttpEvent entity)
+ {
+ //Random hex hash
+ string cookie = RandomHash.GetRandomBase32(_tokenSize);
+
+ //Set the session id cookie
+ entity.Server.SetCookie(SessionCookieName, cookie, ValidFor, secure: true, httpOnly: true);
+
+ //return session-id value from cookie value
+ return ComputeSessionIdFromCookie(cookie);
+ }
+
+ bool ISessionIdFactory.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(SessionCookieName, out string? cookie) && (cookie.Length + SessionIdPrefix.Length) <= _bufferSize)
+ {
+ //Compute session id from token
+ sessionId = ComputeSessionIdFromCookie(cookie);
+
+ 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;
+ }
+ }
+
+ private readonly string SessionCookieName;
+ private readonly string SessionIdPrefix;
+ private readonly int _bufferSize;
+ private readonly int _tokenSize;
+
+ /// <summary>
+ /// Initialzies a new web session Id factory
+ /// </summary>
+ /// <param name="cookieSize">The size of the cookie in bytes</param>
+ /// <param name="sessionCookieName">The name of the session cookie</param>
+ /// <param name="sessionIdPrefix">The session-id internal prefix</param>
+ /// <param name="validFor">The time the session cookie is valid for</param>
+ public WebSessionIdFactoryImpl(uint cookieSize, string sessionCookieName, string sessionIdPrefix, TimeSpan validFor)
+ {
+ ValidFor = validFor;
+ SessionCookieName = sessionCookieName;
+ SessionIdPrefix = sessionIdPrefix;
+ _tokenSize = (int)cookieSize;
+ //Calc buffer size
+ _bufferSize = Math.Max(32, ((int)cookieSize * 3) + sessionIdPrefix.Length);
+ }
+
+
+ private string ComputeSessionIdFromCookie(string sessionId)
+ {
+ //Buffer to copy data to
+ using UnsafeMemoryHandle<char> buffer = Memory.UnsafeAlloc<char>(_bufferSize, true);
+
+ //Writer to accumulate data
+ ForwardOnlyWriter<char> writer = new(buffer.Span);
+
+ //Append prefix and session id
+ writer.Append(SessionIdPrefix);
+ writer.Append(sessionId);
+
+ //Compute base64 hash of token and
+ return ManagedHash.ComputeBase64Hash(writer.AsSpan(), HashAlg.SHA256);
+ }
+ }
+} \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs
new file mode 100644
index 0000000..fd725bf
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs
@@ -0,0 +1,122 @@
+using System;
+
+using VNLib.Net.Http;
+using VNLib.Net.Messaging.FBM.Client;
+using VNLib.Plugins.Sessions.Cache.Client;
+
+namespace VNLib.Plugins.Essentials.Sessions.VNCache
+{
+ /// <summary>
+ /// The implementation of a VNCache web based session
+ /// </summary>
+ internal sealed class WebSessionProvider : SessionCacheClient, ISessionProvider
+ {
+ static readonly TimeSpan BackgroundUpdateTimeout = TimeSpan.FromSeconds(10);
+
+ private readonly IWebSessionIdFactory factory;
+ private readonly uint MaxConnections;
+
+ /// <summary>
+ /// Initializes a new <see cref="WebSessionProvider"/>
+ /// </summary>
+ /// <param name="client">The cache client to make cache operations against</param>
+ /// <param name="maxCacheItems">The max number of items to store in cache</param>
+ /// <param name="maxWaiting">The maxium number of waiting session events before 503s are sent</param>
+ /// <param name="factory">The session-id factory</param>
+ public WebSessionProvider(FBMClient client, int maxCacheItems, uint maxWaiting, IWebSessionIdFactory factory) : base(client, maxCacheItems)
+ {
+ this.factory = factory;
+ MaxConnections = maxWaiting;
+ }
+
+ private string UpdateSessionId(IHttpEvent entity, string oldId)
+ {
+ //Generate and set a new sessionid
+ string newid = factory.GenerateSessionId(entity);
+ //Aquire lock on cache
+ lock (CacheLock)
+ {
+ //Change the cache lookup id
+ if (CacheTable.Remove(oldId, out RemoteSession? session))
+ {
+ CacheTable.Add(newid, session);
+ }
+ }
+ return newid;
+ }
+
+ protected override RemoteSession SessionCtor(string sessionId) => new WebSession(sessionId, Client, BackgroundUpdateTimeout, UpdateSessionId);
+
+
+ private uint _waitingCount;
+
+ public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ //Callback to close the session when the handle is closeed
+ static ValueTask HandleClosedAsync(ISession session, IHttpEvent entity)
+ {
+ return (session as SessionBase)!.UpdateAndRelease(true, entity);
+ }
+
+ try
+ {
+ //Get session id
+ if (!factory.TryGetSessionId(entity, out string? sessionId))
+ {
+ //Id not allowed/found, so do not attach a session
+ return SessionHandle.Empty;
+ }
+
+ //Limit max number of waiting clients
+ if (_waitingCount > MaxConnections)
+ {
+ //Set 503 for temporary unavail
+ entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
+ return new SessionHandle(null, FileProcessArgs.VirtualSkip, null);
+ }
+
+ RemoteSession session;
+
+ //Inc waiting count
+ Interlocked.Increment(ref _waitingCount);
+ try
+ {
+ //Recover the session
+ session = await GetSessionAsync(entity, sessionId, cancellationToken);
+ }
+ finally
+ {
+ //Dec on exit
+ Interlocked.Decrement(ref _waitingCount);
+ }
+
+ //If the session is new (not in cache), then overwrite the session id with a new one as user may have specified their own
+ if (session.IsNew)
+ {
+ session.RegenID();
+ }
+
+ //Make sure the session has not expired yet
+ if (session.Created.Add(factory.ValidFor) < DateTimeOffset.UtcNow)
+ {
+ //Invalidate the session, so its technically valid for this request, but will be cleared on this handle close cycle
+ session.Invalidate();
+ //Clear basic login status
+ session.Token = null;
+ session.UserID = null;
+ session.Privilages = 0;
+ session.SetLoginToken(null);
+ }
+ return new SessionHandle(session, HandleClosedAsync);
+ }
+ catch (SessionException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new SessionException("Exception raised while retreiving or loading Web session", ex);
+ }
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs
new file mode 100644
index 0000000..f72d1c5
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs
@@ -0,0 +1,101 @@
+using System.Text.Json;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Configuration;
+
+namespace VNLib.Plugins.Essentials.Sessions.VNCache
+{
+ public sealed class WebSessionProviderEntry : IRuntimeSessionProvider
+ {
+ const string VNCACHE_CONFIG_KEY = "vncache";
+ const string WEB_SESSION_CONFIG = "web";
+
+ private WebSessionProvider? _sessions;
+
+ public bool CanProcess(IHttpEvent entity)
+ {
+ //Web sessions can always be provided so long as cache is loaded
+ return _sessions != null;
+ }
+
+ public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ return _sessions!.GetSessionAsync(entity, cancellationToken);
+ }
+
+ void IRuntimeSessionProvider.Load(PluginBase plugin, ILogProvider localized)
+ {
+ //Try get vncache config element
+ IReadOnlyDictionary<string, JsonElement> cacheConfig = plugin.GetConfig(VNCACHE_CONFIG_KEY);
+
+ IReadOnlyDictionary<string, JsonElement> webSessionConfig = plugin.GetConfig(WEB_SESSION_CONFIG);
+
+ uint cookieSize = webSessionConfig["cookie_size"].GetUInt32();
+ string cookieName = webSessionConfig["cookie_name"].GetString() ?? throw new KeyNotFoundException($"Missing required element 'cookie_name' for config '{WEB_SESSION_CONFIG}'");
+ string cachePrefix = webSessionConfig["cache_prefix"].GetString() ?? throw new KeyNotFoundException($"Missing required element 'cache_prefix' for config '{WEB_SESSION_CONFIG}'");
+ TimeSpan validFor = webSessionConfig["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
+
+
+ //Init id factory
+ WebSessionIdFactoryImpl idFactory = new(cookieSize, cookieName, cachePrefix, validFor);
+
+ //Run client connection
+ _ = WokerDoWorkAsync(plugin, localized, idFactory, cacheConfig, webSessionConfig);
+ }
+
+
+ /*
+ * Starts and monitors the VNCache connection
+ */
+
+ private async Task WokerDoWorkAsync(
+ PluginBase plugin,
+ ILogProvider localized,
+ WebSessionIdFactoryImpl idFactory,
+ IReadOnlyDictionary<string, JsonElement> cacheConfig,
+ IReadOnlyDictionary<string, JsonElement> webSessionConfig)
+ {
+ //Init cache client
+ using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Memory.Shared);
+
+ try
+ {
+ int cacheLimit = (int)webSessionConfig["cache_size"].GetUInt32();
+ uint maxConnections = webSessionConfig["max_waiting_connections"].GetUInt32();
+
+ //Try loading config
+ await cache.LoadConfigAsync(plugin, cacheConfig);
+
+ //Init provider
+ _sessions = new(cache.Resource!, cacheLimit, maxConnections, idFactory);
+
+
+ localized.Information("Session provider loaded");
+
+ //Run and wait for exit
+ await cache.RunAsync(localized, plugin.UnloadToken);
+
+ }
+ catch (OperationCanceledException)
+ { }
+ catch (KeyNotFoundException e)
+ {
+ localized.Error("Missing required configuration variable for VnCache client: {0}", e.Message);
+ }
+ catch (Exception ex)
+ {
+ localized.Error(ex, "Cache client error occured in session provider");
+ }
+ finally
+ {
+ _sessions = null;
+ }
+
+ localized.Information("Cache client exited");
+ }
+ }
+} \ No newline at end of file