Implemented Core client
This commit is contained in:
43
Cloudflare/Auth/ApiKeyAuthentication.cs
Normal file
43
Cloudflare/Auth/ApiKeyAuthentication.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the interface to authenticate using an API key and email address.
|
||||
/// </summary>
|
||||
public class ApiKeyAuthentication : IAuthentication
|
||||
{
|
||||
private static readonly Regex _emailCheckRegex = new(@"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$", RegexOptions.Compiled);
|
||||
private readonly string _emailAddress;
|
||||
private readonly string _apiKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiKeyAuthentication"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emailAddress">The email address.</param>
|
||||
/// <param name="apiKey">The global API key.</param>
|
||||
public ApiKeyAuthentication(string emailAddress, string apiKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(emailAddress))
|
||||
throw new ArgumentNullException(nameof(emailAddress));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
throw new ArgumentNullException(nameof(apiKey));
|
||||
|
||||
if (!_emailCheckRegex.IsMatch(emailAddress))
|
||||
throw new ArgumentException("Invalid email address", nameof(emailAddress));
|
||||
|
||||
_emailAddress = emailAddress;
|
||||
_apiKey = apiKey;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddHeader(HttpClient httpClient)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add("X-Auth-Email", _emailAddress);
|
||||
httpClient.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Cloudflare/Auth/ApiTokenAuthentication.cs
Normal file
32
Cloudflare/Auth/ApiTokenAuthentication.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the interface to authenticate using an API token.
|
||||
/// </summary>
|
||||
public class ApiTokenAuthentication : IAuthentication
|
||||
{
|
||||
private readonly string _apiToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiTokenAuthentication"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiToken">The API token.</param>
|
||||
public ApiTokenAuthentication(string apiToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(apiToken))
|
||||
throw new ArgumentNullException(nameof(apiToken));
|
||||
|
||||
_apiToken = apiToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddHeader(HttpClient httpClient)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Cloudflare/ClientOptions.cs
Normal file
55
Cloudflare/ClientOptions.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the Cloudflare API.
|
||||
/// </summary>
|
||||
public class ClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default base url for the API.
|
||||
/// </summary>
|
||||
public virtual string BaseUrl { get; set; } = "https://api.cloudflare.com/client/v4/";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default timeout for the API.
|
||||
/// </summary>
|
||||
public virtual TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of retries for the API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The API may respond with an 5xx error and a X-Should-Retry header indicating that the request should be retried.
|
||||
/// </remarks>
|
||||
public virtual int MaxRetries { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional default headers to every request.
|
||||
/// </summary>
|
||||
public virtual IDictionary<string, string> DefaultHeaders { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional default query parameters to every request.
|
||||
/// </summary>
|
||||
public virtual IDictionary<string, string> DefaultQueryParams { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to allow redirects.
|
||||
/// </summary>
|
||||
public virtual bool AllowRedirects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use a proxy.
|
||||
/// </summary>
|
||||
public virtual bool UseProxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy information.
|
||||
/// </summary>
|
||||
public virtual IWebProxy Proxy { get; set; }
|
||||
}
|
||||
}
|
||||
62
Cloudflare/Cloudflare.csproj
Normal file
62
Cloudflare/Cloudflare.csproj
Normal file
@@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
|
||||
|
||||
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/AM-WD/cloudflare-api.git</RepositoryUrl>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
|
||||
<PackageIcon>package-icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageTags>cloudflare api</PackageTags>
|
||||
<PackageProjectUrl>https://developers.cloudflare.com/api</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>false</EmbedUntrackedSources>
|
||||
|
||||
<PackageId>AMWD.Net.API.Cloudflare</PackageId>
|
||||
<AssemblyName>amwd-cloudflare-core</AssemblyName>
|
||||
<RootNamespace>AMWD.Net.Api.Cloudflare</RootNamespace>
|
||||
|
||||
<Product>Cloudflare API - Core</Product>
|
||||
<Description>Core features of the Cloudflare API</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^v[0-9.]+'))">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" />
|
||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
324
Cloudflare/CloudflareClient.cs
Normal file
324
Cloudflare/CloudflareClient.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the Core of the Cloudflare API client.
|
||||
/// </summary>
|
||||
public partial class CloudflareClient : ICloudflareClient, IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
Formatting = Formatting.None,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
};
|
||||
|
||||
private readonly ClientOptions _clientOptions;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private bool _isDisposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emailAddress">The email address of the Cloudflare account.</param>
|
||||
/// <param name="apiKey">The API key of the Cloudflare account.</param>
|
||||
/// <param name="clientOptions">The client options (optional).</param>
|
||||
public CloudflareClient(string emailAddress, string apiKey, ClientOptions clientOptions = null)
|
||||
: this(new ApiKeyAuthentication(emailAddress, apiKey), clientOptions)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiToken">The API token.</param>
|
||||
/// <param name="clientOptions">The client options (optional).</param>
|
||||
public CloudflareClient(string apiToken, ClientOptions clientOptions = null)
|
||||
: this(new ApiTokenAuthentication(apiToken), clientOptions)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authentication">The authentication information.</param>
|
||||
/// <param name="clientOptions">The client options (optional).</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the resources used by the <see cref="CloudflareClient"/> object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_httpClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(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<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath);
|
||||
|
||||
HttpContent httpRequestContent;
|
||||
if (request is HttpContent httpContent)
|
||||
{
|
||||
httpRequestContent = httpContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
|
||||
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var response = await _httpClient.PostAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
|
||||
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath);
|
||||
|
||||
HttpContent httpRequestContent;
|
||||
if (request is HttpContent httpContent)
|
||||
{
|
||||
httpRequestContent = httpContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
|
||||
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var response = await _httpClient.PutAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
|
||||
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath);
|
||||
|
||||
var response = await _httpClient.DeleteAsync(requestUrl, cancellationToken).ConfigureAwait(false);
|
||||
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath);
|
||||
|
||||
HttpContent httpRequestContent;
|
||||
if (request is HttpContent httpContent)
|
||||
{
|
||||
httpRequestContent = httpContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
|
||||
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var response = await _httpClient.PatchAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var httpRequestMessage = new HttpRequestMessage
|
||||
{
|
||||
Version = HttpVersion.Version11,
|
||||
Method = new HttpMethod("PATCH"),
|
||||
RequestUri = new Uri(requestUrl),
|
||||
Content = httpRequestContent,
|
||||
};
|
||||
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
return await GetCloudflareResponse<TResponse>(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<AssemblyInformationalVersionAttribute>()
|
||||
.InformationalVersion;
|
||||
|
||||
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 async Task<CloudflareResponse<TRes>> GetCloudflareResponse<TRes>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Forbidden:
|
||||
case HttpStatusCode.Unauthorized:
|
||||
var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content);
|
||||
throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}")));
|
||||
|
||||
default:
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<CloudflareResponse<TRes>>(content);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (typeof(TRes) == typeof(string))
|
||||
{
|
||||
object cObj = content.Replace("\\n", Environment.NewLine);
|
||||
return new CloudflareResponse<TRes>
|
||||
{
|
||||
Success = true,
|
||||
ResultInfo = new ResultInfo(),
|
||||
Result = (TRes)cObj,
|
||||
};
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildRequestUrl(string requestPath, IQueryParameterFilter queryFilter = null)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Cloudflare/Enums/FilterMatchType.cs
Normal file
24
Cloudflare/Enums/FilterMatchType.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to match all search requirements or at least one (any).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum FilterMatchType
|
||||
{
|
||||
/// <summary>
|
||||
/// Match all search requirements.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "all")]
|
||||
All = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Match at least one search requirement.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "any")]
|
||||
Any = 2,
|
||||
}
|
||||
}
|
||||
24
Cloudflare/Enums/OrderDirection.cs
Normal file
24
Cloudflare/Enums/OrderDirection.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// The direction to order the entity.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum OrderDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Order in ascending order.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "asc")]
|
||||
Asc = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Order in descending order.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "desc")]
|
||||
Desc = 2
|
||||
}
|
||||
}
|
||||
61
Cloudflare/Exceptions/CloudflareException.cs
Normal file
61
Cloudflare/Exceptions/CloudflareException.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occur during Cloudflare API calls.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class CloudflareException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareException"/> class.
|
||||
/// </summary>
|
||||
public CloudflareException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
|
||||
/// message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public CloudflareException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
|
||||
/// message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception, or a <see langword="null"/> reference
|
||||
/// if no inner exception is specified.
|
||||
/// </param>
|
||||
public CloudflareException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudflareException"/> class with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">
|
||||
/// The <see cref="SerializationInfo"/> that holds the serialized
|
||||
/// object data about the exception being thrown.
|
||||
/// </param>
|
||||
/// <param name="context">
|
||||
/// The <see cref="StreamingContext"/> that contains contextual information
|
||||
/// about the source or destination.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">The info parameter is null.</exception>
|
||||
/// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0).</exception>
|
||||
protected CloudflareException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Cloudflare/Extensions/EnumExtensions.cs
Normal file
33
Cloudflare/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Enum"/>s.
|
||||
/// </summary>
|
||||
public static class EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="EnumMemberAttribute.Value"/> of the <see cref="Enum"/> when available, otherwise the <see cref="Enum.ToString()"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value.</param>
|
||||
public static string GetEnumMemberValue(this Enum value)
|
||||
{
|
||||
var fieldInfo = value.GetType().GetField(value.ToString());
|
||||
if (fieldInfo == null)
|
||||
return value.ToString();
|
||||
|
||||
var enumMember = fieldInfo
|
||||
.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: false)
|
||||
.Cast<EnumMemberAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (enumMember == null)
|
||||
return value.ToString();
|
||||
|
||||
return enumMember.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Cloudflare/Interfaces/IAuthentication.cs
Normal file
16
Cloudflare/Interfaces/IAuthentication.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the interface to add authentication information.
|
||||
/// </summary>
|
||||
public interface IAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds authentication headers to the given <see cref="HttpClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">The <see cref="HttpClient"/> to add the headers to.</param>
|
||||
void AddHeader(HttpClient httpClient);
|
||||
}
|
||||
}
|
||||
75
Cloudflare/Interfaces/ICloudflareClient.cs
Normal file
75
Cloudflare/Interfaces/ICloudflareClient.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a client for the Cloudflare API.
|
||||
/// </summary>
|
||||
public interface ICloudflareClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes a GET request to the Cloudflare API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The GET method requests a representation of the specified resource.
|
||||
/// Requests using GET should only retrieve data and should not contain a request content.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <param name="requestPath">The request path (extending the base URL).</param>
|
||||
/// <param name="queryFilter">The query parameters.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(string requestPath, IQueryParameterFilter queryFilter = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a POST request to the Cloudflare API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <typeparam name="TRequest">The request type.</typeparam>
|
||||
/// <param name="requestPath">The request path (extending the base URL).</param>
|
||||
/// <param name="request">The request content.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a PUT request to the Cloudflare API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The PUT method replaces all current representations of the target resource with the request content.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <typeparam name="TRequest">The request type.</typeparam>
|
||||
/// <param name="requestPath">The request path (extending the base URL).</param>
|
||||
/// <param name="request">The request content.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a DELETE request to the Cloudflare API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The DELETE method deletes the specified resource.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <param name="requestPath">The request path (extending the base URL).</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
/// <returns></returns>
|
||||
Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a PATCH request to the Cloudflare API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The PATCH method applies partial modifications to a resource.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TResponse">The response type.</typeparam>
|
||||
/// <typeparam name="TRequest">The request type.</typeparam>
|
||||
/// <param name="requestPath">The request path (extending the base URL).</param>
|
||||
/// <param name="request">The request content.</param>
|
||||
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
|
||||
Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
15
Cloudflare/Interfaces/IQueryParameterFilter.cs
Normal file
15
Cloudflare/Interfaces/IQueryParameterFilter.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents filter options defined via query parameters.
|
||||
/// </summary>
|
||||
public interface IQueryParameterFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the query parameters.
|
||||
/// </summary>
|
||||
IDictionary<string, string> GetQueryParameters();
|
||||
}
|
||||
}
|
||||
21
Cloudflare/Models/AccountBase.cs
Normal file
21
Cloudflare/Models/AccountBase.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation of an account.
|
||||
/// </summary>
|
||||
public class AccountBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier
|
||||
/// </summary>
|
||||
// <= 32 characters
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the account.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
27
Cloudflare/Models/OwnerBase.cs
Normal file
27
Cloudflare/Models/OwnerBase.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation of an owner.
|
||||
/// </summary>
|
||||
public class OwnerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier.
|
||||
/// </summary>
|
||||
// <= 32 characters
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the owner.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of owner.
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
21
Cloudflare/README.md
Normal file
21
Cloudflare/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Cloudflare API for .NET | Core
|
||||
|
||||
This is the core package for all extensions of the Cloudflare API implemented by [AM.WD].
|
||||
|
||||
## Contents
|
||||
|
||||
- The `(I)CloudflareClient` with base calls for `GET`, `POST`, `PUT`, `PATCH` and `DELETE` requests.
|
||||
- Base classes to receive responses.
|
||||
- `CloudflareException` to specify some errors.
|
||||
- `IAuthentication` implementations to allow API-Token and API-Key (legacy) authentication.
|
||||
|
||||
Any specific request will be defined via extension packages.
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [choose a license])
|
||||
|
||||
|
||||
|
||||
[AM.WD]: https://www.nuget.org/packages?q=AMWD.&sortby=created-desc
|
||||
[choose a license]: https://choosealicense.com/licenses/mit/
|
||||
47
Cloudflare/Responses/CloudflareResponse.cs
Normal file
47
Cloudflare/Responses/CloudflareResponse.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// The base Cloudflare response.
|
||||
/// </summary>
|
||||
public class CloudflareResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about the result of the request.
|
||||
/// </summary>
|
||||
[JsonProperty("result_info")]
|
||||
public ResultInfo ResultInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the API call was successful.
|
||||
/// </summary>
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Errors returned by the API call.
|
||||
/// </summary>
|
||||
[JsonProperty("errors")]
|
||||
public IReadOnlyList<ResponseInfo> Errors { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Messages returned by the API call.
|
||||
/// </summary>
|
||||
[JsonProperty("messages")]
|
||||
public IReadOnlyList<ResponseInfo> Messages { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base Cloudflare response with a result.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class CloudflareResponse<T> : CloudflareResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of the API call.
|
||||
/// </summary>
|
||||
[JsonProperty("result")]
|
||||
public T Result { get; set; }
|
||||
}
|
||||
}
|
||||
20
Cloudflare/Responses/ResponseInfo.cs
Normal file
20
Cloudflare/Responses/ResponseInfo.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// A Cloudflare response information.
|
||||
/// </summary>
|
||||
public class ResponseInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The message code.
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message.
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
32
Cloudflare/Responses/ResultInfo.cs
Normal file
32
Cloudflare/Responses/ResultInfo.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace AMWD.Net.Api.Cloudflare
|
||||
{
|
||||
/// <summary>
|
||||
/// Cloudflare Result Information.
|
||||
/// </summary>
|
||||
public class ResultInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of results for the requested service.
|
||||
/// </summary>
|
||||
[JsonProperty("count")]
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current page within paginated list of results.
|
||||
/// </summary>
|
||||
[JsonProperty("page")]
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of results per page of results.
|
||||
/// </summary>
|
||||
[JsonProperty("per_page")]
|
||||
public int PerPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total results available without any search parameters.
|
||||
/// </summary>
|
||||
[JsonProperty("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user