diff options
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions')
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 "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\SessionProviders\$(TargetName)" /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> |