Skip to content
Open
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/Options/AcmebotOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public class AcmebotOptions

public IonosDnsOptions? IonosDns { get; set; }

public OvhOptions? OVH { get; set; }

public Route53Options? Route53 { get; set; }

public TransIpOptions? TransIp { get; set; }
Expand Down
10 changes: 10 additions & 0 deletions src/Acmebot.App/Options/OVHOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Acmebot.App.Options;

public class OvhOptions
{
public required string ApplicationKey { get; set; }

public required string ApplicationSecret { get; set; }

public required string ConsumerKey { get; set; }
}
1 change: 1 addition & 0 deletions src/Acmebot.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
dnsProviders.TryAdd(options.GoDaddy, o => new GoDaddyProvider(o));
dnsProviders.TryAdd(options.GoogleDns, o => new GoogleDnsProvider(o));
dnsProviders.TryAdd(options.IonosDns, o => new IonosDnsProvider(o));
dnsProviders.TryAdd(options.OVH, o => new OvhDnsProvider(o));
dnsProviders.TryAdd(options.Route53, o => new Route53Provider(o));
dnsProviders.TryAdd(options.TransIp, o => new TransIpProvider(options, o, credential));

Expand Down
260 changes: 260 additions & 0 deletions src/Acmebot.App/Providers/OVHProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

using Acmebot.App.Options;

using Newtonsoft.Json;

namespace Acmebot.App.Providers;

public class OvhDnsProvider : IDnsProvider

Check failure on line 16 in src/Acmebot.App/Providers/OVHProvider.cs

View workflow job for this annotation

GitHub Actions / build

'OvhDnsProvider' does not implement interface member 'IDnsProvider.CreateTxtRecordAsync(DnsZone, string, string[], CancellationToken)'

Check failure on line 16 in src/Acmebot.App/Providers/OVHProvider.cs

View workflow job for this annotation

GitHub Actions / build

