using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; using AMWD.Net.Api.Cloudflare.Auth; namespace AMWD.Net.Api.Cloudflare { /// /// Implements the Core of the Cloudflare API client. /// public class CloudflareClient : ICloudflareClient, IDisposable { private static readonly JsonSerializerSettings _jsonSerializerSettings = new() { Culture = CultureInfo.InvariantCulture, Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore, }; private readonly ClientOptions _clientOptions; private readonly HttpClient _httpClient; private bool _isDisposed = false; /// /// Initializes a new instance of the class. /// /// The email address of the Cloudflare account. /// The API key of the Cloudflare account. /// The client options (optional). public CloudflareClient(string emailAddress, string apiKey, ClientOptions? clientOptions = null) : this(new ApiKeyAuthentication(emailAddress, apiKey), clientOptions) { } /// /// Initializes a new instance of the class. /// /// The API token. /// The client options (optional). public CloudflareClient(string apiToken, ClientOptions? clientOptions = null) : this(new ApiTokenAuthentication(apiToken), clientOptions) { } /// /// Initializes a new instance of the class. /// /// The authentication information. /// The client options (optional). public CloudflareClient(IAuthentication authentication, ClientOptions? clientOptions = null) { if (authentication == null) throw new ArgumentNullException(nameof(authentication)); _clientOptions = clientOptions ?? new ClientOptions(); ValidateClientOptions(); _httpClient = CreateHttpClient(); authentication.AddHeader(_httpClient); } /// /// Disposes of the resources used by the object. /// public void Dispose() { if (_isDisposed) return; _isDisposed = true; _httpClient.Dispose(); GC.SuppressFinalize(this); } /// public async Task> GetAsync(string requestPath, IQueryParameterFilter? queryFilter = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); ValidateRequestPath(requestPath); string requestUrl = BuildRequestUrl(requestPath, queryFilter); var response = await _httpClient.GetAsync(requestUrl, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse(response, cancellationToken).ConfigureAwait(false); } /// public async Task> PostAsync(string requestPath, TRequest? request, IQueryParameterFilter? queryFilter = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); ValidateRequestPath(requestPath); string requestUrl = BuildRequestUrl(requestPath, queryFilter); var httpContent = ConvertRequest(request); var response = await _httpClient.PostAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse(response, cancellationToken).ConfigureAwait(false); } /// public async Task> PutAsync(string requestPath, TRequest? request, CancellationToken cancellationToken = default) { ThrowIfDisposed(); ValidateRequestPath(requestPath); string requestUrl = BuildRequestUrl(requestPath); var httpContent = ConvertRequest(request); var response = await _httpClient.PutAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse(response, cancellationToken).ConfigureAwait(false); } /// public async Task> DeleteAsync(string requestPath, IQueryParameterFilter? queryFilter = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); ValidateRequestPath(requestPath); string requestUrl = BuildRequestUrl(requestPath, queryFilter); var response = await _httpClient.DeleteAsync(requestUrl, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse(response, cancellationToken).ConfigureAwait(false); } /// public async Task> PatchAsync(string requestPath, TRequest? request, CancellationToken cancellationToken = default) { ThrowIfDisposed(); ValidateRequestPath(requestPath); string requestUrl = BuildRequestUrl(requestPath); var httpContent = ConvertRequest(request); var response = await _httpClient.PatchAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false); return await GetCloudflareResponse(response, cancellationToken).ConfigureAwait(false); } private void ThrowIfDisposed() { if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); } private void ValidateClientOptions() { if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl)) throw new ArgumentNullException(nameof(_clientOptions.BaseUrl)); if (_clientOptions.Timeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(_clientOptions.Timeout), "Timeout must be positive."); if (_clientOptions.MaxRetries < 0 || 10 < _clientOptions.MaxRetries) throw new ArgumentOutOfRangeException(nameof(_clientOptions.MaxRetries), "MaxRetries should be between 0 and 10."); if (_clientOptions.UseProxy && _clientOptions.Proxy == null) throw new ArgumentNullException(nameof(_clientOptions.Proxy)); } private void ValidateRequestPath(string requestPath) { if (string.IsNullOrWhiteSpace(requestPath)) throw new ArgumentNullException(nameof(requestPath)); if (requestPath.Contains("?")) throw new ArgumentException("Query parameters are not allowed", nameof(requestPath)); } private HttpClient CreateHttpClient() { string version = typeof(CloudflareClient).Assembly .GetCustomAttribute() ?.InformationalVersion ?? "unknown"; HttpMessageHandler handler; try { handler = new HttpClientHandler { AllowAutoRedirect = _clientOptions.AllowRedirects, UseProxy = _clientOptions.UseProxy, Proxy = _clientOptions.Proxy, }; } catch (PlatformNotSupportedException) { handler = new HttpClientHandler { AllowAutoRedirect = _clientOptions.AllowRedirects, }; } var client = new HttpClient(handler, true) { BaseAddress = new Uri(_clientOptions.BaseUrl), Timeout = _clientOptions.Timeout, }; client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", version)); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (_clientOptions.DefaultHeaders.Count > 0) { foreach (var headerKvp in _clientOptions.DefaultHeaders) client.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value); } return client; } private static async Task> GetCloudflareResponse(HttpResponseMessage httpResponse, CancellationToken cancellationToken) { #if NET6_0_OR_GREATER string content = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); #else string content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); #endif switch (httpResponse.StatusCode) { case HttpStatusCode.Forbidden: case HttpStatusCode.Unauthorized: var errorResponse = JsonConvert.DeserializeObject>(content, _jsonSerializerSettings) ?? throw new CloudflareException("Response is not a valid Cloudflare API response."); throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}"))); default: try { var response = JsonConvert.DeserializeObject>(content) ?? throw new CloudflareException("Response is not a valid Cloudflare API response."); return response; } catch { if (typeof(TRes) == typeof(string)) { object cObj = content.Replace("\\n", Environment.NewLine); return new CloudflareResponse { Success = true, Result = (TRes)cObj, }; } throw; } } } private string BuildRequestUrl(string requestPath, IQueryParameterFilter? queryFilter = null) { var dict = new Dictionary(); if (_clientOptions.DefaultQueryParams.Count > 0) { foreach (var paramKvp in _clientOptions.DefaultQueryParams) dict[paramKvp.Key] = paramKvp.Value; } var queryParams = queryFilter?.GetQueryParameters(); if (queryParams?.Count > 0) { foreach (var kvp in queryParams) dict[kvp.Key] = kvp.Value; } if (dict.Count == 0) return requestPath; string[] param = dict.Select(kvp => $"{kvp.Key}={WebUtility.UrlEncode(kvp.Value)}").ToArray(); string query = string.Join("&", param); return $"{requestPath}?{query}"; } private static HttpContent? ConvertRequest(T request) { if (request == null) return null; if (request is HttpContent httpContent) return httpContent; string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings); return new StringContent(json, Encoding.UTF8, "application/json"); } } }