diff options
23 files changed, 583 insertions, 217 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs index 5c09697..a159456 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs @@ -49,7 +49,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints { private readonly CreateTokenImpl CreateToken; - private readonly Applications Applications; + private readonly ApplicationStore Applications; private readonly Task<JsonDocument?> JWTVerificationKey; diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs index 3c65056..d981f69 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs @@ -34,13 +34,13 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints /// An OAuth2 authorized endpoint for revoking the access token /// held by the current connection /// </summary> - [ConfigurationName("oauth2")] + [ConfigurationName("o2_revocation_endpoint")] internal class RevocationEndpoint : O2EndpointBase { public RevocationEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) { - string? path = config["revocation_path"].GetString(); + string? path = config["path"].GetString(); InitPathAndLog(path, pbase.Log); } diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2AuthenticationPluginEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2AuthenticationPluginEntry.cs new file mode 100644 index 0000000..4a48f8b --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2AuthenticationPluginEntry.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: O2AuthenticationPluginEntry.cs +* +* O2AuthenticationPluginEntry.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.OAuth 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.OAuth 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.Utils.Logging; +using VNLib.Plugins.Essentials.Sessions.Runtime; + + +namespace VNLib.Plugins.Essentials.Sessions.Oauth +{ + public sealed class O2AuthenticationPluginEntry : PluginBase + { + public override string PluginName => "Essentials.Oauth.Authentication"; + + private readonly O2SessionProviderEntry SessionProvider = new(); + + protected override void OnLoad() + { + try + { + //Load the session provider, that will only load the endpoints + (SessionProvider as IRuntimeSessionProvider).Load(this, Log); + } + catch(KeyNotFoundException kne) + { + Log.Error("Missing required configuration keys {err}", kne.Message); + } + } + + protected override void OnUnLoad() + { + Log.Information("Plugin unloaded"); + } + + protected override void ProcessHostCommand(string cmd) + { + throw new NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs index e7c7f29..07b6530 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs @@ -27,6 +27,7 @@ using System.Text.Json; using VNLib.Net.Http; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Oauth.Tokens; using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Sessions.OAuth; using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints; @@ -35,10 +36,11 @@ using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Extensions.Loading.Sql; using VNLib.Plugins.Extensions.Loading.Events; using VNLib.Plugins.Essentials.Sessions.Runtime; -using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Data.Caching.Extensions; namespace VNLib.Plugins.Essentials.Sessions.Oauth { + public sealed class O2SessionProviderEntry : IRuntimeSessionProvider { const string VNCACHE_CONFIG_KEY = "vncache"; @@ -49,7 +51,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth bool IRuntimeSessionProvider.CanProcess(IHttpEvent entity) { //If authorization header is set try to process as oauth2 session - return entity.Server.Headers.HeaderSet(System.Net.HttpRequestHeader.Authorization); + return _sessions != null && entity.Server.Headers.HeaderSet(System.Net.HttpRequestHeader.Authorization); } ValueTask<SessionHandle> ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) @@ -65,23 +67,30 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth IReadOnlyDictionary<string, JsonElement> oauth2Config = plugin.GetConfig(OAUTH2_CONFIG_KEY); - string tokenEpPath = oauth2Config["token_path"].GetString() ?? throw new KeyNotFoundException($"Missing required 'token_path' in '{OAUTH2_CONFIG_KEY}' config"); - //Optional application jwt token Task<JsonDocument?> jwtTokenSecret = plugin.TryGetSecretAsync("application_token_key") - .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result)); + .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result), TaskScheduler.Default); - //Init auth endpoint - AccessTokenEndpoint authEp = new(tokenEpPath, plugin, CreateTokenDelegateAsync, jwtTokenSecret); + //Access token endpoint is optional + if (oauth2Config.TryGetValue("token_path", out JsonElement el)) + { + //Init auth endpoint + AccessTokenEndpoint authEp = new(el.GetString()!, plugin, CreateTokenDelegateAsync, jwtTokenSecret); - //route auth endpoint - plugin.Route(authEp); - - //Route revocation endpoint - plugin.Route<RevocationEndpoint>(); + //route auth endpoint + plugin.Route(authEp); + } + + //Optional revocation endpoint + if (plugin.HasConfigForType<RevocationEndpoint>()) + { + //Route revocation endpoint + plugin.Route<RevocationEndpoint>(); + } //Run - _ = CacheWokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + _ = plugin.DeferTask(() => CacheWokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config), 100); + } private async Task<IOAuth2TokenResult?> CreateTokenDelegateAsync(HttpEntity entity, UserApplication app, CancellationToken cancellation) @@ -133,6 +142,10 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth { localized.Error("Missing required configuration variable for VnCache client: {0}", e.Message); } + catch(FBMServerNegiationException fne) + { + localized.Error("Failed to negotiate connection with cache server {reason}", fne.Message); + } catch (Exception ex) { localized.Error(ex, "Cache client error occured in session provider"); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs index 5f9fc7e..d698c81 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs @@ -27,6 +27,7 @@ using System.Net; using Microsoft.EntityFrameworkCore; +using VNLib.Net.Http; using VNLib.Utils; using VNLib.Utils.Logging; using VNLib.Data.Caching; @@ -37,8 +38,6 @@ using VNLib.Plugins.Essentials.Oauth; using VNLib.Plugins.Essentials.Oauth.Tokens; using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Extensions.Loading.Events; -using VNLib.Net.Http.Core; -using VNLib.Net.Http; namespace VNLib.Plugins.Essentials.Sessions.OAuth { @@ -200,8 +199,10 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth {} catch (Exception ex) { - errors ??= new(); - errors.Add(ex); + errors = new() + { + ex + }; } } if (errors?.Count > 0) diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj index 4cfcd86..d75a1c0 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj @@ -4,20 +4,38 @@ <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <PlatformTarget>x64</PlatformTarget> - <GenerateDocumentationFile>False</GenerateDocumentationFile> + <GenerateDocumentationFile>True</GenerateDocumentationFile> <Authors>Vaughn Nugent</Authors> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <EnableDynamicLoading>true</EnableDynamicLoading> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <Version>1.0.1.1</Version> + <PackageProjectUrl>https://www.vaughnugent.com</PackageProjectUrl> + <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + <Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> </Target> - + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\Web Plugins\DevPlugins\RuntimeAssets\$(TargetName)" /q > nul" /> + </Target> <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib\Http\VNLib.Net.Http.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib\Plugins\src\VNLib.Plugins.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" /> + <ProjectReference Include="..\..\..\DataCaching\VNLib.Data.Caching.Extensions\VNLib.Data.Caching.Extensions.csproj" /> + <ProjectReference Include="..\..\..\DataCaching\VNLib.Data.Caching\src\VNLib.Data.Caching.csproj" /> <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading.Sql\VNLib.Plugins.Extensions.Loading.Sql.csproj" /> <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Validation\VNLib.Plugins.Extensions.Validation.csproj" /> diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj index 5924b93..d72d6b9 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj @@ -4,29 +4,13 @@ <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> + <Version>1.0.1.1</Version> <SignAssembly>False</SignAssembly> - <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> - </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> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> <ItemGroup> diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs index fd14098..3b348d2 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs @@ -81,6 +81,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime /// Loads required configuration variables from the config store and /// intializes the interal client /// </summary> + /// <param name="pbase"></param> /// <param name="config">A dictionary of configuration varables</param> /// <exception cref="KeyNotFoundException"></exception> public async Task LoadConfigAsync(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) @@ -107,10 +108,11 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime _client = new(conf); //Add the configuration - _client.UseBroker(brokerUri) - .ImportBrokerPublicKey(brokerPub) - .ImportClientPrivateKey(privKey) - .UseTls(brokerUri.Scheme == Uri.UriSchemeHttps); + _client.GetCacheConfiguration() + .WithBroker(brokerUri) + .ImportVerificationKey(brokerPub) + .ImportSigningKey(privKey) + .WithTls(brokerUri.Scheme == Uri.UriSchemeHttps); //Zero the key memory Memory.InitializeBlock(privKey.AsSpan()); @@ -139,7 +141,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime { Log.Debug("Discovering cluster nodes in broker"); //Get server list - servers = await Resource.DiscoverNodesAsync(cancellationToken); + servers = await Resource.DiscoverCacheNodesAsync(cancellationToken); break; } catch (HttpRequestException re) when (re.InnerException is SocketException) @@ -150,6 +152,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime { Log.Warn("Failed to get server list from broker, reason {r}", ex.Message); } + //Gen random ms delay int randomMsDelay = RandomNumberGenerator.GetInt32(1000, 2000); await Task.Delay(randomMsDelay, cancellationToken); @@ -160,13 +163,18 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime await Task.Delay(RetryInterval, cancellationToken); continue; } - //select random server from the list of servers - ActiveServer selected = servers!.SelectRandom(); + try { - Log.Debug("Connecting to server {server}", selected.ServerId); - //Try to connect to server - await Resource.ConnectAndWaitForExitAsync(selected, cancellationToken); + Log.Debug("Connecting to random cache server"); + + //Connect to a random server + ActiveServer selected = await Resource.ConnectToRandomCacheAsync(cancellationToken); + Log.Debug("Connected to cache server {s}", selected.ServerId); + + //Wait for disconnect + await Resource.WaitForExitAsync(cancellationToken); + Log.Debug("Cache server disconnected"); } catch (WebSocketException wse) @@ -176,7 +184,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime } catch (HttpRequestException he) when (he.InnerException is SocketException) { - Log.Debug("Failed to connect to recommended server {server}", selected.ServerId); + Log.Debug("Failed to connect to random cache server server"); //Continue next loop continue; } 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 index 25b5223..8c3e23a 100644 --- 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 @@ -4,31 +4,16 @@ <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> + <Version>1.0.1.1</Version> <SignAssembly>False</SignAssembly> - <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> + <PackageProjectUrl>https://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> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> <ItemGroup> @@ -37,6 +22,9 @@ <ItemGroup> <ProjectReference Include="..\..\..\..\VNLib\Http\VNLib.Net.Http.csproj" /> + <ProjectReference Include="..\..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" /> + <ProjectReference Include="..\..\..\DataCaching\VNLib.Data.Caching.Extensions\VNLib.Data.Caching.Extensions.csproj" /> + <ProjectReference Include="..\..\..\DataCaching\VNLib.Data.Caching\src\VNLib.Data.Caching.csproj" /> <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> <ProjectReference Include="..\..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" /> <ProjectReference Include="..\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" /> @@ -46,5 +34,9 @@ <Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> </Target> + + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\Web Plugins\DevPlugins\RuntimeAssets\$(TargetName)" /q > nul" /> + </Target> </Project> diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs index cf46c2b..382717b 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs @@ -30,6 +30,7 @@ using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Essentials.Sessions.Runtime; +using VNLib.Data.Caching.Extensions; namespace VNLib.Plugins.Essentials.Sessions.VNCache { @@ -63,12 +64,11 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache 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); + _ = plugin.DeferTask(() => WokerDoWorkAsync(plugin, localized, idFactory, cacheConfig, webSessionConfig)); } @@ -110,6 +110,10 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache { localized.Error("Missing required configuration variable for VnCache client: {0}", e.Message); } + catch (FBMServerNegiationException fne) + { + localized.Error("Failed to negotiate connection with cache server {reason}", fne.Message); + } catch (Exception ex) { localized.Error(ex, "Cache client error occured in session provider"); 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 index ca90155..0d3cb40 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj @@ -2,23 +2,28 @@ <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> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + </PropertyGroup> + + <!-- Resolve nuget dll files and store them in the output dir --> + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <EnableDynamicLoading>true</EnableDynamicLoading> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> - <DocumentationFile></DocumentationFile> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> + <ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions> <ItemGroup> <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> @@ -37,9 +42,13 @@ <ProjectReference Include="..\..\..\PluginBase\VNLib.Plugins.PluginBase.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\RuntimeAssets\$(TargetName)" /E /Y /R" /> </Target> + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\Web Plugins\DevPlugins\RuntimeAssets\$(TargetName)" /q > nul" /> + </Target> </Project> diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs index e2a9725..d2d4200 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs @@ -59,7 +59,7 @@ namespace VNLib.Plugins.Sessions.Cache.Client /// </summary> protected Dictionary<string, string>? DataStore; - public RemoteSession(string sessionId, FBMClient client, TimeSpan backgroundTimeOut) + protected RemoteSession(string sessionId, FBMClient client, TimeSpan backgroundTimeOut) { SessionID = sessionId; UpdateTimeout = backgroundTimeOut; diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs index 40dafa6..8eed404 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs @@ -74,7 +74,7 @@ namespace VNLib.Plugins.Sessions.Cache.Client /// </summary> /// <param name="client"></param> /// <param name="maxCacheItems">The maximum number of sessions to keep in memory</param> - public SessionCacheClient(FBMClient client, int maxCacheItems) + protected SessionCacheClient(FBMClient client, int maxCacheItems) { MaxLoadedEntires = maxCacheItems; CacheLock = new(); 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 4b4cd89..fc99bbf 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 @@ -1,15 +1,25 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <Platforms>AnyCPU;x64</Platforms> + <TargetFramework>net6.0</TargetFramework> <Authors>Vaughn Nugent</Authors> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> - <Version>1.0.0.1</Version> + <Version>1.0.1.1</Version> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <PropertyGroup> <DocumentationFile></DocumentationFile> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AnalysisLevel>latest-all</AnalysisLevel> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> <ItemGroup> diff --git a/Plugins/CacheBroker/CacheBroker.csproj b/Plugins/CacheBroker/CacheBroker.csproj index 47a48cf..97a7e30 100644 --- a/Plugins/CacheBroker/CacheBroker.csproj +++ b/Plugins/CacheBroker/CacheBroker.csproj @@ -2,7 +2,10 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> - <Platforms>AnyCPU;x64</Platforms> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <RootNamespace>VNLib.Plugins.Cache.Broker</RootNamespace> + <Authors>Vaughn Nugent</Authors> + <Version>1.0.1.2</Version> </PropertyGroup> <ItemGroup> @@ -26,10 +29,18 @@ <PropertyGroup> <!--Enable dynamic loading--> <EnableDynamicLoading>true</EnableDynamicLoading> - <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> - <RootNamespace>VNLib.Plugins.Cache.Broker</RootNamespace> - <Authors>Vaughn Nugent</Authors> - <Version>1.0.0.1</Version> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AnalysisLevel>latest-all</AnalysisLevel> + + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> </PropertyGroup> diff --git a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs index 2c454bf..340c47e 100644 --- a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs +++ b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs @@ -51,6 +51,7 @@ using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Events; using VNLib.Net.Rest.Client; +using VaultSharp.V1.SystemBackend; #nullable enable @@ -101,7 +102,14 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints private readonly Task<byte[]> ClientPubKey; private readonly Task<byte[]> BrokerPrivateKey; - protected override ProtectionSettings EndpointProtectionSettings { get; } + //Loosen up protection settings since this endpoint is not desinged for browsers or sessions + protected override ProtectionSettings EndpointProtectionSettings { get; } = new() + { + BrowsersOnly = false, + CrossSiteDenied = false, + SessionsRequired = false, + VerifySessionCors = false, + }; public BrokerRegistrationEndpoint(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config) { @@ -112,31 +120,22 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints { _ = secret.Result ?? throw new InvalidOperationException("Broker private key not found in vault"); return Convert.FromBase64String(secret.Result); - }); + }, TaskScheduler.Default); CachePubKey = plugin.TryGetSecretAsync("cache_public_key").ContinueWith((Task<string?> secret) => { _ = secret.Result ?? throw new InvalidOperationException("Cache public key not found in vault"); return Convert.FromBase64String(secret.Result); - }); + }, TaskScheduler.Default); ClientPubKey = plugin.TryGetSecretAsync("client_public_key").ContinueWith((Task<string?> secret) => { _ = secret.Result ?? throw new InvalidOperationException("Client public key not found in vault"); return Convert.FromBase64String(secret.Result); - }); - + }, TaskScheduler.Default); InitPathAndLog(path, plugin.Log); - //Loosen up protection settings since this endpoint is not desinged for browsers or sessions - EndpointProtectionSettings = new() - { - SessionsRequired = false, - BrowsersOnly = false, - CrossSiteDenied = false, - }; - ListLock = new(); ActiveServers = new(); } @@ -149,7 +148,8 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints //Verify with the client's pub key using (ECDsa alg = ECDsa.Create(DefaultCurve)) { - alg.ImportSubjectPublicKeyInfo(ClientPubKey.Result, out _); + ReadOnlyMemory<byte> client = await ClientPubKey; + alg.ImportSubjectPublicKeyInfo(client.Span, out _); //Verify with client public key if (!jwt.Verify(alg, in SignatureHashAlg)) { @@ -176,20 +176,14 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints //Sign the jwt using the broker key using(ECDsa alg = ECDsa.Create(DefaultCurve)) { - alg.ImportPkcs8PrivateKey(BrokerPrivateKey.Result, out _); + ReadOnlyMemory<byte> brokerPrivate = await BrokerPrivateKey; + + alg.ImportPkcs8PrivateKey(brokerPrivate.Span, out _); response.Sign(alg, in SignatureHashAlg, 128); } - //Alloc output buffer - int bufSize = response.ByteSize * 2; - - using UnsafeMemoryHandle<char> charBuf = Memory.UnsafeAlloc<char>(bufSize, true); - - //compile jwt - ERRNO count = response.Compile(charBuf); - - entity.CloseResponse(HttpStatusCode.OK, ContentType.Text, charBuf.Span[..(int)count]); + entity.CloseResponse(HttpStatusCode.OK, ContentType.Text, response.DataBuffer); return VfReturnType.VirtualSkip; } catch (KeyNotFoundException) @@ -217,11 +211,13 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) { //Parse jwt - using JsonWebToken? jwt = await entity.ParseFileAsAsync(ParseJwtAsync); + using JsonWebToken? jwt = await entity.ParseFileAsAsync(ParseJwtAsync) ?? throw new Exception(""); //Verify with the cache server's pub key using (ECDsa alg = ECDsa.Create(DefaultCurve)) { - alg.ImportSubjectPublicKeyInfo(CachePubKey.Result, out _); + ReadOnlyMemory<byte> cache = await CachePubKey; + + alg.ImportSubjectPublicKeyInfo(cache.Span, out _); //Verify the jwt if (!jwt.Verify(alg, in SignatureHashAlg)) { @@ -237,7 +233,7 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints using JsonDocument requestBody = jwt.GetPayload(); //Get request keys - string? serverId = requestBody.RootElement.GetProperty("server_id").GetString(); + string? serverId = requestBody.RootElement.GetProperty("sub").GetString(); string? hostname = requestBody.RootElement.GetProperty("address").GetString(); string? token = requestBody.RootElement.GetProperty("token").GetString(); @@ -366,7 +362,9 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints //Sign the jwt using the broker key using (ECDsa alg = ECDsa.Create(DefaultCurve)) { - alg.ImportPkcs8PrivateKey(BrokerPrivateKey.Result, out _); + ReadOnlyMemory<byte> broker = await BrokerPrivateKey; + + alg.ImportPkcs8PrivateKey(broker.Span, out _); //Sign with broker key jwt.Sign(alg, in SignatureHashAlg, 128); } diff --git a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs index dc94825..fee1ea7 100644 --- a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs +++ b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs @@ -33,7 +33,10 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Collections.Concurrent; +using VNLib.Net.Http; +using VNLib.Hashing; using VNLib.Utils.Async; +using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Hashing.IdentityUtility; using VNLib.Net.Messaging.FBM; @@ -50,7 +53,6 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints { class ConnectEndpoint : ResourceEndpointBase { - const int MAX_RECV_BUF_SIZE = 1000 * 1024; const int MIN_RECV_BUF_SIZE = 8 * 1024; const int MAX_HEAD_BUF_SIZE = 2048; @@ -60,8 +62,9 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints const int MAX_EVENT_QUEUE_SIZE = 10000; const int MAX_RESPONSE_BUFFER_SIZE = 10 * 1024; - private static readonly Encoding FBMHeaderEncoding = Helpers.DefaultEncoding; + private static readonly TimeSpan AuthTokenExpiration = TimeSpan.FromSeconds(30); + private readonly string AudienceLocalServerId; private readonly ObjectCacheStore Store; private readonly PluginBase Pbase; @@ -71,7 +74,13 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints public uint ConnectedClients => _connectedClients; - protected override ProtectionSettings EndpointProtectionSettings { get; } + //Loosen up protection settings + protected override ProtectionSettings EndpointProtectionSettings { get; } = new() + { + BrowsersOnly = false, + SessionsRequired = false, + CrossSiteDenied = false + }; public ConnectEndpoint(string path, ObjectCacheStore store, PluginBase pbase) { @@ -79,18 +88,107 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints Store = store;//Load client public key to verify signed messages Pbase = pbase; - StatefulEventQueue = new(StringComparer.OrdinalIgnoreCase); + //Start the queue worker - _ = ChangeWorkerAsync().ConfigureAwait(false); + _ = pbase.DeferTask(() => ChangeWorkerAsync(pbase.UnloadToken), 10); + + AudienceLocalServerId = Guid.NewGuid().ToString("N"); + } + + /* + * Used as a client negotiation and verification request + * + * The token created during this request will be verified by the client + * and is already verified by this server, will be passed back + * via the authorization header during the websocket upgrade. + * + * This server must verify the authenticity of the returned token + * + * The tokens are very short lived as requests are intended to be made + * directly after verification + */ + + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + //Parse jwt from authoriation + string? jwtAuth = entity.Server.Headers[HttpRequestHeader.Authorization]; + if (string.IsNullOrWhiteSpace(jwtAuth)) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + string? nodeId = null; + string? challenge = null; + + // Parse jwt + using (JsonWebToken jwt = JsonWebToken.Parse(jwtAuth)) + { + //Get the client public key + byte[] clientPub = await GetClientPubAsync(); + + //Init sig alg + using ECDsa verAlg = ECDsa.Create(FBMDataCacheExtensions.CacheCurve); + //Import client pub key + verAlg.ImportSubjectPublicKeyInfo(clientPub, out _); + + //verify signature for client + if (!jwt.Verify(verAlg, in FBMDataCacheExtensions.CacheJwtAlgorithm)) + { + Log.Information("Client signature verification failed"); + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + //Recover json body + using JsonDocument doc = jwt.GetPayload(); + if (doc.RootElement.TryGetProperty("sub", out JsonElement servIdEl)) + { + nodeId = servIdEl.GetString(); + } + if (doc.RootElement.TryGetProperty("chl", out JsonElement challengeEl)) + { + challenge = challengeEl.GetString(); + } + } - //Loosen up protection settings - EndpointProtectionSettings = new() + Log.Debug("Received negotiation request from node {node}", nodeId); + + //Verified, now we can create an auth message with a short expiration + using JsonWebToken auth = new(); + auth.WriteHeader(FBMDataCacheExtensions.JwtMessageHeader.Span); + auth.InitPayloadClaim() + .AddClaim("aud", AudienceLocalServerId) + .AddClaim("exp", DateTimeOffset.UtcNow.Add(AuthTokenExpiration).ToUnixTimeSeconds()) + .AddClaim("nonce", RandomHash.GetRandomHex(8)) + .AddClaim("chl", challenge) + //Specify the server's node id if set + .AddClaim("sub", nodeId) + //Add negotiaion args + .AddClaim(FBMClient.REQ_HEAD_BUF_QUERY_ARG, MAX_HEAD_BUF_SIZE) + .AddClaim(FBMClient.REQ_RECV_BUF_QUERY_ARG, MAX_RECV_BUF_SIZE) + .AddClaim(FBMClient.REQ_MAX_MESS_QUERY_ARG, MAX_MESSAGE_SIZE) + .CommitClaims(); + + //Sign the auth message + ECDsa sigAlg = ECDsa.Create(FBMDataCacheExtensions.CacheCurve); + byte[] cachePrivate = await GetCachePrivateKeyAsync(); + try { - BrowsersOnly = false, - SessionsRequired = false, - CrossSiteDenied = false - }; + sigAlg.ImportPkcs8PrivateKey(cachePrivate, out _); + //sign jwt + auth.Sign(sigAlg, in FBMDataCacheExtensions.CacheJwtAlgorithm, 256); + } + finally + { + Memory.InitializeBlock(cachePrivate.AsSpan()); + sigAlg.Dispose(); + } + + //Close response + entity.CloseResponse(HttpStatusCode.OK, ContentType.Text, auth.DataBuffer); + return VfReturnType.VirtualSkip; } private async Task<byte[]> GetClientPubAsync() @@ -99,15 +197,31 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints return Convert.FromBase64String(brokerPubKey); } + private async Task<byte[]> GetCachePubAsync() + { + string? brokerPubKey = await Pbase.TryGetSecretAsync("cache_public_key") ?? throw new KeyNotFoundException("Missing required secret : client_public_key"); + + return Convert.FromBase64String(brokerPubKey); + } + private async Task<byte[]> GetCachePrivateKeyAsync() + { + string? cachePrivate = await Pbase.TryGetSecretAsync("cache_private_key") ?? throw new KeyNotFoundException("Missing required secret : client_public_key"); + + byte[] data = Convert.FromBase64String(cachePrivate); + + Memory.UnsafeZeroMemory<char>(cachePrivate); + + return data; + } - private async Task ChangeWorkerAsync() + private async Task ChangeWorkerAsync(CancellationToken cancellation) { try { //Listen for changes while (true) { - ChangeEvent ev = await Store.EventQueue.DequeueAsync(Pbase.UnloadToken); + ChangeEvent ev = await Store.EventQueue.DequeueAsync(cancellation); //Add event to queues foreach (AsyncQueue<ChangeEvent> queue in StatefulEventQueue.Values) { @@ -139,45 +253,70 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints { try { - //Parse jwt from authoriation + //Parse jwt from authorization string? jwtAuth = entity.Server.Headers[HttpRequestHeader.Authorization]; if (string.IsNullOrWhiteSpace(jwtAuth)) { entity.CloseResponse(HttpStatusCode.Unauthorized); return VfReturnType.VirtualSkip; } + string? nodeId = null; //Parse jwt using (JsonWebToken jwt = JsonWebToken.Parse(jwtAuth)) { - //Get the client public key - byte[] clientPub = await GetClientPubAsync(); - - //Init sig alg - using ECDsa sigAlg = ECDsa.Create(FBMDataCacheExtensions.CacheCurve); - //Import client pub key - sigAlg.ImportSubjectPublicKeyInfo(clientPub, out _); - //verify signature for client - if (!jwt.Verify(sigAlg, FBMDataCacheExtensions.CacheJwtAlgorithm)) + //Init sig alg, we will verify that the token was signed by this server + using (ECDsa sigAlg = ECDsa.Create(FBMDataCacheExtensions.CacheCurve)) { - entity.CloseResponse(HttpStatusCode.Unauthorized); - return VfReturnType.VirtualSkip; + //Get the cache public key + byte[] cachePub = await GetCachePubAsync(); + + sigAlg.ImportSubjectPublicKeyInfo(cachePub, out _); + + //verify signature against the cache public key, since this server have signed it + if (!jwt.Verify(sigAlg, in FBMDataCacheExtensions.CacheJwtAlgorithm)) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } } + //Recover json body using JsonDocument doc = jwt.GetPayload(); - if (doc.RootElement.TryGetProperty("server_id", out JsonElement servIdEl)) + + //Verify audience, expiration + + if (!doc.RootElement.TryGetProperty("aud", out JsonElement audEl) + || AudienceLocalServerId.Equals(audEl.GetString(), StringComparison.OrdinalIgnoreCase)) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + if (!doc.RootElement.TryGetProperty("exp", out JsonElement expEl) + || DateTimeOffset.FromUnixTimeSeconds(expEl.GetInt64()) < DateTimeOffset.UtcNow) + { + entity.CloseResponse(HttpStatusCode.Unauthorized); + return VfReturnType.VirtualSkip; + } + + //The node id is optional and stored in the 'sub' field + if (doc.RootElement.TryGetProperty("sub", out JsonElement servIdEl)) { nodeId = servIdEl.GetString(); } } + //Get query config suggestions from the client string recvBufCmd = entity.QueryArgs[FBMClient.REQ_RECV_BUF_QUERY_ARG]; string maxHeaderCharCmd = entity.QueryArgs[FBMClient.REQ_HEAD_BUF_QUERY_ARG]; string maxMessageSizeCmd = entity.QueryArgs[FBMClient.REQ_MAX_MESS_QUERY_ARG]; + //Parse recv buffer size int recvBufSize = int.TryParse(recvBufCmd, out int rbs) ? rbs : MIN_RECV_BUF_SIZE; int maxHeadBufSize = int.TryParse(maxHeaderCharCmd, out int hbs) ? hbs : MIN_HEAD_BUF_SIZE; int maxMessageSize = int.TryParse(maxMessageSizeCmd, out int mxs) ? mxs : MIN_MESSAGE_SIZE; + AsyncQueue<ChangeEvent>? nodeQueue = null; //The connection may be a caching server node, so get its node-id if (!string.IsNullOrWhiteSpace(nodeId)) @@ -212,7 +351,9 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints MaxResponseBufferSize = Math.Min(maxMessageSize, MAX_RESPONSE_BUFFER_SIZE), SyncQueue = nodeQueue }; + Log.Debug("Client recv buffer suggestion {recv}, header buffer size {head}, response buffer size {r}", recvBufCmd, maxHeaderCharCmd, state.MaxResponseBufferSize); + //Accept socket and pass state object entity.AcceptWebSocket(WebsocketAcceptedAsync, state); return VfReturnType.VirtualSkip; @@ -239,7 +380,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints RecvBufferSize = state.RecvBufferSize, ResponseBufferSize = state.MaxResponseBufferSize, MaxHeaderBufferSize = state.MaxHeaderBufferSize, - HeaderEncoding = FBMHeaderEncoding, + HeaderEncoding = Helpers.DefaultEncoding, }; //Listen for requests diff --git a/Plugins/SessionCacheServer/ObjectCacheServer.csproj b/Plugins/SessionCacheServer/ObjectCacheServer.csproj index 5598acc..01b33f3 100644 --- a/Plugins/SessionCacheServer/ObjectCacheServer.csproj +++ b/Plugins/SessionCacheServer/ObjectCacheServer.csproj @@ -2,17 +2,21 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> + <Nullable>enable</Nullable> <Authors>Vaughn Nugent</Authors> - <Version>1.0.0.1</Version> + <Version>1.0.1.1</Version> <RootNamespace>VNLib.Plugins.Essentials.Sessions.Server</RootNamespace> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> - <Platforms>AnyCPU;x64</Platforms> + </PropertyGroup> <!-- Resolve nuget dll files and store them in the output dir --> <PropertyGroup> <EnableDynamicLoading>true</EnableDynamicLoading> - <Nullable>enable</Nullable> + <GenerateDocumentationFile>False</GenerateDocumentationFile> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AnalysisLevel>latest-all</AnalysisLevel> + </PropertyGroup> <ItemGroup> <Compile Remove="liveplugin2\**" /> diff --git a/Plugins/SessionCacheServer/ObjectCacheServerEntry.cs b/Plugins/SessionCacheServer/ObjectCacheServerEntry.cs index 6c969ba..17d8ba5 100644 --- a/Plugins/SessionCacheServer/ObjectCacheServerEntry.cs +++ b/Plugins/SessionCacheServer/ObjectCacheServerEntry.cs @@ -29,7 +29,6 @@ using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; -using System.Diagnostics; using System.Net.Sockets; using System.Threading.Tasks; using System.Collections.Generic; @@ -50,6 +49,7 @@ using VNLib.Plugins.Cache.Broker.Endpoints; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Essentials.Sessions.Server.Endpoints; +using VNLib.Utils.Memory.Caching; namespace VNLib.Plugins.Essentials.Sessions.Server { @@ -72,16 +72,16 @@ namespace VNLib.Plugins.Essentials.Sessions.Server string swapDir = PluginConfig.GetProperty("swap_dir").GetString() ?? throw new KeyNotFoundException("Missing required key 'swap_dir' for config"); int cacheSize = PluginConfig.GetProperty("max_cache").GetInt32(); string connectPath = PluginConfig.GetProperty("connect_path").GetString() ?? throw new KeyNotFoundException("Missing required element 'connect_path' for config 'cluster'"); - TimeSpan cleanupInterval = PluginConfig.GetProperty("cleanup_interval_sec").GetTimeSpan(TimeParseType.Seconds); - TimeSpan validFor = PluginConfig.GetProperty("valid_for_sec").GetTimeSpan(TimeParseType.Seconds); + //TimeSpan cleanupInterval = PluginConfig.GetProperty("cleanup_interval_sec").GetTimeSpan(TimeParseType.Seconds); + //TimeSpan validFor = PluginConfig.GetProperty("valid_for_sec").GetTimeSpan(TimeParseType.Seconds); int maxMessageSize = PluginConfig.GetProperty("max_blob_size").GetInt32(); - TimeSpan initialCleanupDelay = TimeSpan.FromSeconds(2); //Init dir DirectoryInfo dir = new(swapDir); dir.Create(); //Init cache listener, single threaded reader ObjectCacheStore CacheListener = new(dir, cacheSize, Log, CacheHeap, true); + //Init connect endpoint { //Init connect endpoint @@ -96,19 +96,20 @@ namespace VNLib.Plugins.Essentials.Sessions.Server //Route the broker endpoint BrokerHeartBeat brokerEp = new(() => BrokerHeartBeatToken!, mre, new Uri(brokerAddress), this); Route(brokerEp); + //start registration - _ = RegisterServerAsync(mre) - .ConfigureAwait(false); + _ = this.DeferTask(() => RegisterServerAsync(mre), 200); } + //Setup cluster worker { //Get pre-configured fbm client config for caching FBMClientConfig conf = FBMDataCacheExtensions.GetDefaultConfig(CacheHeap, maxMessageSize, this.IsDebug() ? Log : null); //Start Client runner - _ = RunClientAsync(CacheListener, new Uri(brokerAddress), conf) - .ConfigureAwait(false); + _ = this.DeferTask(() => RunClientAsync(CacheListener, new Uri(brokerAddress), conf), 300); } + //Load a cache broker to the current server if the config is defined { if(this.HasConfigForType<BrokerRegistrationEndpoint>()) @@ -116,13 +117,10 @@ namespace VNLib.Plugins.Essentials.Sessions.Server this.Route<BrokerRegistrationEndpoint>(); } } - //Init timer and fire immediatly to cleanup - Timer CleanupTimer = new((object? state) => OnCleanupElapsed(state, validFor), CacheListener, initialCleanupDelay, cleanupInterval); - + void Cleanup() { CacheHeap.Dispose(); - CleanupTimer.Dispose(); CacheListener.Dispose(); } @@ -148,24 +146,6 @@ namespace VNLib.Plugins.Essentials.Sessions.Server Log.Information("Plugin unloaded"); } - private void OnCleanupElapsed(object? state, TimeSpan validFor) - { - try - { - ObjectCacheStore listener = state as ObjectCacheStore; - Stopwatch sw = new(); - sw.Start(); - //Cleanup - //await listener.CleanupExpiredAsync(validFor); - sw.Stop(); - Log.Debug("Expired cache records cleaned in {ms} ms", sw.Elapsed.TotalMilliseconds); - } - catch (Exception ex) - { - Log.Error(ex); - } - } - #region Registration private async Task RegisterServerAsync(ManualResetEvent keepaliveWait) @@ -175,12 +155,12 @@ namespace VNLib.Plugins.Essentials.Sessions.Server //Get the broker config element IReadOnlyDictionary<string, JsonElement> clusterConfig = this.GetConfig("cluster"); - Uri brokerAddress = new(clusterConfig["broker_address"].GetString()); + //Server id is just dns name for now string serverId = Dns.GetHostName(); int heartBeatDelayMs = clusterConfig["heartbeat_timeout_sec"].GetInt32() * 1000; - string connectPath = PluginConfig.GetProperty("connect_path").GetString(); + string? connectPath = PluginConfig.GetProperty("connect_path").GetString(); //Get the port of the primary webserver int port; @@ -195,14 +175,27 @@ namespace VNLib.Plugins.Essentials.Sessions.Server //If a certificate is specified, tls is enabled on the port usingTls = firstHost.TryGetProperty("cert", out _); } - - //Try to get the cache private key - string base64Priv = await this.TryGetSecretAsync("cache_private_key") ?? throw new KeyNotFoundException("Failed to load the cache private key"); - - byte[] privKey = Convert.FromBase64String(base64Priv); - //Init url builder for payload, see if tls is enabled - Uri connectAddress = new UriBuilder(usingTls ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, Dns.GetHostName(), port, connectPath).Uri; + using BrokerRegistrationRequest request = new(); + { + string addr = clusterConfig["broker_address"].GetString() ?? throw new KeyNotFoundException("Missing required key 'broker_address' for config 'cluster'"); + + //Try to get the cache private key + string base64Priv = await this.TryGetSecretAsync("cache_private_key") ?? throw new KeyNotFoundException("Failed to load the cache private key"); + + ReadOnlyMemory<byte> privKey = Convert.FromBase64String(base64Priv); + + //Init url builder for payload, see if tls is enabled + Uri connectAddress = new UriBuilder(usingTls ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, Dns.GetHostName(), port, connectPath).Uri; + + request.WithBroker(new(addr)) + .WithRegistrationAddress(connectAddress.ToString()) + .WithNodeId(serverId) + .WithPrivateKey(privKey.Span); + //Wipe memory + Memory.UnsafeZeroMemory<char>(base64Priv); + Memory.UnsafeZeroMemory(privKey); + } while (true) { @@ -210,11 +203,13 @@ namespace VNLib.Plugins.Essentials.Sessions.Server { //Gen a random reg token before registering BrokerHeartBeatToken = RandomHash.GetRandomHex(32); + //Assign new hb token + request.WithHeartbeatToken(BrokerHeartBeatToken); - Log.Information("Registering with cache broker {addr}, with node-id {id}", brokerAddress, serverId); + Log.Information("Registering with cache broker {addr}, with node-id {id}", request.BrokerAddress, serverId); //Register with the broker - await FBMDataCacheExtensions.ResgisterWithBrokerAsync(brokerAddress, privKey, connectAddress.ToString(), serverId, BrokerHeartBeatToken); + await FBMDataCacheExtensions.ResgisterWithBrokerAsync(request); Log.Debug("Successfully registered with cache broker"); @@ -357,15 +352,12 @@ namespace VNLib.Plugins.Essentials.Sessions.Server await Task.Delay(noServerDelay, UnloadToken); continue; } + //Select servers that are not the current server and are not already being monitored - IEnumerable<ActiveServer> serversToConnectTo = from s in - (from ss in servers - where ss.ServerId != nodeId - select ss) - where !ActiveServers.ContainsKey(s.ServerId) - select s; + IEnumerable<ActiveServer> serversToConnectTo = servers.Where(s => s.ServerId != nodeId) + .Where(s => !ActiveServers.ContainsKey(s.ServerId!)); //Connect to servers - foreach(ActiveServer server in serversToConnectTo) + foreach (ActiveServer server in serversToConnectTo) { _ = RunSyncTaskAsync(server, ActiveServers, cacheStore, clientConf, clientPrivKey, nodeId) .ConfigureAwait(false); @@ -439,10 +431,16 @@ namespace VNLib.Plugins.Essentials.Sessions.Server client.ReturnRequest(modRequest); } } - - string challenge = RandomHash.GetRandomBase64(24); + + //Configure cache + client.GetCacheConfiguration() + .ImportVerificationKey(privateKey.Span) + .ImportVerificationKey(null) + .WithNodeId(nodeId) //set nodeid since were listening for changes + .WithTls(false); + //Connect to the server - await client.ConnectAsync(server.HostName, privateKey, challenge, nodeId, false, UnloadToken); + await client.ConnectToCacheAsync(server, UnloadToken); //Wroker task callback method async Task BgWorkerAsync() diff --git a/Plugins/SessionProvider/SessionProvider - Backup (1).csproj b/Plugins/SessionProvider/SessionProvider - Backup (1).csproj new file mode 100644 index 0000000..7eb7022 --- /dev/null +++ b/Plugins/SessionProvider/SessionProvider - Backup (1).csproj @@ -0,0 +1,60 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RootNamespace>VNLib.Plugins.Essentials.Sessions</RootNamespace> + <AssemblyName>SessionProvider</AssemblyName> + <PackageId>SessionProvider</PackageId> + <Authors>Vaughn Nugent</Authors> + <Product>SessionProvider</Product> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <Version>1.0.3.1</Version> + <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> + <Platforms>AnyCPU;x64</Platforms> + </PropertyGroup> + + <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> + + <!-- Resolve nuget dll files and store them in the output dir --> + <PropertyGroup> + <EnableDynamicLoading>true</EnableDynamicLoading> + <AssemblyVersion>1.0.2.1</AssemblyVersion> + <FileVersion>1.0.2.1</FileVersion> + <Nullable>enable</Nullable> + </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\Utils\src\VNLib.Utils.csproj" /> + <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> + <ProjectReference Include="..\..\Libs\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="SessionProvider.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + + <Target Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\$(TargetName)" /E /Y /R" /> + </Target> + + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\Web Plugins\DevPlugins\$(TargetName)" > nul" /> + </Target> + +</Project> diff --git a/Plugins/SessionProvider/SessionProvider - Backup.csproj b/Plugins/SessionProvider/SessionProvider - Backup.csproj new file mode 100644 index 0000000..d20a524 --- /dev/null +++ b/Plugins/SessionProvider/SessionProvider - Backup.csproj @@ -0,0 +1,56 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RootNamespace>VNLib.Plugins.Essentials.Sessions</RootNamespace> + <AssemblyName>SessionProvider</AssemblyName> + <PackageId>SessionProvider</PackageId> + <Authors>Vaughn Nugent</Authors> + <Product>SessionProvider</Product> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <Version>1.0.3.1</Version> + <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> + <Platforms>AnyCPU;x64</Platforms> + </PropertyGroup> + + <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> + + <!-- Resolve nuget dll files and store them in the output dir --> + <PropertyGroup> + <EnableDynamicLoading>true</EnableDynamicLoading> + <AssemblyVersion>1.0.2.1</AssemblyVersion> + <FileVersion>1.0.2.1</FileVersion> + <Nullable>enable</Nullable> + </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\Utils\src\VNLib.Utils.csproj" /> + <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> + <ProjectReference Include="..\..\Libs\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="SessionProvider.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + + <Target Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> diff --git a/Plugins/SessionProvider/SessionProvider.csproj b/Plugins/SessionProvider/SessionProvider.csproj index d20a524..4ed7dc1 100644 --- a/Plugins/SessionProvider/SessionProvider.csproj +++ b/Plugins/SessionProvider/SessionProvider.csproj @@ -2,6 +2,7 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> + <Nullable>enable</Nullable> <RootNamespace>VNLib.Plugins.Essentials.Sessions</RootNamespace> <AssemblyName>SessionProvider</AssemblyName> <PackageId>SessionProvider</PackageId> @@ -9,8 +10,7 @@ <Product>SessionProvider</Product> <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <Version>1.0.3.1</Version> - <PackageProjectUrl>www.vaughnnugent.com/resources</PackageProjectUrl> - <Platforms>AnyCPU;x64</Platforms> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> </PropertyGroup> <ItemGroup> @@ -27,16 +27,10 @@ <!-- Resolve nuget dll files and store them in the output dir --> <PropertyGroup> <EnableDynamicLoading>true</EnableDynamicLoading> - <AssemblyVersion>1.0.2.1</AssemblyVersion> - <FileVersion>1.0.2.1</FileVersion> - <Nullable>enable</Nullable> - </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> - <Deterministic>False</Deterministic> - </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> - <Deterministic>False</Deterministic> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" /> <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> @@ -53,4 +47,8 @@ <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\$(TargetName)" /E /Y /R" /> </Target> + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\Web Plugins\DevPlugins\$(TargetName)" /q > nul" /> + </Target> + </Project> diff --git a/Plugins/SessionProvider/null b/Plugins/SessionProvider/null new file mode 100644 index 0000000..48b2100 --- /dev/null +++ b/Plugins/SessionProvider/null @@ -0,0 +1 @@ +F:\Programming\Web Plugins\DevPlugins\SessionProvider\*, Are you sure (Y/N)? |