diff options
author | vnugent <public@vaughnnugent.com> | 2023-01-12 17:47:40 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-01-12 17:47:40 -0500 |
commit | b75668b164d398b99ee942beced06aa27ef65a50 (patch) | |
tree | c1faf6df3caa78083dcc38eb1a7247e456bbe754 /lib/VNLib.Data.Caching.Extensions/src | |
parent | cea64e619e714f6dbe51d37ca8329b58d8c271fb (diff) |
Large project reorder and consolidation
Diffstat (limited to 'lib/VNLib.Data.Caching.Extensions/src')
7 files changed, 957 insertions, 0 deletions
diff --git a/lib/VNLib.Data.Caching.Extensions/src/ActiveServer.cs b/lib/VNLib.Data.Caching.Extensions/src/ActiveServer.cs new file mode 100644 index 0000000..1e28947 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/ActiveServer.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: ActiveServer.cs +* +* ActiveServer.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json.Serialization; + +namespace VNLib.Data.Caching.Extensions +{ + public class ActiveServer + { + [JsonPropertyName("address")] + public string? HostName { get; set; } + [JsonPropertyName("server_id")] + public string? ServerId { get; set; } + [JsonPropertyName("ip_address")] + public string? Ip { get; set; } + ///<inheritdoc/> + public override int GetHashCode() => ServerId!.GetHashCode(StringComparison.OrdinalIgnoreCase); + ///<inheritdoc/> + public override bool Equals(object? obj) => obj is ActiveServer s && GetHashCode() == s.GetHashCode(); + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/BrokerRegistrationRequest.cs b/lib/VNLib.Data.Caching.Extensions/src/BrokerRegistrationRequest.cs new file mode 100644 index 0000000..4ed4ab7 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/BrokerRegistrationRequest.cs @@ -0,0 +1,113 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: BrokerRegistrationRequest.cs +* +* BrokerRegistrationRequest.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions 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; +using VNLib.Hashing.IdentityUtility; + + +namespace VNLib.Data.Caching.Extensions +{ + /// <summary> + /// A broker registration request message in a fluent api + /// format. This message may be disposed when no longer in use + /// </summary> + public sealed class BrokerRegistrationRequest : VnDisposeable + { + private bool ownsKey; + private ReadOnlyJsonWebKey? SigningKey; + + /// <summary> + /// The cache server node id + /// </summary> + public string? NodeId { get; private set; } + /// <summary> + /// The broker server's address + /// </summary> + public Uri? BrokerAddress { get; private set; } + /// <summary> + /// The security token used by the broker server to + /// authenticate during heartbeat connections + /// </summary> + public string? HeartbeatToken { get; private set; } + /// <summary> + /// The address for remote clients to use to + /// connect to this server + /// </summary> + public string? RegistrationAddress { get; private set; } + + /// <summary> + /// Recovers the private key from the supplied certificate + /// </summary> + /// <param name="jwk">The private key used to sign messages</param> + /// <param name="ownsKey">A value that indicates if the current instance owns the key</param> + /// <returns></returns> + /// <exception cref="ArgumentException"></exception> + public BrokerRegistrationRequest WithSigningKey(ReadOnlyJsonWebKey jwk, bool ownsKey) + { + this.ownsKey = ownsKey; + SigningKey = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + + public BrokerRegistrationRequest WithBroker(Uri brokerUri) + { + BrokerAddress = brokerUri; + return this; + } + + public BrokerRegistrationRequest WithRegistrationAddress(string address) + { + RegistrationAddress = address; + return this; + } + + public BrokerRegistrationRequest WithHeartbeatToken(string token) + { + HeartbeatToken = token; + return this; + } + + public BrokerRegistrationRequest WithNodeId(string nodeId) + { + NodeId = nodeId; + return this; + } + + internal void SignJwt(JsonWebToken jwt) + { + jwt.SignFromJwk(SigningKey); + } + + internal IReadOnlyDictionary<string, string?> JsonHeader => SigningKey!.JwtHeader; + + ///<inheritdoc/> + protected override void Free() + { + if (ownsKey) + { + SigningKey?.Dispose(); + } + } + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/ClientCacheConfiguration.cs b/lib/VNLib.Data.Caching.Extensions/src/ClientCacheConfiguration.cs new file mode 100644 index 0000000..d9cca33 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/ClientCacheConfiguration.cs @@ -0,0 +1,133 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: ClientCacheConfiguration.cs +* +* ClientCacheConfiguration.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Security.Cryptography; + +using VNLib.Hashing; +using VNLib.Hashing.IdentityUtility; + +namespace VNLib.Data.Caching.Extensions +{ + /// <summary> + /// A fluent api configuration object for configuring a <see cref="FBMClient"/> + /// to connect to cache servers. + /// </summary> + public sealed class ClientCacheConfiguration + { + internal ReadOnlyJsonWebKey? SigningKey { get; private set; } + internal ReadOnlyJsonWebKey? VerificationKey { get; private set; } + internal ReadOnlyJsonWebKey? BrokerVerificationKey { get; private set; } + + internal string ServerChallenge { get; } = RandomHash.GetRandomBase32(24); + internal string? NodeId { get; set; } + internal Uri? BrokerAddress { get; set; } + internal bool UseTls { get; set; } + internal ActiveServer[]? CacheServers { get; set; } + + internal IReadOnlyDictionary<string, string?> JwtHeader => SigningKey!.JwtHeader; + + /// <summary> + /// Imports the private key used to sign messages + /// </summary> + /// <param name="jwk">The <see cref="ReadOnlyJsonWebKey"/> with a private key loaded</param> + /// <returns>Chainable fluent object</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="CryptographicException"></exception> + public ClientCacheConfiguration WithSigningCertificate(ReadOnlyJsonWebKey jwk) + { + SigningKey = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + + /// <summary> + /// Imports the public key used to verify messages from the remote server + /// </summary> + /// <param name="jwk">The <see cref="ReadOnlyJsonWebKey"/> public key only used for message verification</param> + /// <returns>Chainable fluent object</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="CryptographicException"></exception> + public ClientCacheConfiguration WithVerificationKey(ReadOnlyJsonWebKey jwk) + { + VerificationKey = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + + public ClientCacheConfiguration WithBrokerVerificationKey(ReadOnlyJsonWebKey jwk) + { + BrokerVerificationKey = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + + /// <summary> + /// Specifies if all connections should be using TLS + /// </summary> + /// <param name="useTls">A value that indicates if connections should use TLS</param> + /// <returns>Chainable fluent object</returns> + public ClientCacheConfiguration WithTls(bool useTls) + { + UseTls = useTls; + return this; + } + /// <summary> + /// Specifies the broker address to discover cache nodes from + /// </summary> + /// <param name="brokerAddress">The address of the server broker</param> + /// <returns>Chainable fluent object</returns> + /// <exception cref="ArgumentNullException"></exception> + public ClientCacheConfiguration WithBroker(Uri brokerAddress) + { + this.BrokerAddress = brokerAddress ?? throw new ArgumentNullException(nameof(brokerAddress)); + return this; + } + + /// <summary> + /// Specifies the current server's cluster node id. If this + /// is a server connection attempting to listen for changes on the + /// remote server, this id must be set and unique + /// </summary> + /// <param name="nodeId">The cluster node id of the current server</param> + /// <returns>Chainable fluent object</returns> + /// <exception cref="ArgumentNullException"></exception> + public ClientCacheConfiguration WithNodeId(string nodeId) + { + this.NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId)); + return this; + } + + internal void SignJwt(JsonWebToken jwt) + { + jwt.SignFromJwk(SigningKey); + } + + internal bool VerifyCache(JsonWebToken jwt) + { + return jwt.VerifyFromJwk(VerificationKey); + } + + internal bool VerifyBroker(JsonWebToken jwt) + { + return jwt.VerifyFromJwk(BrokerVerificationKey); + } + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/FBMDataCacheExtensions.cs b/lib/VNLib.Data.Caching.Extensions/src/FBMDataCacheExtensions.cs new file mode 100644 index 0000000..ebdfd5b --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/FBMDataCacheExtensions.cs @@ -0,0 +1,469 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: FBMDataCacheExtensions.cs +* +* FBMDataCacheExtensions.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Text; +using System.Security; +using System.Text.Json; +using System.Security.Cryptography; +using System.Runtime.CompilerServices; + +using RestSharp; + +using VNLib.Net.Http; +using VNLib.Hashing; +using VNLib.Hashing.IdentityUtility; +using VNLib.Utils.Memory; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Net.Rest.Client; +using VNLib.Net.Messaging.FBM; +using VNLib.Net.Messaging.FBM.Client; + +namespace VNLib.Data.Caching.Extensions +{ + + /// <summary> + /// Provides extension methods for FBM data caching using + /// cache servers and brokers + /// </summary> + public static class FBMDataCacheExtensions + { + /// <summary> + /// The websocket sub-protocol to use when connecting to cache servers + /// </summary> + public const string CACHE_WS_SUB_PROCOL = "object-cache"; + /// <summary> + /// The default cache message header size + /// </summary> + public const int MAX_FBM_MESSAGE_HEADER_SIZE = 1024; + + private static readonly RestClientPool ClientPool = new(2,new RestClientOptions() + { + MaxTimeout = 10 * 1000, + FollowRedirects = false, + Encoding = Encoding.UTF8, + AutomaticDecompression = DecompressionMethods.All, + ThrowOnAnyError = true, + }); + + private static readonly ConditionalWeakTable<FBMClient, ClientCacheConfiguration> ClientCacheConfig = new(); + + /// <summary> + /// Gets a <see cref="FBMClientConfig"/> preconfigured object caching + /// protocl + /// </summary> + /// <param name="heap">The client buffer heap</param> + /// <param name="maxMessageSize">The maxium message size (in bytes)</param> + /// <param name="debugLog">An optional debug log</param> + /// <returns>A preconfigured <see cref="FBMClientConfig"/> for object caching</returns> + public static FBMClientConfig GetDefaultConfig(IUnmangedHeap heap, int maxMessageSize, ILogProvider? debugLog = null) + { + return new() + { + BufferHeap = heap, + MaxMessageSize = maxMessageSize * 2, + RecvBufferSize = maxMessageSize, + MessageBufferSize = maxMessageSize, + + MaxHeaderBufferSize = MAX_FBM_MESSAGE_HEADER_SIZE, + SubProtocol = CACHE_WS_SUB_PROCOL, + + HeaderEncoding = Helpers.DefaultEncoding, + + KeepAliveInterval = TimeSpan.FromSeconds(30), + + DebugLog = debugLog + }; + } + + private static void LogDebug(this FBMClient client, string message) + { + client.Config.DebugLog?.Debug("{debug}: {data}", "[CACHE]", message); + } + + /// <summary> + /// Contacts the cache broker to get a list of active servers to connect to + /// </summary> + /// <param name="request">The request message used to connecto the broker server</param> + /// <param name="cancellationToken">A token to cancel the operationS</param> + /// <returns>The list of active servers</returns> + /// <exception cref="SecurityException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public static async Task<ActiveServer[]?> ListServersAsync(ListServerRequest request, CancellationToken cancellationToken = default) + { + _ = request ?? throw new ArgumentNullException(nameof(request)); + + string jwtBody; + //Build request jwt + using (JsonWebToken requestJwt = new()) + { + requestJwt.WriteHeader(request.JwtHeader); + requestJwt.InitPayloadClaim() + .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) + .AddClaim("nonce", RandomHash.GetRandomBase32(16)) + .CommitClaims(); + //sign the jwt + request.SignJwt(requestJwt); + //Compile the jwt + jwtBody = requestJwt.Compile(); + } + + //New list request + RestRequest listRequest = new(request.BrokerAddress, Method.Post); + + //Add the jwt as a string to the request body + listRequest.AddStringBody(jwtBody, DataFormat.None); + listRequest.AddHeader("Accept", HttpHelpers.GetContentTypeString(ContentType.Text)); + listRequest.AddHeader("Content-Type", HttpHelpers.GetContentTypeString(ContentType.Text)); + + byte[] data; + + //Rent client + using (ClientContract client = ClientPool.Lease()) + { + //Exec list request + RestResponse response = await client.Resource.ExecuteAsync(listRequest, cancellationToken); + + if (!response.IsSuccessful) + { + throw response.ErrorException!; + } + + data = response.RawBytes ?? throw new InvalidOperationException("No data returned from broker"); + } + //Response is jwt + using JsonWebToken responseJwt = JsonWebToken.ParseRaw(data); + + //Verify the jwt + if (!request.VerifyJwt(responseJwt)) + { + throw new SecurityException("Failed to verify the broker's challenge, cannot continue"); + } + + using JsonDocument doc = responseJwt.GetPayload(); + return doc.RootElement.GetProperty("servers").Deserialize<ActiveServer[]>(); + } + + /// <summary> + /// Registers the current server as active with the specified broker + /// </summary> + /// <param name="registration">The registration request</param> + public static async Task ResgisterWithBrokerAsync(BrokerRegistrationRequest registration) + { + _ = registration ?? throw new ArgumentNullException(nameof(registration)); + _ = registration.HeartbeatToken ?? throw new ArgumentException("Missing required heartbeat access token"); + _ = registration.NodeId ?? throw new ArgumentException("Missing required cache server NodeId"); + _ = registration.BrokerAddress ?? throw new ArgumentException("Broker server address has not been configured"); + _ = registration.RegistrationAddress ?? throw new ArgumentException("Missing required registration address", nameof(registration)); + + string requestData; + //Create the jwt for signed registration message + using (JsonWebToken jwt = new()) + { + //Shared jwt header + jwt.WriteHeader(registration.JsonHeader); + //build jwt claim + jwt.InitPayloadClaim() + .AddClaim("address", registration.RegistrationAddress) + .AddClaim("sub", registration.NodeId) + .AddClaim("token", registration.HeartbeatToken) + .CommitClaims(); + + //Sign the jwt + registration.SignJwt(jwt); + //Compile and save + requestData = jwt.Compile(); + } + //Create reg request message + RestRequest regRequest = new(registration.BrokerAddress); + regRequest.AddStringBody(requestData, DataFormat.None); + regRequest.AddHeader("Content-Type", "text/plain"); + //Rent client + using ClientContract cc = ClientPool.Lease(); + //Exec the regitration request + RestResponse response = await cc.Resource.ExecutePutAsync(regRequest); + if(!response.IsSuccessful) + { + throw response.ErrorException!; + } + } + + + /// <summary> + /// Allows for configuration of an <see cref="FBMClient"/> + /// for a connection to a cache server + /// </summary> + /// <param name="client"></param> + /// <returns>A fluent api configuration builder for the current client</returns> + public static ClientCacheConfiguration GetCacheConfiguration(this FBMClient client) => ClientCacheConfig.GetOrCreateValue(client); + + /// <summary> + /// Discovers cache nodes in the broker configured for the current client. + /// </summary> + /// <param name="client"></param> + /// <param name="token">A token to cancel the discovery</param> + /// <returns>A task the resolves the list of active servers on the broker server</returns> + public static Task<ActiveServer[]?> DiscoverCacheNodesAsync(this FBMClientWorkerBase client, CancellationToken token = default) => client.Client.DiscoverCacheNodesAsync(token); + + /// <summary> + /// Discovers cache nodes in the broker configured for the current client. + /// </summary> + /// <param name="client"></param> + /// <param name="token">A token to cancel the discovery </param> + /// <returns>A task the resolves the list of active servers on the broker server</returns> + public static async Task<ActiveServer[]?> DiscoverCacheNodesAsync(this FBMClient client, CancellationToken token = default) + { + ClientCacheConfiguration conf = ClientCacheConfig.GetOrCreateValue(client); + //Request from config + using ListServerRequest req = ListServerRequest.FromConfig(conf); + //List servers async + return conf.CacheServers = await ListServersAsync(req, token); + } + + /// <summary> + /// Waits for the client to disconnect from the server while observing + /// the cancellation token. If the token is cancelled, the connection is + /// closed cleanly if possible + /// </summary> + /// <param name="client"></param> + /// <param name="token">A token to cancel the connection to the server</param> + /// <returns>A task that complets when the connecion has been closed successfully</returns> + public static async Task WaitForExitAsync(this FBMClient client, CancellationToken token = default) + { + client.LogDebug("Waiting for cache client to exit"); + //Get task for cancellation + Task cancellation = token.WaitHandle.WaitAsync(); + //Task for status handle + Task run = client.ConnectionStatusHandle.WaitAsync(); + //Wait for cancellation or + _ = await Task.WhenAny(cancellation, run); + + client.LogDebug("Disconnecting the cache client"); + //Normal try to disconnect the socket + await client.DisconnectAsync(CancellationToken.None); + //Notify if cancelled + token.ThrowIfCancellationRequested(); + } + + /// <summary> + /// Connects to a random server from the servers discovered during a cache server discovery + /// </summary> + /// <param name="client"></param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The server that the connection was made with</returns> + /// <exception cref="ArgumentException"></exception> + public static async Task<ActiveServer> ConnectToRandomCacheAsync(this FBMClient client, CancellationToken cancellation = default) + { + //Get stored config + ClientCacheConfiguration conf = ClientCacheConfig.GetOrCreateValue(client); + //Select random + ActiveServer? randomServer = conf.CacheServers?.SelectRandom(); + _ = randomServer ?? throw new ArgumentException("No servers detected, cannot connect"); + await ConnectToCacheAsync(client, randomServer, cancellation); + return randomServer; + } + + /// <summary> + /// Connects to the specified server on the configured cache client + /// </summary> + /// <param name="client"></param> + /// <param name="server">The server to connect to</param> + /// <param name="token">A token to cancel the operation</param> + /// <returns>A task that resolves when the client is connected to the cache server</returns> + /// <exception cref="FBMException"></exception> + /// <exception cref="FBMServerNegiationException"></exception> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="SecurityException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static Task ConnectToCacheAsync(this FBMClient client, ActiveServer server, CancellationToken token = default) + { + _ = client ?? throw new ArgumentNullException(nameof(client)); + _ = server ?? throw new ArgumentNullException(nameof(server)); + + //Get stored config + ClientCacheConfiguration conf = ClientCacheConfig.GetOrCreateValue(client); + //Connect to server (no server id because client not replication server) + return ConnectToCacheAsync(client, conf, server, token); + } + + + private static async Task ConnectToCacheAsync(FBMClient client, ClientCacheConfiguration request, ActiveServer server, CancellationToken token = default) + { + //Construct server uri + Uri serverUri = new(server.HostName!); + + //build ws uri + UriBuilder uriBuilder = new(serverUri) + { + Scheme = request.UseTls ? "wss://" : "ws://" + }; + + string jwtMessage; + //Init jwt for connecting to server + using (JsonWebToken jwt = new()) + { + jwt.WriteHeader(request.JwtHeader); + + //Init claim + JwtPayload claim = jwt.InitPayloadClaim(); + + claim.AddClaim("chl", request.ServerChallenge); + + if (!string.IsNullOrWhiteSpace(request.NodeId)) + { + /* + * The unique node id so the other nodes know to load the + * proper event queue for the current server + */ + claim.AddClaim("sub", request.NodeId); + } + + claim.CommitClaims(); + + //Sign jwt + request.SignJwt(jwt); + + //Compile to string + jwtMessage = jwt.Compile(); + } + + RestRequest negotation = new(serverUri, Method.Get); + //Set the jwt auth header for negotiation + negotation.AddHeader("Authorization", jwtMessage); + negotation.AddHeader("Accept", HttpHelpers.GetContentTypeString(ContentType.Text)); + + client.LogDebug("Negotiating with cache server"); + + string authToken; + + //rent client + using (ClientContract clientContract = ClientPool.Lease()) + { + //Execute the request + RestResponse response = await clientContract.Resource.ExecuteGetAsync(negotation, token); + + //Check verify the response + if (!response.IsSuccessful) + { + throw response.ErrorException!; + } + + if (response.Content == null) + { + throw new FBMServerNegiationException("Failed to negotiate with the server, no response"); + } + + //Raw content + authToken = response.Content; + } + + //Parse the jwt + using (JsonWebToken jwt = JsonWebToken.Parse(authToken)) + { + //Verify the jwt + if (!request.VerifyCache(jwt)) + { + throw new SecurityException("Failed to verify the cache server's negotiation message, cannot continue"); + } + + //Confirm the server's buffer configuration + ValidateServerNegotation(client, request.ServerChallenge, jwt); + } + + client.LogDebug("Server negotiation validated, connecting to server"); + + //The client authorization header is the exact response + client.ClientSocket.Headers[HttpRequestHeader.Authorization] = authToken; + + //Connect async + await client.ConnectAsync(uriBuilder.Uri, token); + } + + private static void ValidateServerNegotation(FBMClient client, string challenge, JsonWebToken jwt) + { + try + { + //Get the response message to verify the challenge, and client arguments + using JsonDocument doc = jwt.GetPayload(); + + IReadOnlyDictionary<string, JsonElement> args = doc.RootElement + .EnumerateObject() + .ToDictionary(static k => k.Name, static v => v.Value); + + //get the challenge response + string challengeResponse = args["chl"].GetString()!; + + //Check the challenge response + if (!challenge.Equals(challengeResponse, StringComparison.Ordinal)) + { + throw new FBMServerNegiationException("Failed to negotiate with the server, challenge response does not match"); + } + + //Get the negiation values + uint recvBufSize = args[FBMClient.REQ_RECV_BUF_QUERY_ARG].GetUInt32(); + uint headerBufSize = args[FBMClient.REQ_HEAD_BUF_QUERY_ARG].GetUInt32(); + uint maxMessSize = args[FBMClient.REQ_MAX_MESS_QUERY_ARG].GetUInt32(); + + //Verify the values + if (client.Config.RecvBufferSize > recvBufSize) + { + throw new FBMServerNegiationException("Failed to negotiate with the server, the server's recv buffer size is too small"); + } + + if (client.Config.MaxHeaderBufferSize > headerBufSize) + { + throw new FBMServerNegiationException("Failed to negotiate with the server, the server's header buffer size is too small"); + } + + if (client.Config.MaxMessageSize > maxMessSize) + { + throw new FBMServerNegiationException("Failed to negotiate with the server, the server's max message size is too small"); + } + } + catch (FBMException) + { + throw; + } + catch (Exception ex) + { + throw new FBMServerNegiationException("Negotiation with the server failed", ex); + } + } + + /// <summary> + /// Selects a random server from a collection of active servers + /// </summary> + /// <param name="servers"></param> + /// <returns>A server selected at random</returns> + public static ActiveServer SelectRandom(this ICollection<ActiveServer> servers) + { + //select random server + int randServer = RandomNumberGenerator.GetInt32(0, servers.Count); + return servers.ElementAt(randServer); + } + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/FBMServerNegiationException.cs b/lib/VNLib.Data.Caching.Extensions/src/FBMServerNegiationException.cs new file mode 100644 index 0000000..d4d08c8 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/FBMServerNegiationException.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: FBMServerNegiationException.cs +* +* FBMServerNegiationException.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using VNLib.Net.Messaging.FBM; + + +namespace VNLib.Data.Caching.Extensions +{ + /// <summary> + /// Represents an exception that is raised because of a client-server + /// negotiation failure. + /// </summary> + public class FBMServerNegiationException : FBMException + { + public FBMServerNegiationException() + {} + public FBMServerNegiationException(string message) : base(message) + {} + public FBMServerNegiationException(string message, Exception innerException) : base(message, innerException) + {} + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/ListServerRequest.cs b/lib/VNLib.Data.Caching.Extensions/src/ListServerRequest.cs new file mode 100644 index 0000000..9e80bd2 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/ListServerRequest.cs @@ -0,0 +1,116 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching.Extensions +* File: ListServerRequest.cs +* +* ListServerRequest.cs is part of VNLib.Data.Caching.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching.Extensions 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.Data.Caching.Extensions is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using VNLib.Utils; +using VNLib.Hashing.IdentityUtility; + +namespace VNLib.Data.Caching.Extensions +{ + /// <summary> + /// A request container for a ListServer request + /// </summary> + public sealed class ListServerRequest : VnDisposeable + { + private readonly bool _ownsKeys; + + private ReadOnlyJsonWebKey? VerificationKey; + private ReadOnlyJsonWebKey? SigningAlg; + + /// <summary> + /// The address of the broker server to connect to + /// </summary> + public Uri BrokerAddress { get; } + + public ListServerRequest(Uri brokerAddress) + { + BrokerAddress = brokerAddress; + _ownsKeys = true; + } + + private ListServerRequest(ClientCacheConfiguration conf) + { + //Broker verification key is required + VerificationKey = conf.BrokerVerificationKey; + SigningAlg = conf.SigningKey; + BrokerAddress = conf.BrokerAddress ?? throw new ArgumentException("Broker address must be specified"); + _ownsKeys = false; + } + + internal static ListServerRequest FromConfig(ClientCacheConfiguration conf) => new (conf); + + /// <summary> + /// Sets the public key used to verify the signature of the response. + /// </summary> + /// <param name="jwk">The key used to verify messages </param> + public ListServerRequest WithVerificationKey(ReadOnlyJsonWebKey jwk) + { + VerificationKey = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + /// <summary> + /// Sets the private key used to sign the request. + /// </summary> + /// <param name="jwk">The <see cref="ReadOnlyJsonWebKey"/> containing the private key used to sign the message</param> + /// <exception cref="ArgumentNullException"></exception> + public ListServerRequest WithSigningKey(ReadOnlyJsonWebKey jwk) + { + SigningAlg = jwk ?? throw new ArgumentNullException(nameof(jwk)); + return this; + } + + /// <summary> + /// Signs the <see cref="JsonWebToken"/> using the private key. + /// </summary> + /// <param name="jwt">The message to sign</param> + internal void SignJwt(JsonWebToken jwt) + { + jwt.SignFromJwk(SigningAlg); + } + + /// <summary> + /// Verifies the signature of the <see cref="JsonWebToken"/> + /// </summary> + /// <param name="jwt"></param> + /// <returns>A value that indicates if the signature is verified</returns> + internal bool VerifyJwt(JsonWebToken jwt) + { + return jwt.VerifyFromJwk(VerificationKey); + } + + internal IReadOnlyDictionary<string, string?> JwtHeader => SigningAlg!.JwtHeader; + + ///<inheritdoc/> + protected override void Free() + { + if (_ownsKeys) + { + VerificationKey?.Dispose(); + SigningAlg?.Dispose(); + } + } + } +} diff --git a/lib/VNLib.Data.Caching.Extensions/src/VNLib.Data.Caching.Extensions.csproj b/lib/VNLib.Data.Caching.Extensions/src/VNLib.Data.Caching.Extensions.csproj new file mode 100644 index 0000000..10dcd71 --- /dev/null +++ b/lib/VNLib.Data.Caching.Extensions/src/VNLib.Data.Caching.Extensions.csproj @@ -0,0 +1,41 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <Version>1.0.1.1</Version> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2023 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <AnalysisLevel>latest-all</AnalysisLevel> + <Description>A libray for working with VNCache object cache clusters. </Description> + </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> + <PackageReference Include="RestSharp" Version="108.0.3" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\..\core\lib\Hashing.Portable\src\VNLib.Hashing.Portable.csproj" /> + <ProjectReference Include="..\..\..\..\..\core\lib\Net.Messaging.FBM\src\VNLib.Net.Messaging.FBM.csproj" /> + <ProjectReference Include="..\..\..\..\..\core\lib\Net.Rest.Client\src\VNLib.Net.Rest.Client.csproj" /> + <ProjectReference Include="..\..\..\..\..\core\lib\Utils\src\VNLib.Utils.csproj" /> + </ItemGroup> + +</Project> |