'OvhDnsProvider' does not implement interface member 'IDnsProvider.CreateTxtRecordAsync(DnsZone, string, string[], CancellationToken)'
{

public OvhDnsProvider(OvhOptions options)
Comment on lines +16 to +19
{
_client = new OvhClient(options.ApplicationKey, options.ApplicationSecret, options.ConsumerKey);
}

private readonly OvhClient _client;

public string Name => "OVH";

public TimeSpan PropagationDelay => TimeSpan.FromSeconds(300);

public async Task<IReadOnlyList<DnsZone>> ListZonesAsync(CancellationToken cancellationToken = default)
{
var zones = await _client.ListZonesAsync();

return zones.Select(x => new DnsZone(this) { Id = x, Name = x }).ToArray();
}

public Task CreateTxtRecordAsync(DnsZone zone, string relativeRecordName, IEnumerable<string> values, CancellationToken cancellationToken = default)
{
var entries = values.Select(x => new DnsEntry { Name = relativeRecordName, Type = "TXT", TTL = 600, Data = x }).ToArray();
return _client.AddRecordAsync(zone.Name, entries);
}

public Task DeleteTxtRecordAsync(DnsZone zone, string relativeRecordName, CancellationToken cancellationToken = default)
{
return _client.DeleteRecordAsync(zone.Name, "TXT", relativeRecordName);
}
Comment on lines +30 to +46

private class OvhClient
{

public OvhClient(string applicationKey, string applicationSecret, string consumerKey)
{
_httpClient = new HttpClient();

ArgumentNullException.ThrowIfNull(applicationKey);
ArgumentNullException.ThrowIfNull(applicationSecret);
ArgumentNullException.ThrowIfNull(consumerKey);

_applicationKey = applicationKey;
_applicationSecret = applicationSecret;
_consumerKey = consumerKey;
}

private readonly HttpClient _httpClient;
private readonly string _applicationKey;
private readonly string _applicationSecret;
private readonly string _consumerKey;

public async Task<IReadOnlyList<string>> ListZonesAsync()
{
var url = "https://api.ovh.com/1.0/domain/zone";
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
var time = await GetTime();
var signature = GenerateSignature(_applicationSecret, _consumerKey,
time, requestMessage.Method.Method, url);

requestMessage.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage.Headers.Add("X-Ovh-Signature", signature);
requestMessage.Headers.Add("X-Ovh-Timestamp", time.ToString());


var response = await _httpClient.SendAsync(requestMessage);
response.EnsureSuccessStatusCode();
var domains = await response.Content.ReadFromJsonAsync<string[]>();
return domains!;
}
}


public async Task DeleteRecordAsync(string zone, string type, string relativeRecordName)
{

var recordIds = Array.Empty<string>();
var url = "https://api.ovh.com/1.0/domain/zone/" + zone + "/record";
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
var time = await GetTime();
var signature = GenerateSignature(_applicationSecret, _consumerKey,
time, requestMessage.Method.Method, url);

requestMessage.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage.Headers.Add("X-Ovh-Signature", signature);
requestMessage.Headers.Add("X-Ovh-Timestamp", time.ToString());


var response = await _httpClient.SendAsync(requestMessage);
response.EnsureSuccessStatusCode();
recordIds = await response.Content.ReadFromJsonAsync<string[]>();
}



foreach (var recordId in recordIds!)
Comment on lines +113 to +118
{
url = "https://api.ovh.com/1.0/domain/zone/" + zone + "/record/" + recordId;
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
var time = await GetTime();
var signature = GenerateSignature(_applicationSecret, _consumerKey,
time, requestMessage.Method.Method, url);

requestMessage.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage.Headers.Add("X-Ovh-Signature", signature);
requestMessage.Headers.Add("X-Ovh-Timestamp", time.ToString());

var response = await _httpClient.SendAsync(requestMessage);
response.EnsureSuccessStatusCode();
var ovhRecord = await response.Content.ReadFromJsonAsync<OvhRecord>();

if (ovhRecord!.fieldType == type && ovhRecord.subDomain == relativeRecordName)
{
Comment on lines +96 to +138
url = "https://api.ovh.com/1.0/domain/zone/" + zone + "/record/" + recordId;
using (var requestMessage2 =
new HttpRequestMessage(HttpMethod.Delete, url))
{
var time2 = await GetTime();
var signature2 = GenerateSignature(_applicationSecret, _consumerKey,
time2, requestMessage2.Method.Method, url);

Comment on lines +124 to +146
requestMessage2.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage2.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage2.Headers.Add("X-Ovh-Signature", signature2);
requestMessage2.Headers.Add("X-Ovh-Timestamp", time2.ToString());

var response2 = await _httpClient.SendAsync(requestMessage2);
response2.EnsureSuccessStatusCode();
}
}
}
}

}
public async Task AddRecordAsync(string zone, IReadOnlyList<DnsEntry> dnsEntries)
{
foreach (var dnsEntry in dnsEntries)
{
var recordUrl = "https://api.ovh.com/1.0/domain/zone/" + zone + "/record";
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Post, recordUrl))
{
var body = new
{
fieldType = dnsEntry.Type,
subDomain = dnsEntry.Name,
target = dnsEntry.Data,
ttl = dnsEntry.TTL
};
var bodyString = JsonConvert.SerializeObject(body);
requestMessage.Content = new StringContent(bodyString, Encoding.UTF8, "application/json");
var time = await GetTime();
var signature = GenerateSignature(_applicationSecret, _consumerKey,
time, requestMessage.Method.Method, recordUrl, bodyString);

requestMessage.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage.Headers.Add("X-Ovh-Signature", signature);
requestMessage.Headers.Add("X-Ovh-Timestamp", time.ToString());


var response = await _httpClient.SendAsync(requestMessage);
response.EnsureSuccessStatusCode();
}
}
var refreshUrl = "https://api.ovh.com/1.0/domain/zone/" + zone + "/refresh";
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Post, refreshUrl))
{
var time = await GetTime();
var signature = GenerateSignature(_applicationSecret, _consumerKey,
time, requestMessage.Method.Method, refreshUrl);

requestMessage.Headers.Add("X-Ovh-Application", _applicationKey);
requestMessage.Headers.Add("X-Ovh-Consumer", _consumerKey);
requestMessage.Headers.Add("X-Ovh-Signature", signature);
requestMessage.Headers.Add("X-Ovh-Timestamp", time.ToString());


var responseRefresh = await _httpClient.SendAsync(requestMessage);
responseRefresh.EnsureSuccessStatusCode();
}

}


private static string GenerateSignature(string applicationSecret, string consumerKey,
long currentTimestamp, string method, string target, string? data = null)
{

using (var sha1Hasher = SHA1.Create())
{
var toSign =
string.Join("+", applicationSecret, consumerKey, method,
target, data, currentTimestamp);
var binaryHash = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(toSign));
var signature = string.Join("",
binaryHash.Select(x => x.ToString("X2"))).ToLower();
return $"$1${signature}";
}
}

private async Task<long> GetTime()
{
var response = await _httpClient.GetAsync("https://api.ovh.com/1.0/auth/time");

response.EnsureSuccessStatusCode();
var time = await response.Content.ReadFromJsonAsync<long>();
return time;
}
}

private class OvhRecord
{
public string? id { get; set; }
public string? zone { get; set; }
public string? subDomain { get; set; }
public string? fieldType { get; set; }
public string? target { get; set; }
public int ttl { get; set; }
Comment on lines +240 to +245
}


private class DnsEntry
{
public string? Data { get; set; }

public string? Name { get; set; }

public int TTL { get; set; }

public string? Type { get; set; }
}

}
Loading