diff options
Diffstat (limited to 'lib/Net.Rest.Client/src')
-rw-r--r-- | lib/Net.Rest.Client/src/ClientContract.cs | 60 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/ClientPool.cs | 71 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/OAuth2/Credential.cs | 78 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/OAuth2/O2ErrorResponseMessage.cs | 80 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/OAuth2/OAuth2AuthenticationException.cs | 77 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/OAuth2/OAuth2Authenticator.cs | 137 | ||||
-rw-r--r-- | lib/Net.Rest.Client/src/VNLib.Net.Rest.Client.csproj | 44 |
7 files changed, 547 insertions, 0 deletions
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 +{ + /// <summary> + /// Represents a RestClient least contract. When disposed, + /// releases it for use by other waiters + /// </summary> + public class ClientContract : OpenResourceHandle<RestClient> + { + private readonly RestClient _client; + private readonly ObjectRental<RestClient> _pool; + + internal ClientContract(RestClient client, ObjectRental<RestClient> pool) + { + _client = client; + _pool = pool; + } + ///<inheritdoc/> + public override RestClient Resource + { + get + { + Check(); + return _client; + } + } + ///<inheritdoc/> + 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 +{ + /// <summary> + /// Maintains a pool of lazy loaded <see cref="RestClient"/> instances to allow for concurrent client usage + /// </summary> + public class RestClientPool : ObjectRental<RestClient> + { + /// <summary> + /// Creates a new <see cref="RestClientPool"/> instance and creates the specified + /// number of clients with the same number of concurrency. + /// </summary> + /// <param name="maxClients">The maximum number of clients to create and authenticate, should be the same as the number of maximum allowed tokens</param> + /// <param name="options">A <see cref="RestClientOptions"/> used to initialze the pool of clients</param> + /// <param name="authenticator">An optional authenticator for clients to use</param> + /// <param name="initCb">An optional client initialzation callback</param> + public RestClientPool(int maxClients, RestClientOptions options, Action<RestClient>? 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) + { + } + + /// <summary> + /// Obtains a new <see cref="ClientContract"/> for a reused, or new, <see cref="RestClient"/> instance + /// </summary> + /// <returns>The contract that manages the client</returns> + 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 +{ + /// <summary> + /// Creates a disposeable "protected" credential object. + /// </summary> + /// <remarks> + /// Properties used after the instance has been disposed are + /// undefined + /// </remarks> + public class Credential : PrivateStringManager + { + /// <summary> + /// The credential username parameter + /// </summary> + public string UserName => this[0]; + /// <summary> + /// The credential's password + /// </summary> + public string Password => this[1]; + + private Credential(string uname, string pass) : base(2) + { + this[0] = uname; + this[1] = pass; + } + + /// <summary> + /// Creates a new protected <see cref="Credential"/> + /// that owns the memory to the supplied strings + /// </summary> + /// <param name="username">The username string to consume</param> + /// <param name="password">The password string to consume</param> + /// <returns>A new protected credential object</returns> + public static Credential CreateUnsafe(in string username, in string password) + { + return new Credential(username, password); + } + /// <summary> + /// Creates a new "safe" <see cref="Credential"/> by copying + /// the values of the supplied credential properites + /// </summary> + /// <param name="username">The username value to copy</param> + /// <param name="password">The password value to copy</param> + /// <returns>A new protected credential object</returns> + public static Credential Create(ReadOnlySpan<char> username, ReadOnlySpan<char> 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 +{ + /// <summary> + /// An OAuth2 standard error message + /// </summary> + public class O2ErrorResponseMessage + { + /// <summary> + /// The OAuth2 error code + /// </summary> + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } + + /// <summary> + /// The OAuth2 human readable error description + /// </summary> + [JsonPropertyName("error_description")] + public string ErrorDescription { get; set; } + /// <summary> + /// Initializes a new <see cref="O2ErrorResponseMessage"/> + /// </summary> + public O2ErrorResponseMessage() + {} + /// <summary> + /// Initializes a new <see cref="O2ErrorResponseMessage"/> + /// </summary> + /// <param name="code">The OAuth2 error code</param> + /// <param name="description">The OAuth2 error description</param> + public O2ErrorResponseMessage(string code, string description) + { + this.ErrorCode = code; + ErrorDescription = description; + } + + /// <summary> + /// Initializes a new <see cref="O2ErrorResponseMessage"/> instance + /// from a <see cref="JsonElement"/> error element + /// </summary> + /// <param name="el">An error element that represens the error</param> + 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 +{ + /// <summary> + /// Raised when a token request made to the authorization endpoint + /// fails. Inner exceptions may be set if the response succeeds but + /// returned invalid data + /// </summary> + public class OAuth2AuthenticationException : AuthenticationException + { + /// <summary> + /// The status code returned from the request + /// </summary> + public HttpStatusCode StatusCode => ErrorResponse?.StatusCode ?? 0; + /// <summary> + /// The string representation of the response that was received by the token request + /// </summary> + public RestResponse ErrorResponse { get; } + ///<inheritdoc/> + public OAuth2AuthenticationException() + { } + ///<inheritdoc/> + public OAuth2AuthenticationException(string message) : base(message) + { } + ///<inheritdoc/> + public OAuth2AuthenticationException(string message, Exception innerException) : base(message, innerException) + { } + /// <summary> + /// Initializes a new <see cref="OAuth2AuthenticationException"/> with the + /// specified server response + /// </summary> + /// <param name="response">The response containing the error result</param> + public OAuth2AuthenticationException(RestResponse response) : base() + { + ErrorResponse = response; + } + /// <summary> + /// Initializes a new <see cref="OAuth2AuthenticationException"/> with the + /// specified server response + /// </summary> + /// <param name="response">The response containing the error result</param> + /// <param name="innerException">An inner excepion that caused the authentication to fail</param> + 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 +{ + /// <summary> + /// 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 + /// </summary> + public class OAuth2Authenticator : VnDisposeable, IAuthenticator + { + private string? AccessToken; + private DateTime Expires; + + private uint _counter; + + private readonly RestClient _client; + private readonly Func<Credential> CredFunc; + private readonly string TokenAuthPath; + private readonly SemaphoreSlim _tokenLock; + + /// <summary> + /// Initializes a new <see cref="OAuth2Authenticator"/> to be used for + /// authorizing requests to a remote server + /// </summary> + /// <param name="clientOps">The RestClient options for the client used to authenticate with the OAuth2 server</param> + /// <param name="credFunc">The credential factory function</param> + /// <param name="tokenPath">The path within the remote OAuth2 server to authenticate against</param> + public OAuth2Authenticator(RestClientOptions clientOps, Func<Credential> 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 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <Authors>Vaughn Nugent</Authors> + <Version>1.0.1.1</Version> + <Description>An Oauth2 rest client connection pool for OAuth2 authenticated services</Description> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AssemblyName>VNLib.Net.Rest.Client</AssemblyName> + <RootNamespace>VNLib.Net.Rest.Client</RootNamespace> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <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> + <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.2" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" /> + </ItemGroup> + + <ItemGroup> + <Folder Include="Exceptions\" /> + </ItemGroup> + +</Project> |