diff options
15 files changed, 277 insertions, 153 deletions
diff --git a/Libs/README.md b/Libs/README.md new file mode 100644 index 0000000..db899cb --- /dev/null +++ b/Libs/README.md @@ -0,0 +1,3 @@ +## VNLib.Plugins.Essentials.Sessions Helper libraries + +This folder contains helper libraries for the developing session providers.
\ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs index 271328a..c87c761 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs @@ -1,16 +1,21 @@ using System; using System.Net; +using System.Text.Json; using VNLib.Utils.Memory; +using VNLib.Hashing.IdentityUtility; using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Extensions.Validation; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Sql; - +using VNLib.Plugins.Extensions.Validation; namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints { + delegate Task<IOAuth2TokenResult?> CreateTokenImpl(HttpEntity ev, UserApplication application, CancellationToken cancellation = default); + /// <summary> /// Grants authorization to OAuth2 clients to protected resources /// with access tokens @@ -18,9 +23,11 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints internal sealed class AccessTokenEndpoint : ResourceEndpointBase { - private readonly Lazy<ITokenManager> TokenStore; + private readonly CreateTokenImpl CreateToken; private readonly Applications Applications; + private readonly Task<JsonDocument?> JWTVerificationKey; + //override protection settings to allow most connections to authenticate protected override ProtectionSettings EndpointProtectionSettings { get; } = new() { @@ -29,12 +36,14 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints VerifySessionCors = false }; - public AccessTokenEndpoint(string path, PluginBase pbase, Lazy<ITokenManager> tokenStore) + public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore, Task<JsonDocument?> verificationKey) { InitPathAndLog(path, pbase.Log); - TokenStore = tokenStore; + CreateToken = tokenStore; Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + JWTVerificationKey = verificationKey; } + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) { @@ -43,66 +52,117 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints { //process a refresh token } - //Check for grant_type parameter from the request body - if (!entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials")) - { - entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type"); - //Default to bad request - return VfReturnType.VirtualSkip; - } - //Get client id and secret (and make sure theyre not empty - if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) && - entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret)) + + //See if we have an application authorized with JWT + else if (entity.RequestArgs.IsArgumentSet("grant_type", "application")) { - if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(clientId)) - { - //respond with error message - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_id"); - return VfReturnType.VirtualSkip; - } - if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(secret)) + if(entity.RequestArgs.TryGetNonEmptyValue("token", out string? appJwt)) { - //respond with error message - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret"); - return VfReturnType.VirtualSkip; + //Try to get and verify the app + UserApplication? app = GetApplicationFromJwt(appJwt); + + //generate token + return await GenerateTokenAsync(entity, app); } - //Convert the clientid and secret to lowercase - clientId = clientId.ToLower(); - secret = secret.ToLower(); - //Convert secret to private string - PrivateString secretPv = new(secret, false); - //Get the application from apps store - UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); - if (app == null) - { - //App was not found or the credentials do not match - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The client credentials are invalid"); - return VfReturnType.VirtualSkip; - } - //Create a new session - IOAuth2TokenResult? result = await TokenStore.Value.CreateAccessTokenAsync(entity.Entity, app, entity.EventCancellation); - if (result == null) + } + + //Check for grant_type parameter from the request body + else if (entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials")) + { + //Get client id and secret (and make sure theyre not empty + if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) && + entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret)) { - entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); - return VfReturnType.VirtualSkip; + + if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(clientId)) + { + //respond with error message + entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_id"); + return VfReturnType.VirtualSkip; + } + if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(secret)) + { + //respond with error message + entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret"); + return VfReturnType.VirtualSkip; + } + + //Convert the clientid and secret to lowercase + clientId = clientId.ToLower(); + secret = secret.ToLower(); + + //Convert secret to private string that is unreferrenced + PrivateString secretPv = new(secret, false); + + //Get the application from apps store + UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); + + return await GenerateTokenAsync(entity, app); } - //Create the new response message - OauthTokenResponseMessage tokenMessage = new() - { - AccessToken = result.AccessToken, - - //set expired as seconds in int form - Expires = result.ExpiresSeconds, - RefreshToken = result.RefreshToken, - TokenType = result.TokenType - }; - //Respond with the token message - entity.CloseResponseJson(HttpStatusCode.OK, tokenMessage); + } + + entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type"); + //Default to bad request + return VfReturnType.VirtualSkip; + } + + private UserApplication? GetApplicationFromJwt(string jwtData) + { + //Not enabled + if (JWTVerificationKey.Result == null) + { + return null; + } + + //Parse application token + using JsonWebToken jwt = JsonWebToken.Parse(jwtData); + + //verify the application jwt + if (!jwt.VerifyFromJwk(JWTVerificationKey.Result.RootElement)) + { + return null; + } + + using JsonDocument doc = jwt.GetPayload(); + + //Get expiration time + DateTimeOffset exp = doc.RootElement.GetProperty("exp").GetDateTimeOffset(); + + //Check if token is expired + return exp < DateTimeOffset.UtcNow ? null : UserApplication.FromJwtDoc(doc.RootElement); + } + + + private async Task<VfReturnType> GenerateTokenAsync(HttpEntity entity, UserApplication? app) + { + if (app == null) + { + //App was not found or the credentials do not match + entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The credentials are invalid or do not exist"); return VfReturnType.VirtualSkip; + } + IOAuth2TokenResult? result = await CreateToken(entity, app, entity.EventCancellation); + + if (result == null) + { + entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); + return VfReturnType.VirtualSkip; } - //respond with error message - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The request was missing required arguments"); + + //Create the new response message + OauthTokenResponseMessage tokenMessage = new() + { + AccessToken = result.AccessToken, + IdToken = result.IdentityToken, + //set expired as seconds in int form + Expires = result.ExpiresSeconds, + RefreshToken = result.RefreshToken, + TokenType = result.TokenType + }; + + //Respond with the token message + entity.CloseResponseJson(HttpStatusCode.OK, tokenMessage); return VfReturnType.VirtualSkip; } } diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs index 9a65d62..9eb9928 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs @@ -1,7 +1,7 @@ using System; using VNLib.Net.Http; -using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Sessions.Cache.Client; namespace VNLib.Plugins.Essentials.Sessions.OAuth diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs index e15c6e4..89b36ad 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs @@ -5,6 +5,7 @@ using VNLib.Net.Http; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Sessions.OAuth; using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints; using VNLib.Plugins.Extensions.Loading; @@ -43,11 +44,12 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth string tokenEpPath = oauth2Config["token_path"].GetString() ?? throw new KeyNotFoundException($"Missing required 'token_path' in '{OAUTH2_CONFIG_KEY}' config"); - //TODO fix with method that will wait until cache is actually loaded - Lazy<ITokenManager> lazyTokenMan = new(() => _sessions!, false); + //Optional application jwt token + Task<JsonDocument?> jwtTokenSecret = plugin.TryGetSecretAsync("application_token_key") + .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result)); //Init auth endpoint - AccessTokenEndpoint authEp = new(tokenEpPath, plugin, lazyTokenMan); + AccessTokenEndpoint authEp = new(tokenEpPath, plugin, CreateTokenDelegateAsync, jwtTokenSecret); //route auth endpoint plugin.Route(authEp); @@ -56,14 +58,21 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth plugin.Route<RevocationEndpoint>(); //Run - _ = WokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + _ = CacheWokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + } + + private async Task<IOAuth2TokenResult?> CreateTokenDelegateAsync(HttpEntity entity, UserApplication app, CancellationToken cancellation) + { + return await _sessions!.CreateAccessTokenAsync(entity, app, cancellation).ConfigureAwait(false); } /* * Starts and monitors the VNCache connection */ - private async Task WokerDoWorkAsync(PluginBase plugin, ILogProvider localized, IReadOnlyDictionary<string, JsonElement> cacheConfig, IReadOnlyDictionary<string, JsonElement> oauth2Config) + private async Task CacheWokerDoWorkAsync(PluginBase plugin, ILogProvider localized, + IReadOnlyDictionary<string, JsonElement> cacheConfig, + IReadOnlyDictionary<string, JsonElement> oauth2Config) { //Init cache client using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Utils.Memory.Memory.Shared); @@ -89,7 +98,6 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth //Schedule cleanup interval with the plugin scheduler plugin.ScheduleInterval(_sessions, cleanupInterval); - localized.Information("Session provider loaded"); //Run and wait for exit diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs index 2b23721..1058653 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs @@ -1,12 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; -using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; using VNLib.Hashing; using VNLib.Net.Http; -using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Sessions.Cache.Client; using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions; @@ -90,7 +89,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth string sessionId = ComputeSessionIdFromToken(token); //Clear buffer - Utils.Memory.Memory.InitializeBlock(mem.Span); + Memory.InitializeBlock(mem.Span); //Return sessid result return new(sessionId, token, null); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs index d938641..79d3789 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs @@ -9,9 +9,10 @@ using VNLib.Net.Http; using VNLib.Data.Caching; using VNLib.Data.Caching.Exceptions; using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Sessions.Cache.Client; using VNLib.Plugins.Essentials.Oauth; using VNLib.Plugins.Essentials.Oauth.Tokens; -using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Extensions.Loading.Events; namespace VNLib.Plugins.Essentials.Sessions.OAuth @@ -24,8 +25,10 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth { private static readonly SessionHandle NotFoundHandle = new(null, FileProcessArgs.NotFound, null); - static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10); + + private static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10); + private readonly IOauthSessionIdFactory factory; private readonly TokenStore TokenStore; @@ -103,26 +106,26 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth } } ///<inheritdoc/> - async Task<IOAuth2TokenResult?> ITokenManager.CreateAccessTokenAsync(IHttpEvent ev, UserApplication app, CancellationToken cancellation) + public async Task<IOAuth2TokenResult?> CreateAccessTokenAsync(HttpEntity ev, UserApplication app, CancellationToken cancellation) { //Get a new session for the current connection TokenAndSessionIdResult ids = factory.GenerateTokensAndId(); //try to insert token into the store, may fail if max has been reached - if (await TokenStore.InsertTokenAsync(ids.SessionId, app.Id, ids.RefreshToken, factory.MaxTokensPerApp, cancellation) != ERRNO.SUCCESS) + if (await TokenStore.InsertTokenAsync(ids.SessionId, app.Id!, ids.RefreshToken, factory.MaxTokensPerApp, cancellation) != ERRNO.SUCCESS) { return null; } //Create new session from the session id RemoteSession session = SessionCtor(ids.SessionId); - await session.WaitAndLoadAsync(ev, cancellation); + await session.WaitAndLoadAsync(ev.Entity, cancellation); try { //Init new session - factory.InitNewSession(session, app, ev); + factory.InitNewSession(session, app, ev.Entity); } finally { - await session.UpdateAndRelease(false, ev); + await session.UpdateAndRelease(false, ev.Entity); } //Init new token result to pass to client return new OAuth2TokenResult() @@ -137,14 +140,20 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth ///<inheritdoc/> Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation) { - throw new NotImplementedException(); + return TokenStore.RevokeTokensAsync(tokens, cancellation); } ///<inheritdoc/> Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) { - throw new NotImplementedException(); + return TokenStore.RevokeTokenAsync(appId, cancellation); } - + + + /* + * Interval for remving expired tokens + */ + + ///<inheritdoc/> async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) { //Calculate valid token time diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs index fd725bf..bf2b28e 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs @@ -109,6 +109,10 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache } return new SessionHandle(session, HandleClosedAsync); } + catch (OperationCanceledException) + { + throw; + } catch (SessionException) { throw; 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; + } + } + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj index 0c12cec..52404be 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj @@ -21,7 +21,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.44" /> + <PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.48" /> <PackageReference Include="RestSharp" Version="108.0.2" /> </ItemGroup> diff --git a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs index cdc0dc1..06ebfc3 100644 --- a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs +++ b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs @@ -22,13 +22,13 @@ using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Hashing.IdentityUtility; using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Endpoints; using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Events; using VNLib.Plugins.Extensions.Loading.Configuration; using VNLib.Net.Rest.Client; - #nullable enable namespace VNLib.Plugins.Cache.Broker.Endpoints @@ -188,7 +188,7 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints //Copy input stream to buffer await inputStream.CopyToAsync(buffer, 4096, Memory.Shared); //Parse jwt - return JsonWebToken.Parse(buffer.AsSpan()); + return JsonWebToken.ParseRaw(buffer.AsSpan()); } protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) @@ -200,7 +200,7 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints { alg.ImportSubjectPublicKeyInfo(CachePubKey.Result, out _); //Verify the jwt - if (!jwt.Verify(alg, SignatureHashAlg)) + if (!jwt.Verify(alg, in SignatureHashAlg)) { entity.CloseResponse(HttpStatusCode.Unauthorized); return VfReturnType.VirtualSkip; diff --git a/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs b/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs index e80be77..d501fca 100644 --- a/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs +++ b/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs @@ -9,11 +9,10 @@ using System.Security.Cryptography; using VNLib.Data.Caching.Extensions; using VNLib.Hashing.IdentityUtility; +using VNLib.Plugins.Essentials.Endpoints; using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Extensions.Loading; -#nullable enable - namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints { internal class BrokerHeartBeat : ResourceEndpointBase diff --git a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs index fc4de30..0385601 100644 --- a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs +++ b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs @@ -18,6 +18,7 @@ using VNLib.Net.Messaging.FBM.Server; using VNLib.Data.Caching.Extensions; using VNLib.Data.Caching.ObjectCache; using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Essentials.Endpoints; using VNLib.Plugins.Essentials.Extensions; |