Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Acmebot.App/Providers/AkamaiEdgeDnsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public AkamaiEdgeDnsClient(string host, string clientToken, string clientSecret,
_httpClient = EdgeGridSigner.CreateHttpClient(new EdgeGridCredentials(host, clientToken, clientSecret, accessToken));

_httpClient.BaseAddress = new Uri($"https://{host}/config-dns/v2/");

DnsProviderHttpClient.AddUserAgent(_httpClient);
}

private readonly HttpClient _httpClient;
Expand Down
6 changes: 1 addition & 5 deletions src/Acmebot.App/Providers/CloudflareProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,8 @@ private class CloudflareClient
{
public CloudflareClient(string apiToken)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.cloudflare.com/client/v4/")
};
_httpClient = DnsProviderHttpClient.Create("https://api.cloudflare.com/client/v4/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
}

Expand Down
7 changes: 1 addition & 6 deletions src/Acmebot.App/Providers/CustomDnsProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -60,12 +59,8 @@ private class CustomDnsClient
{
public CustomDnsClient(string endpoint, string apiKey, string apiKeyHeaderName)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri(endpoint)
};
_httpClient = DnsProviderHttpClient.Create(endpoint);

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation(apiKeyHeaderName, apiKey);
}

Expand Down
8 changes: 1 addition & 7 deletions src/Acmebot.App/Providers/DnsMadeEasyProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
Expand Down Expand Up @@ -67,12 +66,7 @@ private class DnsMadeEasyClient
{
public DnsMadeEasyClient(string apiKey, string secretKey)
{
_httpClient = new HttpClient(new ApiKeyHandler(apiKey, secretKey))
{
BaseAddress = new Uri("https://api.dnsmadeeasy.com/V2.0/dns/")
};

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient = DnsProviderHttpClient.Create("https://api.dnsmadeeasy.com/V2.0/dns/", new ApiKeyHandler(apiKey, secretKey));
}

private readonly HttpClient _httpClient;
Expand Down
36 changes: 36 additions & 0 deletions src/Acmebot.App/Providers/DnsProviderHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Net.Http.Headers;

using Acmebot.App.Infrastructure;

namespace Acmebot.App.Providers;

/// <summary>
/// Creates the <see cref="HttpClient"/> instances that DNS providers use to talk to their REST APIs.
/// A User-Agent is required in practice: some provider edges (IONOS is one) reject requests without one
/// with a 5xx before authentication is even attempted, and the resulting failure surfaces only as an
/// empty zone list. Centralizing the defaults here keeps the header from being forgotten by a provider.
/// </summary>
internal static class DnsProviderHttpClient
{
private static readonly ProductInfoHeaderValue s_userAgent = new("Acmebot", Constants.ApplicationVersion);

public static HttpClient Create(string baseAddress, HttpMessageHandler? handler = null) => Create(new Uri(baseAddress), handler);

public static HttpClient Create(Uri baseAddress, HttpMessageHandler? handler = null)
{
var httpClient = handler is null ? new HttpClient() : new HttpClient(handler);

httpClient.BaseAddress = baseAddress;

httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

AddUserAgent(httpClient);

return httpClient;
}

/// <summary>
/// Applies the User-Agent to a client that was created elsewhere, such as by a provider SDK.
/// </summary>
public static void AddUserAgent(HttpClient httpClient) => httpClient.DefaultRequestHeaders.UserAgent.Add(s_userAgent);
}
6 changes: 1 addition & 5 deletions src/Acmebot.App/Providers/GandiLiveDnsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,8 @@ private class GandiLiveDnsClient
{
public GandiLiveDnsClient(string apiKey)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.gandi.net/v5/")
};
_httpClient = DnsProviderHttpClient.Create("https://api.gandi.net/v5/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}

Expand Down
6 changes: 1 addition & 5 deletions src/Acmebot.App/Providers/GoDaddyProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,8 @@ private class GoDaddyClient
{
public GoDaddyClient(string apiKey, string apiSecret)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.godaddy.com/v1/")
};
_httpClient = DnsProviderHttpClient.Create("https://api.godaddy.com/v1/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("sso-key", $"{apiKey}:{apiSecret}");
}

Expand Down
7 changes: 1 addition & 6 deletions src/Acmebot.App/Providers/IonosDnsProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -69,12 +68,8 @@ private class IonosDnsClient
{
public IonosDnsClient(string apiKey)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.hosting.ionos.com/dns/v1/")
};
_httpClient = DnsProviderHttpClient.Create("https://api.hosting.ionos.com/dns/v1/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("X-API-Key", apiKey);
}

Expand Down
8 changes: 1 addition & 7 deletions src/Acmebot.App/Providers/OvhProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
Expand Down Expand Up @@ -69,12 +68,7 @@ private class OvhClient
public OvhClient(string endpoint, string applicationKey, string applicationSecret, string consumerKey)
{
// DNS providers in this project own their API clients; this one is constructed once by the singleton provider.
_httpClient = new HttpClient(new ApiKeyHandler(applicationKey, applicationSecret, consumerKey))
{
BaseAddress = new Uri(NormalizeEndpoint(endpoint))
};

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient = DnsProviderHttpClient.Create(NormalizeEndpoint(endpoint), new ApiKeyHandler(applicationKey, applicationSecret, consumerKey));
}

private readonly HttpClient _httpClient;
Expand Down
7 changes: 1 addition & 6 deletions src/Acmebot.App/Providers/PowerDnsProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -70,12 +69,8 @@ public PowerDnsClient(Uri endpoint, string apiKey, string serverId)

var baseAddress = endpoint.AbsoluteUri.EndsWith('/') ? endpoint : new Uri(endpoint.AbsoluteUri + "/");

_httpClient = new HttpClient
{
BaseAddress = baseAddress
};
_httpClient = DnsProviderHttpClient.Create(baseAddress);

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("X-API-Key", apiKey);

_serverId = Uri.EscapeDataString(serverId);
Expand Down
7 changes: 1 addition & 6 deletions src/Acmebot.App/Providers/RegfishProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -90,12 +89,8 @@ public RegfishClient(string apiKey)
{
ArgumentException.ThrowIfNullOrWhiteSpace(apiKey);

_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.regfish.com/")
};
_httpClient = DnsProviderHttpClient.Create("https://api.regfish.com/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-api-key", apiKey);
}

Expand Down
9 changes: 2 additions & 7 deletions src/Acmebot.App/Providers/TransIpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,7 @@ private class TransIpClient
{
public TransIpClient(string customerName, CryptographyClient cryptoClient)
{
_httpClient = new HttpClient(new TransIpSignHandler(customerName, cryptoClient))
{
BaseAddress = new Uri("https://api.transip.nl/v6/")
};

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient = DnsProviderHttpClient.Create("https://api.transip.nl/v6/", new TransIpSignHandler(customerName, cryptoClient));
}

private readonly HttpClient _httpClient;
Expand Down Expand Up @@ -127,7 +122,7 @@ public async Task CreateRecordAsync(string zoneName, DnsEntry entry, Cancellatio

private class TransIpSignHandler(string customerName, CryptographyClient cryptoClient) : DelegatingHandler(new HttpClientHandler())
{
private readonly HttpClient _httpClient = new() { BaseAddress = new Uri("https://api.transip.nl/v6/") };
private readonly HttpClient _httpClient = DnsProviderHttpClient.Create("https://api.transip.nl/v6/");

private TransIpToken? _token;

Expand Down
8 changes: 1 addition & 7 deletions src/Acmebot.App/Providers/UnitedDomainsProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -60,14 +59,9 @@ private class UnitedDomainsClient
{
public UnitedDomainsClient(string apiKey)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://dnsapi.united-domains.de/dns/")
};
_httpClient = DnsProviderHttpClient.Create("https://dnsapi.united-domains.de/dns/");

_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("acmebot");
}

private readonly HttpClient _httpClient;
Expand Down
46 changes: 46 additions & 0 deletions tests/Acmebot.App.Tests/DnsProviderHttpClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Acmebot.App.Providers;

using Xunit;

namespace Acmebot.App.Tests;

public sealed class DnsProviderHttpClientTests
{
[Fact]
public void Create_SetsBaseAddressAndJsonAccept()
{
using var httpClient = DnsProviderHttpClient.Create("https://api.example.com/v1/");

Assert.Equal(new Uri("https://api.example.com/v1/"), httpClient.BaseAddress);
Assert.Contains(httpClient.DefaultRequestHeaders.Accept, x => x.MediaType == "application/json");
}

[Fact]
public void Create_SendsAcmebotUserAgent()
{
using var httpClient = DnsProviderHttpClient.Create("https://api.example.com/v1/");

var userAgent = Assert.Single(httpClient.DefaultRequestHeaders.UserAgent);

Assert.Equal("Acmebot", userAgent.Product?.Name);
Assert.False(string.IsNullOrEmpty(userAgent.Product?.Version));
}

[Fact]
public void Create_WithHandler_SendsAcmebotUserAgent()
{
using var httpClient = DnsProviderHttpClient.Create("https://api.example.com/v1/", new HttpClientHandler());

Assert.Contains(httpClient.DefaultRequestHeaders.UserAgent, x => x.Product?.Name == "Acmebot");
}

[Fact]
public void AddUserAgent_AppliesToClientCreatedElsewhere()
{
using var httpClient = new HttpClient();

DnsProviderHttpClient.AddUserAgent(httpClient);

Assert.Contains(httpClient.DefaultRequestHeaders.UserAgent, x => x.Product?.Name == "Acmebot");
}
}