From daf7ed9fe6f1ffce633077772fad69926a579d89 Mon Sep 17 00:00:00 2001 From: Jkorf Date: Tue, 19 Aug 2025 09:50:10 +0200 Subject: [PATCH] Refactored RestApiClient authentication to prevent duplicate query string / body serialization --- .../RestClientTests.cs | 2 +- .../TestImplementations/TestBaseClient.cs | 4 + .../Authentication/AuthenticationProvider.cs | 25 +--- CryptoExchange.Net/Clients/RestApiClient.cs | 92 ++++++------- .../Objects/RequestDefinition.cs | 3 + .../Objects/RestRequestConfiguration.cs | 124 ++++++++++++++++++ 6 files changed, 173 insertions(+), 77 deletions(-) create mode 100644 CryptoExchange.Net/Objects/RestRequestConfiguration.cs diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 1a93a3a..57f1f08 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests [TestCase("/sapi/test1", true)] [TestCase("/sapi/test2", true)] [TestCase("/api/test1", false)] - [TestCase("sapi/test1", false)] + [TestCase("sapi/test1", true)] [TestCase("/sapi/", true)] public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting) { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 09df970..1491072 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -82,6 +82,10 @@ namespace CryptoExchange.Net.UnitTests { } + public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig) + { + } + public string GetKey() => _credentials.Key; public string GetSecret() => _credentials.Secret; } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index a092078..1c49f1d 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication } /// - /// Authenticate a request. Output parameters should include the providedParameters input + /// Authenticate a request /// /// The Api client sending the request - /// The uri for the request - /// The method of the request - /// If the requests should be authenticated - /// Array serialization type - /// The formatting of the request body - /// Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri - /// Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body - /// The headers that should be send with the request - /// The position where the providedParameters should go - public abstract void AuthenticateRequest( - RestApiClient apiClient, - Uri uri, - HttpMethod method, - ref IDictionary? uriParameters, - ref IDictionary? bodyParameters, - ref Dictionary? headers, - bool auth, - ArrayParametersSerialization arraySerialization, - HttpMethodParameterPosition parameterPosition, - RequestBodyFormat requestBodyFormat - ); + /// The request configuration + public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig); /// /// SHA256 sign the data and return the bytes diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index c368e3d..a17ce6f 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -55,7 +55,7 @@ namespace CryptoExchange.Net.Clients /// /// Request headers to be sent with each request /// - protected Dictionary? StandardRequestHeaders { get; set; } + protected Dictionary StandardRequestHeaders { get; set; } = []; /// /// Whether parameters need to be ordered @@ -364,74 +364,58 @@ namespace CryptoExchange.Net.Clients ParameterCollection? bodyParameters, Dictionary? additionalHeaders) { - var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters); - var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters); + var requestConfiguration = new RestRequestConfiguration( + definition, + baseAddress, + uriParameters == null ? new Dictionary() : CreateParameterDictionary(uriParameters), + bodyParameters == null ? new Dictionary() : CreateParameterDictionary(bodyParameters), + new Dictionary(additionalHeaders ?? []), + definition.ArraySerialization ?? ArraySerialization, + definition.ParameterPosition ?? ParameterPositions[definition.Method], + definition.RequestBodyFormat ?? RequestBodyFormat); - var uri = new Uri(baseAddress.AppendPath(definition.Path)); - var arraySerialization = definition.ArraySerialization ?? ArraySerialization; - var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; - var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; - - Dictionary? headers = null; - if (AuthenticationProvider != null) + try { - try - { - AuthenticationProvider.AuthenticateRequest( - this, - uri, - definition.Method, - ref uriParams, - ref bodyParams, - ref headers, - definition.Authenticated, - arraySerialization, - parameterPosition, - bodyFormat - ); - } - catch (Exception ex) - { - throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); - } + AuthenticationProvider?.ProcessRequest(this, requestConfiguration); } + catch (Exception ex) + { + throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); + } + + var queryString = requestConfiguration.GetQueryString(true); + if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) + queryString = $"?{queryString}"; - // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters - if (uriParams != null) - uri = uri.SetParameters(uriParams, arraySerialization); - + var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); var request = RequestFactory.Create(definition.Method, uri, requestId); request.Accept = Constants.JsonContentHeader; - if (headers != null) - { - foreach (var header in headers) - request.AddHeader(header.Key, header.Value); - } + foreach (var header in requestConfiguration.Headers) + request.AddHeader(header.Key, header.Value); - if (additionalHeaders != null) + foreach (var header in StandardRequestHeaders) { - foreach (var header in additionalHeaders) + // Only add it if it isn't overwritten + if (!requestConfiguration.Headers.ContainsKey(header.Key)) request.AddHeader(header.Key, header.Value); - } + } - if (StandardRequestHeaders != null) + if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody) { - foreach (var header in StandardRequestHeaders) + var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + var bodyContent = requestConfiguration.GetBodyContent(); + if (bodyContent != null) { - // Only add it if it isn't overwritten - if (additionalHeaders?.ContainsKey(header.Key) != true) - request.AddHeader(header.Key, header.Value); + request.SetContent(bodyContent, contentType); } - } - - if (parameterPosition == HttpMethodParameterPosition.InBody) - { - var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - if (bodyParams != null && bodyParams.Count != 0) - WriteParamBody(request, bodyParams, contentType); else - request.SetContent(RequestBodyEmptyContent, contentType); + { + if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) + WriteParamBody(request, requestConfiguration.BodyParameters, contentType); + else + request.SetContent(RequestBodyEmptyContent, contentType); + } } return request; diff --git a/CryptoExchange.Net/Objects/RequestDefinition.cs b/CryptoExchange.Net/Objects/RequestDefinition.cs index 3d78592..6ce9ab4 100644 --- a/CryptoExchange.Net/Objects/RequestDefinition.cs +++ b/CryptoExchange.Net/Objects/RequestDefinition.cs @@ -76,6 +76,9 @@ namespace CryptoExchange.Net.Objects { Path = path; Method = method; + + if (!Path.StartsWith("/")) + Path = $"/{Path}"; } /// diff --git a/CryptoExchange.Net/Objects/RestRequestConfiguration.cs b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs new file mode 100644 index 0000000..e748b9f --- /dev/null +++ b/CryptoExchange.Net/Objects/RestRequestConfiguration.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Net.Http; + +namespace CryptoExchange.Net.Objects +{ + /// + /// Rest request configuration + /// + public class RestRequestConfiguration + { + private string? _bodyContent; + private string? _queryString; + + /// + /// Http method + /// + public HttpMethod Method { get; set; } + /// + /// Whether the request needs authentication + /// + public bool Authenticated { get; set; } + /// + /// Base address for the request + /// + public string BaseAddress { get; set; } + /// + /// The request path + /// + public string Path { get; set; } + /// + /// Query parameters + /// + public IDictionary QueryParameters { get; set; } + /// + /// Body parameters + /// + public IDictionary BodyParameters { get; set; } + /// + /// Request headers + /// + public IDictionary Headers { get; set; } + /// + /// Array serialization type + /// + public ArrayParametersSerialization ArraySerialization { get; set; } + /// + /// Position of the parameters + /// + public HttpMethodParameterPosition ParameterPosition { get; set; } + /// + /// Body format + /// + public RequestBodyFormat BodyFormat { get; set; } + + /// + /// ctor + /// + public RestRequestConfiguration( + RequestDefinition requestDefinition, + string baseAddress, + IDictionary queryParams, + IDictionary bodyParams, + IDictionary headers, + ArrayParametersSerialization arraySerialization, + HttpMethodParameterPosition parametersPosition, + RequestBodyFormat bodyFormat) + { + Method = requestDefinition.Method; + Authenticated = requestDefinition.Authenticated; + Path = requestDefinition.Path; + BaseAddress = baseAddress; + QueryParameters = queryParams; + BodyParameters = bodyParams; + Headers = headers; + ArraySerialization = arraySerialization; + ParameterPosition = parametersPosition; + BodyFormat = bodyFormat; + } + + /// + /// Get the parameter collection based on the ParameterPosition + /// + public IDictionary GetPositionParameters() + { + if (ParameterPosition == HttpMethodParameterPosition.InBody) + return BodyParameters; + + return QueryParameters; + } + + /// + /// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that. + /// + /// Whether to URL encode the parameter string if creating new + public string GetQueryString(bool urlEncode = true) + { + return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization); + } + + /// + /// Set the query string of the request. Will be returned by subsequent calls + /// + public void SetQueryString(string value) + { + _queryString = value; + } + + /// + /// Get the body content if it's previously set + /// + public string? GetBodyContent() + { + return _bodyContent; + } + + /// + /// Set the body content for the request + /// + public void SetBodyContent(string content) + { + _bodyContent = content; + } + } +}