diff options
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.VNCache/src')
6 files changed, 561 insertions, 0 deletions
diff --git a/libs/VNLib.Plugins.Sessions.VNCache/src/IWebSessionIdFactory.cs b/libs/VNLib.Plugins.Sessions.VNCache/src/IWebSessionIdFactory.cs new file mode 100644 index 0000000..f284e00 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/IWebSessionIdFactory.cs @@ -0,0 +1,57 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.VNCache +* File: IWebSessionIdFactory.cs +* +* IWebSessionIdFactory.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Diagnostics.CodeAnalysis; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Sessions.VNCache +{ + /// <summary> + /// Id factory for <see cref="WebSessionProvider"/> + /// </summary> + internal interface IWebSessionIdFactory + { + /// <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); + + /// <summary> + /// Attempts to recover a session id from + /// </summary> + /// <param name="entity">The entity to get the session-id for</param> + /// <param name="sessionId">The found ID for the session if accepted</param> + /// <returns>True if a session id was found or set for the session</returns> + bool TryGetSessionId(IHttpEvent entity, [NotNullWhen(true)] out string? sessionId); + } +} diff --git a/libs/VNLib.Plugins.Sessions.VNCache/src/VNLib.Plugins.Sessions.VNCache.csproj b/libs/VNLib.Plugins.Sessions.VNCache/src/VNLib.Plugins.Sessions.VNCache.csproj new file mode 100644 index 0000000..ddddab5 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/VNLib.Plugins.Sessions.VNCache.csproj @@ -0,0 +1,39 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2023 Vaughn Nugent</Copyright> + <Version>1.0.1.1</Version> + <SignAssembly>False</SignAssembly> + <PackageProjectUrl>https://www.vaughnugent.com/resources/software</PackageProjectUrl> + + <EnableDynamicLoading>true</EnableDynamicLoading> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + + <ItemGroup> + <Folder Include="Endpoints\" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\DataCaching\lib\VNLib.Plugins.Extensions.VNCache\src\VNLib.Plugins.Extensions.VNCache.csproj" /> + <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" /> + <ProjectReference Include="..\..\VNLib.Plugins.Sessions.Cache.Client\src\VNLib.Plugins.Sessions.Cache.Client.csproj" /> + </ItemGroup> + + <Target Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)" /q > nul" /> + </Target> + +</Project> diff --git a/libs/VNLib.Plugins.Sessions.VNCache/src/WebSession.cs b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSession.cs new file mode 100644 index 0000000..8bd17ef --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSession.cs @@ -0,0 +1,125 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.VNCache +* File: WebSession.cs +* +* WebSession.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Sessions.Cache.Client; +using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions; + +namespace VNLib.Plugins.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, IRemoteCacheStore 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.Sessions.VNCache/src/WebSessionIdFactoryImpl.cs b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionIdFactoryImpl.cs new file mode 100644 index 0000000..9480dc6 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionIdFactoryImpl.cs @@ -0,0 +1,119 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.VNCache +* File: WebSessionIdFactoryImpl.cs +* +* WebSessionIdFactoryImpl.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +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.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); + } + + 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(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.Sessions.VNCache/src/WebSessionProvider.cs b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProvider.cs new file mode 100644 index 0000000..44fb936 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProvider.cs @@ -0,0 +1,139 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.VNCache +* File: WebSessionProvider.cs +* +* WebSessionProvider.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Sessions.Cache.Client; + +namespace VNLib.Plugins.Sessions.VNCache +{ + /// <summary> + /// The implementation of a VNCache web based session + /// </summary> + [ConfigurationName("web")] + 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(IRemoteCacheStore 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, Store, BackgroundUpdateTimeout, UpdateSessionId); + + 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 (WaitingConnections > MaxConnections) + { + //Set 503 for temporary unavail + entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable); + return new SessionHandle(null, FileProcessArgs.VirtualSkip, null); + } + + //Get session + RemoteSession session = await GetSessionAsync(entity, sessionId, cancellationToken); + + //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 (OperationCanceledException) + { + throw; + } + catch (SessionException) + { + throw; + } + catch (Exception ex) + { + throw new SessionException("Exception raised while retreiving or loading Web session", ex); + } + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProviderEntry.cs b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProviderEntry.cs new file mode 100644 index 0000000..623975f --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.VNCache/src/WebSessionProviderEntry.cs @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.VNCache +* File: WebSessionProviderEntry.cs +* +* WebSessionProviderEntry.cs is part of VNLib.Plugins.Essentials.Sessions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json; + +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Data.Caching; +using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.VNCache; +using VNLib.Plugins.Essentials.Sessions; + +namespace VNLib.Plugins.Sessions.VNCache +{ + public sealed class WebSessionProviderEntry : ISessionProvider + { + const string WEB_SESSION_CONFIG = "web"; + + private WebSessionProvider? _sessions; + + //Web sessions can always be provided so long as cache is loaded + public bool CanProcess(IHttpEvent entity) => _sessions != null; + + ValueTask<SessionHandle> ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) + { + return _sessions!.GetSessionAsync(entity, cancellationToken); + } + + public void Load(PluginBase plugin, ILogProvider localized) + { + //Try get vncache config element + IReadOnlyDictionary<string, JsonElement> webSessionConfig = plugin.GetConfigForType<WebSessionProvider>(); + + 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}'"); + int cacheLimit = (int)webSessionConfig["cache_size"].GetUInt32(); + uint maxConnections = webSessionConfig["max_waiting_connections"].GetUInt32(); + TimeSpan validFor = webSessionConfig["valid_for_sec"].GetTimeSpan(TimeParseType.Seconds); + + //Init id factory + WebSessionIdFactoryImpl idFactory = new(cookieSize, cookieName, cachePrefix, validFor); + + //Get shared global-cache + IGlobalCacheProvider globalCache = plugin.GetGlobalCache(localized); + + //Create cache store from global cache + GlobalCacheStore cacheStore = new(globalCache); + + //Init provider + _sessions = new(cacheStore, cacheLimit, maxConnections, idFactory); + + //Load and run cached sessions on deferred task lib + _ = plugin.DeferTask(() => _sessions.CleanupExpiredSessionsAsync(localized, plugin.UnloadToken), 1000); + + localized.Information("Session provider loaded"); + } + } +}
\ No newline at end of file |