using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace CryptoExchange.Net { /// /// Base API for all API clients /// public abstract class BaseApiClient : IDisposable, IBaseApiClient { /// /// Logger /// protected ILogger _logger; /// /// If we are disposing /// protected bool _disposing; /// /// The authentication provider for this API client. (null if no credentials are set) /// public AuthenticationProvider? AuthenticationProvider { get; private set; } /// /// Where to put the parameters for requests with different Http methods /// public Dictionary ParameterPositions { get; set; } = new Dictionary { { HttpMethod.Get, HttpMethodParameterPosition.InUri }, { HttpMethod.Post, HttpMethodParameterPosition.InBody }, { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, { HttpMethod.Put, HttpMethodParameterPosition.InBody } }; /// /// Request body content type /// public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json; /// /// Whether or not we need to manually parse an error instead of relying on the http status code /// public bool manualParseError = false; /// /// How to serialize array parameters when making requests /// public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; /// /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) /// public string requestBodyEmptyContent = "{}"; /// /// The environment this client communicates to /// public string BaseAddress { get; } /// /// Output the original string data along with the deserialized object /// public bool OutputOriginalData { get; } /// /// A default serializer /// private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Utc, Culture = CultureInfo.InvariantCulture }); /// /// Api options /// public ApiOptions ApiOptions { get; } /// /// Client Options /// public ExchangeOptions ClientOptions { get; } /// /// ctor /// /// Logger /// Should data from this client include the orginal data in the call result /// Base address for this API client /// Api credentials /// Client options /// Api options protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) { _logger = logger; ClientOptions = clientOptions; ApiOptions = apiOptions; OutputOriginalData = outputOriginalData; BaseAddress = baseAddress; if (apiCredentials != null) { AuthenticationProvider?.Dispose(); AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy()); } } /// /// Create an AuthenticationProvider implementation instance based on the provided credentials /// /// /// protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); /// public void SetApiCredentials(T credentials) where T : ApiCredentials { if (credentials != null) { AuthenticationProvider?.Dispose(); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); } } /// /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json /// /// The data to parse /// protected CallResult ValidateJson(string data) { if (string.IsNullOrEmpty(data)) { var info = "Empty data object received"; _logger.Log(LogLevel.Error, info); return new CallResult(new DeserializeError(info, data)); } try { return new CallResult(JToken.Parse(data)); } catch (JsonReaderException jre) { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}"; return new CallResult(new DeserializeError(info, data)); } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}"; return new CallResult(new DeserializeError(info, data)); } catch (Exception ex) { var exceptionInfo = ex.ToLogString(); var info = $"Deserialize Unknown Exception: {exceptionInfo}"; return new CallResult(new DeserializeError(info, data)); } } /// /// Deserialize a string into an object /// /// The type to deserialize into /// The data to deserialize /// A specific serializer to use /// Id of the request the data is returned from (used for grouping logging by request) /// protected CallResult Deserialize(string data, JsonSerializer? serializer = null, int? requestId = null) { var tokenResult = ValidateJson(data); if (!tokenResult) { _logger.Log(LogLevel.Error, tokenResult.Error!.Message); return new CallResult(tokenResult.Error); } return Deserialize(tokenResult.Data, serializer, requestId); } /// /// Deserialize a JToken into an object /// /// The type to deserialize into /// The data to deserialize /// A specific serializer to use /// Id of the request the data is returned from (used for grouping logging by request) /// protected CallResult Deserialize(JToken obj, JsonSerializer? serializer = null, int? requestId = null) { serializer ??= _defaultSerializer; try { return new CallResult(obj.ToObject(serializer)!); } catch (JsonReaderException jre) { var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}"; _logger.Log(LogLevel.Error, info); return new CallResult(new DeserializeError(info, obj)); } catch (JsonSerializationException jse) { var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}"; _logger.Log(LogLevel.Error, info); return new CallResult(new DeserializeError(info, obj)); } catch (Exception ex) { var exceptionInfo = ex.ToLogString(); var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}"; _logger.Log(LogLevel.Error, info); return new CallResult(new DeserializeError(info, obj)); } } /// /// Deserialize a stream into an object /// /// The type to deserialize into /// The stream to deserialize /// A specific serializer to use /// Id of the request the data is returned from (used for grouping logging by request) /// Milliseconds response time for the request this stream is a response for /// protected async Task> DeserializeAsync(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null) { serializer ??= _defaultSerializer; string? data = null; try { // Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream. using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); // If we have to output the original json data or output the data into the logging we'll have to read to full response // in order to log/return the json data if (OutputOriginalData == true) { data = await reader.ReadToEndAsync().ConfigureAwait(false); _logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data); var result = Deserialize(data, serializer, requestId); result.OriginalData = data; return result; } // If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly // into the desired object, which has increased performance over first reading the string value into memory and deserializing from that using var jsonReader = new JsonTextReader(reader); _logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms"); return new CallResult(serializer.Deserialize(jsonReader)!); } catch (JsonReaderException jre) { if (data == null) { if (stream.CanSeek) { // If we can seek the stream rewind it so we can retrieve the original data that was sent stream.Seek(0, SeekOrigin.Begin); data = await ReadStreamAsync(stream).ConfigureAwait(false); } else { data = "[Data only available in Trace LogLevel]"; } } _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); return new CallResult(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data)); } catch (JsonSerializationException jse) { if (data == null) { if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); data = await ReadStreamAsync(stream).ConfigureAwait(false); } else { data = "[Data only available in Trace LogLevel]"; } } _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}"); return new CallResult(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data)); } catch (Exception ex) { if (data == null) { if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); data = await ReadStreamAsync(stream).ConfigureAwait(false); } else { data = "[Data only available in Trace LogLevel]"; } } var exceptionInfo = ex.ToLogString(); _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}"); return new CallResult(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data)); } } private static async Task ReadStreamAsync(Stream stream) { using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); return await reader.ReadToEndAsync().ConfigureAwait(false); } /// /// Dispose /// public virtual void Dispose() { _disposing = true; AuthenticationProvider?.Dispose(); } } }