From 71e29f99acc6f85154d0ed574ed8e68bfcaec329 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 8 Jan 2023 18:41:00 -0500 Subject: Net.Rest.Client addition --- lib/Net.Rest.Client/src/ClientContract.cs | 60 +++++++++ lib/Net.Rest.Client/src/ClientPool.cs | 71 +++++++++++ lib/Net.Rest.Client/src/OAuth2/Credential.cs | 78 ++++++++++++ .../src/OAuth2/O2ErrorResponseMessage.cs | 80 ++++++++++++ .../src/OAuth2/OAuth2AuthenticationException.cs | 77 ++++++++++++ .../src/OAuth2/OAuth2Authenticator.cs | 137 +++++++++++++++++++++ .../src/VNLib.Net.Rest.Client.csproj | 44 +++++++ 7 files changed, 547 insertions(+) create mode 100644 lib/Net.Rest.Client/src/ClientContract.cs create mode 100644 lib/Net.Rest.Client/src/ClientPool.cs create mode 100644 lib/Net.Rest.Client/src/OAuth2/Credential.cs create mode 100644 lib/Net.Rest.Client/src/OAuth2/O2ErrorResponseMessage.cs create mode 100644 lib/Net.Rest.Client/src/OAuth2/OAuth2AuthenticationException.cs create mode 100644 lib/Net.Rest.Client/src/OAuth2/OAuth2Authenticator.cs create mode 100644 lib/Net.Rest.Client/src/VNLib.Net.Rest.Client.csproj (limited to 'lib/Net.Rest.Client/src') diff --git a/lib/Net.Rest.Client/src/ClientContract.cs b/lib/Net.Rest.Client/src/ClientContract.cs new file mode 100644 index 0000000..0c61f8c --- /dev/null +++ b/lib/Net.Rest.Client/src/ClientContract.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: ClientContract.cs +* +* ClientContract.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using RestSharp; +using VNLib.Utils.Memory.Caching; +using VNLib.Utils.Resources; + +namespace VNLib.Net.Rest.Client +{ + /// + /// Represents a RestClient least contract. When disposed, + /// releases it for use by other waiters + /// + public class ClientContract : OpenResourceHandle + { + private readonly RestClient _client; + private readonly ObjectRental _pool; + + internal ClientContract(RestClient client, ObjectRental pool) + { + _client = client; + _pool = pool; + } + /// + public override RestClient Resource + { + get + { + Check(); + return _client; + } + } + /// + protected override void Free() + { + _pool.Return(_client); + } + } +} diff --git a/lib/Net.Rest.Client/src/ClientPool.cs b/lib/Net.Rest.Client/src/ClientPool.cs new file mode 100644 index 0000000..7d3bbd0 --- /dev/null +++ b/lib/Net.Rest.Client/src/ClientPool.cs @@ -0,0 +1,71 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: ClientPool.cs +* +* ClientPool.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using RestSharp; +using RestSharp.Authenticators; + +using VNLib.Utils.Memory.Caching; + +#nullable enable + +namespace VNLib.Net.Rest.Client +{ + /// + /// Maintains a pool of lazy loaded instances to allow for concurrent client usage + /// + public class RestClientPool : ObjectRental + { + /// + /// Creates a new instance and creates the specified + /// number of clients with the same number of concurrency. + /// + /// The maximum number of clients to create and authenticate, should be the same as the number of maximum allowed tokens + /// A used to initialze the pool of clients + /// An optional authenticator for clients to use + /// An optional client initialzation callback + public RestClientPool(int maxClients, RestClientOptions options, Action? initCb = null, IAuthenticator? authenticator = null) + : base(() => + { + RestClient client = new(options); + //Add optional authenticator + if (authenticator != null) + { + client.UseAuthenticator(authenticator); + } + //Invoke init callback + initCb?.Invoke(client); + return client; + }, null, null, maxClients) + { + } + + /// + /// Obtains a new for a reused, or new, instance + /// + /// The contract that manages the client + public ClientContract Lease() => new(base.Rent(), this); + } +} diff --git a/lib/Net.Rest.Client/src/OAuth2/Credential.cs b/lib/Net.Rest.Client/src/OAuth2/Credential.cs new file mode 100644 index 0000000..3fd5da6 --- /dev/null +++ b/lib/Net.Rest.Client/src/OAuth2/Credential.cs @@ -0,0 +1,78 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: Credential.cs +* +* Credential.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils.Memory; + +namespace VNLib.Net.Rest.Client.OAuth2 +{ + /// + /// Creates a disposeable "protected" credential object. + /// + /// + /// Properties used after the instance has been disposed are + /// undefined + /// + public class Credential : PrivateStringManager + { + /// + /// The credential username parameter + /// + public string UserName => this[0]; + /// + /// The credential's password + /// + public string Password => this[1]; + + private Credential(string uname, string pass) : base(2) + { + this[0] = uname; + this[1] = pass; + } + + /// + /// Creates a new protected + /// that owns the memory to the supplied strings + /// + /// The username string to consume + /// The password string to consume + /// A new protected credential object + public static Credential CreateUnsafe(in string username, in string password) + { + return new Credential(username, password); + } + /// + /// Creates a new "safe" by copying + /// the values of the supplied credential properites + /// + /// The username value to copy + /// The password value to copy + /// A new protected credential object + public static Credential Create(ReadOnlySpan username, ReadOnlySpan password) + { + return new Credential(username.ToString(), password.ToString()); + } + } +} diff --git a/lib/Net.Rest.Client/src/OAuth2/O2ErrorResponseMessage.cs b/lib/Net.Rest.Client/src/OAuth2/O2ErrorResponseMessage.cs new file mode 100644 index 0000000..151c575 --- /dev/null +++ b/lib/Net.Rest.Client/src/OAuth2/O2ErrorResponseMessage.cs @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: O2ErrorResponseMessage.cs +* +* O2ErrorResponseMessage.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace VNLib.Net.Rest.Client.OAuth2 +{ + /// + /// An OAuth2 standard error message + /// + public class O2ErrorResponseMessage + { + /// + /// The OAuth2 error code + /// + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } + + /// + /// The OAuth2 human readable error description + /// + [JsonPropertyName("error_description")] + public string ErrorDescription { get; set; } + /// + /// Initializes a new + /// + public O2ErrorResponseMessage() + {} + /// + /// Initializes a new + /// + /// The OAuth2 error code + /// The OAuth2 error description + public O2ErrorResponseMessage(string code, string description) + { + this.ErrorCode = code; + ErrorDescription = description; + } + + /// + /// Initializes a new instance + /// from a error element + /// + /// An error element that represens the error + public O2ErrorResponseMessage(ref JsonElement el) + { + if(el.TryGetProperty("error_code", out JsonElement code)) + { + this.ErrorCode = code.GetString(); + } + if (el.TryGetProperty("error_code", out JsonElement description)) + { + this.ErrorDescription = description.GetString(); + } + } + } +} diff --git a/lib/Net.Rest.Client/src/OAuth2/OAuth2AuthenticationException.cs b/lib/Net.Rest.Client/src/OAuth2/OAuth2AuthenticationException.cs new file mode 100644 index 0000000..7176093 --- /dev/null +++ b/lib/Net.Rest.Client/src/OAuth2/OAuth2AuthenticationException.cs @@ -0,0 +1,77 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: OAuth2AuthenticationException.cs +* +* OAuth2AuthenticationException.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Security.Authentication; + +using RestSharp; + +namespace VNLib.Net.Rest.Client.OAuth2 +{ + /// + /// Raised when a token request made to the authorization endpoint + /// fails. Inner exceptions may be set if the response succeeds but + /// returned invalid data + /// + public class OAuth2AuthenticationException : AuthenticationException + { + /// + /// The status code returned from the request + /// + public HttpStatusCode StatusCode => ErrorResponse?.StatusCode ?? 0; + /// + /// The string representation of the response that was received by the token request + /// + public RestResponse ErrorResponse { get; } + /// + public OAuth2AuthenticationException() + { } + /// + public OAuth2AuthenticationException(string message) : base(message) + { } + /// + public OAuth2AuthenticationException(string message, Exception innerException) : base(message, innerException) + { } + /// + /// Initializes a new with the + /// specified server response + /// + /// The response containing the error result + public OAuth2AuthenticationException(RestResponse response) : base() + { + ErrorResponse = response; + } + /// + /// Initializes a new with the + /// specified server response + /// + /// The response containing the error result + /// An inner excepion that caused the authentication to fail + public OAuth2AuthenticationException(RestResponse response, Exception innerException) : base(null, innerException) + { + ErrorResponse = response; + } + } +} diff --git a/lib/Net.Rest.Client/src/OAuth2/OAuth2Authenticator.cs b/lib/Net.Rest.Client/src/OAuth2/OAuth2Authenticator.cs new file mode 100644 index 0000000..b978404 --- /dev/null +++ b/lib/Net.Rest.Client/src/OAuth2/OAuth2Authenticator.cs @@ -0,0 +1,137 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Rest.Client +* File: OAuth2Authenticator.cs +* +* OAuth2Authenticator.cs is part of VNLib.Net.Rest.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Rest.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Net.Rest.Client 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Net.Rest.Client. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using RestSharp; +using RestSharp.Authenticators; + +using VNLib.Utils; +using VNLib.Utils.Extensions; + +#nullable enable + +namespace VNLib.Net.Rest.Client.OAuth2 +{ + /// + /// A self-contained OAuth2 RestSharp authenticator. Contains resources + /// that should be disposed, when no longer in use. Represents a single + /// session on the remote server + /// + public class OAuth2Authenticator : VnDisposeable, IAuthenticator + { + private string? AccessToken; + private DateTime Expires; + + private uint _counter; + + private readonly RestClient _client; + private readonly Func CredFunc; + private readonly string TokenAuthPath; + private readonly SemaphoreSlim _tokenLock; + + /// + /// Initializes a new to be used for + /// authorizing requests to a remote server + /// + /// The RestClient options for the client used to authenticate with the OAuth2 server + /// The credential factory function + /// The path within the remote OAuth2 server to authenticate against + public OAuth2Authenticator(RestClientOptions clientOps, Func credFunc, string tokenPath) + { + //Default to expired + Expires = DateTime.MinValue; + _client = new(clientOps); + CredFunc = credFunc; + TokenAuthPath = tokenPath; + _tokenLock = new(1, 1); + } + + async ValueTask IAuthenticator.Authenticate(RestClient client, RestRequest request) + { + //Wait for access to the token incase another thread is updating it + using SemSlimReleaser releaser = await _tokenLock.GetReleaserAsync(CancellationToken.None); + //Check expiration + if (Expires < DateTime.UtcNow) + { + //We need to refresh the token + await RefreshTokenAsync(); + } + //Add bearer token to the request + request.AddOrUpdateHeader("Authorization", AccessToken!); + //Inc counter + _counter++; + } + + private async Task RefreshTokenAsync() + { + using Credential creds = CredFunc(); + //Build request to the authentication endpoint + RestRequest tokenRequest = new(TokenAuthPath, Method.Post); + //Setup grant-type paramter + tokenRequest.AddParameter("grant_type", "client_credentials", ParameterType.GetOrPost); + //Add client id + tokenRequest.AddParameter("client_id", creds.UserName, ParameterType.GetOrPost); + //Add secret + tokenRequest.AddParameter("client_secret", creds.Password, ParameterType.GetOrPost); + //exec get token + RestResponse tokenResponse = await _client.ExecuteAsync(tokenRequest); + if (!tokenResponse.IsSuccessful) + { + throw new OAuth2AuthenticationException(tokenResponse, tokenResponse.ErrorException); + } + try + { + //Get a json doc for the response data + using JsonDocument response = JsonDocument.Parse(tokenResponse.RawBytes); + //Get expiration + int expiresSec = response.RootElement.GetProperty("expires_in").GetInt32(); + //get access token + string? accessToken = response.RootElement.GetProperty("access_token").GetString(); + string? tokenType = response.RootElement.GetProperty("token_type").GetString(); + + //Store token variables, expire a few minutes before the server to allow for time disparity + Expires = DateTime.UtcNow.AddSeconds(expiresSec).Subtract(TimeSpan.FromMinutes(2)); + //compile auth header value + AccessToken = $"{tokenType} {accessToken}"; + //Reset counter + _counter = 0; + } + catch (Exception ex) + { + throw new OAuth2AuthenticationException(tokenResponse, ex); + } + } + + protected override void Free() + { + _tokenLock.Dispose(); + _client.Dispose(); + } + } +} diff --git a/lib/Net.Rest.Client/src/VNLib.Net.Rest.Client.csproj b/lib/Net.Rest.Client/src/VNLib.Net.Rest.Client.csproj new file mode 100644 index 0000000..1324192 --- /dev/null +++ b/lib/Net.Rest.Client/src/VNLib.Net.Rest.Client.csproj @@ -0,0 +1,44 @@ + + + + net6.0 + Copyright © 2022 Vaughn Nugent + Vaughn Nugent + 1.0.1.1 + An Oauth2 rest client connection pool for OAuth2 authenticated services + https://www.vaughnnugent.com/resources + VNLib.Net.Rest.Client + VNLib.Net.Rest.Client + True + latest-all + + + + False + + + + False + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + -- cgit