CancelationToken for evert async method
bdcbd5c9694edd5646df2d5eec324d7ee0901dbb
7 files changed
src/Vimexx_API.ConsoleApp/Program.cssrc/Vimexx_API/AuthToken.cssrc/Vimexx_API/DNSRecord.cssrc/Vimexx_API/GetDnsResponse.cssrc/Vimexx_API/Response.cssrc/Vimexx_API/VimexxApi.cssrc/Vimexx_API/Vimexx_API.csproj
diff --git a/src/Vimexx_API.ConsoleApp/Program.cs b/src/Vimexx_API.ConsoleApp/Program.cs
index 2f821d9..ae506d0 100644
--- a/src/Vimexx_API.ConsoleApp/Program.cs
+++ b/src/Vimexx_API.ConsoleApp/Program.cs
@@ -1,31 +1,25 @@
+using System.Text;
using Vimexx_API;
-var api = new VimexxApi();
+var VimexxClientId = "123";
+var VimexxClientKey = "85780975tklkjkl76Z7AYB3ajajJJK";
+var VimexxUsername = "info@example.com";
+var VimexxPassword = "AP@ss5w0rd";
-await api.LoginAsync("123", "5aYB3pmxJNA4r2YdltRuDdnVTlNnLe22Ti0k26Z7", "<userid>", "<password>", false);
+var cts = new CancellationTokenSource();
-var result = await api.LetsEncryptAsync("voorbeeld.nl", "<challenge>");
+var sb = new StringBuilder();
-Console.WriteLine($"result: {result.message}");
+var api = new VimexxApi(sb);
-// verwijderen letsencrypt record
-// result = await api.LetsEncryptAsync("voorbeeld.nl", null);
-// Console.WriteLine($"result: {result.message}");
+await api.LoginAsync(VimexxClientId, VimexxClientKey, VimexxUsername, VimexxPassword, false, cts.Token);
-//var getdnsresponse = await api.GetDNSAsync("voorbeeld.nl");
-//Console.WriteLine($"result: {getdnsresponse.message}");
-//if (getdnsresponse.result)
-//{
-// foreach (var r in getdnsresponse.data.dns_records)
-// {
-// Console.WriteLine($"{r.name}\t\t\t{r.type}\t{r.content}\t{r.prio}");
-// }
-//}
+var result = await api.GetDNSAsync("hw.nl", cts.Token);
+if (result?.Data?.DNSRecords != null)
+{
+ _ = await api.SaveDNSAsync("hw.nl", result.Data.DNSRecords, cts.Token);
+}
-//getdnsresponse.data.dns_records.Add(new DnsRecord() { name = "test1", type = "A", content = "1.2.3.4", ttl = 3600, prio = 0 });
-//var savednsresponse = await api.SaveDNSAsync("voorbeeld.nl", getdnsresponse.data.dns_records);
-//Console.WriteLine($"result: {savednsresponse.message}");
-
-//Console.WriteLine("end");
+Console.WriteLine(sb.ToString());
diff --git a/src/Vimexx_API/AuthToken.cs b/src/Vimexx_API/AuthToken.cs
index 5f9433d..ab46548 100644
--- a/src/Vimexx_API/AuthToken.cs
+++ b/src/Vimexx_API/AuthToken.cs
@@ -1,11 +1,20 @@
+using System.Text.Json.Serialization;
+
namespace Vimexx_API;
internal class AuthToken
{
- public string? token_type { get; set; }
- public int expires_in { get; set; }
- public string? access_token { get; set; }
- public string? refresh_token { get; set; }
+ [JsonPropertyName("token_type")]
+ public string? TokenType { get; set; }
+
+ [JsonPropertyName("expires_in")]
+ public int ExpiresIn { get; set; }
+
+ [JsonPropertyName("access_token")]
+ public string? AccessToken { get; set; }
+
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
}
diff --git a/src/Vimexx_API/DNSRecord.cs b/src/Vimexx_API/DNSRecord.cs
index e4dd2b8..6064369 100644
--- a/src/Vimexx_API/DNSRecord.cs
+++ b/src/Vimexx_API/DNSRecord.cs
@@ -1,11 +1,44 @@
+using System.Text.Json.Serialization;
+
namespace Vimexx_API;
+
+public enum RecordTypeEnum
+{
+ Unknown,
+ A,
+ AAAA,
+ CNAME,
+ MX,
+ SRV,
+ TLSA,
+ TXT,
+ CAA,
+ NS // as of 2025-09-28
+}
+
public class DnsRecord
{
- public string name { get; set; } = string.Empty;
- public string? type { get; set; }
- public string? content { get; set; }
- public object? prio { get; set; }
- public object? ttl { get; set; }
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ [JsonPropertyName("type")]
+ public RecordTypeEnum Type { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ [JsonPropertyName("prio")]
+ public string? Prio { get; set; }
+
+ [JsonPropertyName("weight")]
+ public int? Weight { get; set; }
+
+ [JsonPropertyName("port")]
+ public int? Port { get; set; }
+
+ [JsonPropertyName("ttl")]
+ public int? TTL { get; set; }
}
diff --git a/src/Vimexx_API/GetDnsResponse.cs b/src/Vimexx_API/GetDnsResponse.cs
index f36b68d..7c31be9 100644
--- a/src/Vimexx_API/GetDnsResponse.cs
+++ b/src/Vimexx_API/GetDnsResponse.cs
@@ -1,9 +1,11 @@
-
+using System.Text.Json.Serialization;
+
namespace Vimexx_API;
public class DnsRecords
{
- public List<DnsRecord>? dns_records { get; set; }
+ [JsonPropertyName("dns_records")]
+ public List<DnsRecord> DNSRecords { get; set; } = [];
}
public class GetDNSResponse : Response<DnsRecords>
diff --git a/src/Vimexx_API/Response.cs b/src/Vimexx_API/Response.cs
index 00798c8..7215a46 100644
--- a/src/Vimexx_API/Response.cs
+++ b/src/Vimexx_API/Response.cs
@@ -1,11 +1,18 @@
+using System.Text.Json.Serialization;
+
namespace Vimexx_API;
-public class Response<T>
+public class Response<T> where T : class
{
- public string? message { get; set; }
- public bool result { get; set; }
- public T? data { get; set; }
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ [JsonPropertyName("result")]
+ public bool Result { get; set; }
+
+ [JsonPropertyName("data")]
+ public T? Data { get; set; }
}
diff --git a/src/Vimexx_API/VimexxApi.cs b/src/Vimexx_API/VimexxApi.cs
index c026307..8c56ad2 100644
--- a/src/Vimexx_API/VimexxApi.cs
+++ b/src/Vimexx_API/VimexxApi.cs
@@ -1,14 +1,13 @@
-using System.Dynamic;
-using System.Text;
+using System.Text;
using System.Text.Json;
+// https://developers.whmcs.com/api/
namespace Vimexx_API;
public class VimexxApi(StringBuilder log)
{
- private const int TTL = 3600;
- private const string USER_AGENT = "Vimexx-WHMCS api agent .NET core 1.0";
+ private const string USER_AGENT = "Vimexx-WHMCS api agent .NET core 1.1";
private const string API_URL = "https://api.vimexx.nl";
private const string API_VERSION = "8.6.1-release.1";
@@ -18,7 +17,8 @@ public class VimexxApi(StringBuilder log)
private readonly StringBuilder log = log;
- async private Task<T?> RequestAsync<T>(HttpMethod method, string apiMethod, object data)
+ // we do serialization because vimexx tent to error a lot!! so we can debug
+ async private Task<T?> RequestAsync<T>(HttpMethod method, string apiMethod, object data, CancellationToken ct)
{
var jsonResult = string.Empty;
var jsonRequest = string.Empty;
@@ -31,60 +31,63 @@ public class VimexxApi(StringBuilder log)
try
{
- var httpClient = new HttpClient();
+ using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", USER_AGENT);
httpClient.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this.token.access_token);
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this.token.AccessToken);
jsonRequest = JsonSerializer.Serialize(new { body = data, version = API_VERSION });
- var request = new HttpRequestMessage(method, this.endpoint + apiMethod)
+ using var request = new HttpRequestMessage(method, this.endpoint + apiMethod)
{
Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json")
};
- var httpResponseMessage = await httpClient.SendAsync(request);
+ using var httpResponseMessage = await httpClient.SendAsync(request, ct);
httpResponseMessage.EnsureSuccessStatusCode();
if (httpResponseMessage.IsSuccessStatusCode)
{
- var stream = await httpResponseMessage.Content.ReadAsStreamAsync();
+ using var stream = await httpResponseMessage.Content.ReadAsStreamAsync(ct);
- var sr = new StreamReader(stream);
+ using var sr = new StreamReader(stream);
- jsonResult = await sr.ReadToEndAsync();
+ jsonResult = await sr.ReadToEndAsync(ct);
try
{
return JsonSerializer.Deserialize<T>(jsonResult);
}
- catch
+ catch(Exception ex)
{
- log.AppendLine($"Error: Deserialize: {apiMethod}{Environment.NewLine}{jsonRequest}{Environment.NewLine}{jsonResult}{Environment.NewLine}");
+ log.AppendLine($"Error: {ex.Message} {ex.InnerException?.Message} Deserialize: {method} {apiMethod}{Environment.NewLine}{jsonRequest}{Environment.NewLine}{jsonResult}{Environment.NewLine}");
}
}
}
catch(Exception eee)
{
- log.AppendLine($"Error: Exception: {eee.Message}: {apiMethod}{Environment.NewLine}{jsonRequest}{Environment.NewLine}{jsonResult}{Environment.NewLine}");
+ log.AppendLine($"Error: Exception: {eee.Message} {eee.InnerException?.Message}: {method} {apiMethod}{Environment.NewLine}{jsonRequest}{Environment.NewLine}{jsonResult}{Environment.NewLine}");
}
return default;
}
- async public Task LoginAsync(string clientId, string clientKey, string username, string password, bool testmodus = false)
+ async public Task<string?> LoginAsync(string clientId, string clientKey, string username, string password, bool testmodus, CancellationToken ct)
{
+ if (testmodus)
+ this.endpoint = API_URL + "/apitest/v1";
+
List<KeyValuePair<string, string>> data = new Dictionary<string, string>
{
- { "grant_type", "password" },
- { "client_id", clientId },
- { "client_secret", clientKey },
- { "username", username },
- { "password", password },
- { "scope", "whmcs-access" }
+ { "grant_type", "password" },
+ { "client_id", clientId },
+ { "client_secret", clientKey },
+ { "username", username },
+ { "password", password },
+ { "scope", "whmcs-access" }
}.ToList();
var httpClient = new HttpClient();
@@ -93,89 +96,113 @@ public class VimexxApi(StringBuilder log)
var req = new HttpRequestMessage(HttpMethod.Post, API_URL + "/auth/token") { Content = new FormUrlEncodedContent(data) };
- var httpResponseMessage = await httpClient.SendAsync(req);
+ var httpResponseMessage = await httpClient.SendAsync(req, ct);
httpResponseMessage.EnsureSuccessStatusCode();
if (httpResponseMessage.IsSuccessStatusCode)
{
- var stream = await httpResponseMessage.Content.ReadAsStreamAsync();
+ var stream = await httpResponseMessage.Content.ReadAsStreamAsync(ct);
- this.token = await JsonSerializer.DeserializeAsync<AuthToken>(stream);
- }
+ this.token = await JsonSerializer.DeserializeAsync<AuthToken>(stream, JsonSerializerOptions.Default, ct);
- if (testmodus)
- this.endpoint = API_URL + "/apitest/v1";
+ if(this.token != null)
+ return $"type:{this.token.TokenType} exp:{this.token.ExpiresIn}";
+ }
+ return null;
}
- async public Task<GetDNSResponse?> GetDNSAsync(string domainname)
+ async public Task<GetDNSResponse?> GetDNSAsync(string domainname, CancellationToken ct)
{
var args = domainname.Split('.');
- return await RequestAsync<GetDNSResponse>(HttpMethod.Post, "/whmcs/domain/dns", new { sld = args[0], tld = args[1] });
- }
+ GetDNSResponse? response = null;
- async public Task<SaveDNSResponse?> SaveDNSAsync(string domainname, List<DnsRecord> dns_records)
- {
- var args = domainname.Split('.');
+ for (int i = 1; i <= 5; i++)
+ {
+ response = await RequestAsync<GetDNSResponse>(HttpMethod.Post, "/whmcs/domain/dns", new { sld = args[^2], tld = args[^1] }, ct);
+ if (response != null)
+ break;
+ log.AppendLine($"\t\t\t\tError: GetDNSAsync try {i}");
- dns_records.Where(x => x.ttl == null).ToList().ForEach(x => x.ttl = TTL);
- dns_records.Where(x => x.type == "CAA").ToList().ForEach(x => x.ttl = TTL);
+ await Task.Delay(5000, ct);
+ }
- return await RequestAsync<SaveDNSResponse>(HttpMethod.Put, "/whmcs/domain/dns", new { sld = args[0], tld = args[1], dns_records });
+ return response;
}
- async public Task<Response<object>?> LetsEncryptAsync(string domainname, List<string> challenges)
+ async public Task<SaveDNSResponse?> SaveDNSAsync(string domainname, List<DnsRecord> dns_records, CancellationToken ct)
{
- var getdnsresponse = await GetDNSAsync(domainname);
+ var args = domainname.Split('.');
- if (getdnsresponse == null)
- {
- log.AppendLine($"Error: GetDNSAsync returned null on {domainname}");
- return null;
- }
+ dns_records.Where(x => x.TTL == null).ToList().ForEach(x => x.TTL = 3600); // set everything to an hour
- if (getdnsresponse.result == false)
- return new Response<object>() { result = getdnsresponse.result, message = getdnsresponse.message };
+ dns_records.Where(x => x.Type == RecordTypeEnum.SRV && x.Name.StartsWith("_smtp._tcp.") && x.Prio == "0" && x.Port == null)
+ .ToList()
+ .ForEach(x => x.Port = 465);
+ dns_records.Where(x => x.Type == RecordTypeEnum.SRV && x.Name.StartsWith("_smtp._tcp.") && x.Prio== "10" && x.Port == null)
+ .ToList()
+ .ForEach(x => x.Port = 587);
- if (getdnsresponse.data == null || getdnsresponse.data.dns_records == null)
- return null;
+ dns_records.Where(x => x.Type == RecordTypeEnum.SRV && x.Name.StartsWith("_imap._tcp.") && x.Prio == "0" && x.Port == null)
+ .ToList()
+ .ForEach(x => x.Port = 143);
+ dns_records.Where(x => x.Type == RecordTypeEnum.SRV && x.Name.StartsWith("_imap._tcp.") && x.Prio == "10" && x.Port == null)
+ .ToList()
+ .ForEach(x => x.Port = 993);
- var records = getdnsresponse.data.dns_records.Where(x => x.name.StartsWith("_acme-challenge.")).ToList();
+ dns_records.Where(x => x.Type == RecordTypeEnum.SRV && x.Weight == null).ToList().ForEach(x => x.Weight = 0);
- foreach(var challenge in challenges)
- records.Add(new DnsRecord() { name = "_acme-challenge", content = challenge, type = "TXT", ttl = 300 });
+ SaveDNSResponse? response = null;
- return await SaveDNSAsync(domainname, records);
+ for (int i = 0; i < 5; i++)
+ {
+ response = await RequestAsync<SaveDNSResponse>(HttpMethod.Put, "/whmcs/domain/dns", new { sld = args[^2], tld = args[^1], dns_records }, ct);
+ if (response != null)
+ break;
+ log.AppendLine($"\t\t\t\tError: SaveDNSAsync try {i}");
+ await Task.Delay(5000, ct);
+ }
+
+ return response;
}
- async public Task<Response<object>?> LetsEncryptAsync(string domainname, string challenge)
+ async public Task<Response<object>?> LetsEncryptAsync(string domainname, List<string> challenges, CancellationToken ct)
{
- var getdnsresponse = await GetDNSAsync(domainname);
+ var getdnsresponse = await GetDNSAsync(domainname, ct);
if (getdnsresponse == null)
{
- log.AppendLine($"Error: LetsEncryptAsync GetDNSAsync returns null");
+ log.AppendLine($"Error: GetDNSAsync returned null on {domainname}");
return null;
}
- if (getdnsresponse.result == false)
- return new Response<object>() { result = getdnsresponse.result, message = getdnsresponse.message };
-
- if (getdnsresponse.data == null || getdnsresponse.data.dns_records == null)
+ if (getdnsresponse.Result == false)
+ return new Response<object>() { Result = getdnsresponse.Result, Message = getdnsresponse.Message };
+
+ if(getdnsresponse.Data == null)
+ {
+ log.AppendLine($"Error: GetDNSAsync returned null Data on {domainname}");
return null;
+ }
- var records = getdnsresponse.data.dns_records;
+ // filter out, the old dns challenges
+ var records = getdnsresponse.Data.DNSRecords.Where(x => !x.Name.StartsWith("_acme-challenge.")).ToList();
- // clear all _acme-challenge records
- if (string.IsNullOrWhiteSpace(challenge))
- records = records.Where(x => x.name.StartsWith("_acme-challenge.")).ToList();
+ var name = "_acme-challenge";
- if(!string.IsNullOrWhiteSpace(challenge))
- records.Add(new DnsRecord() { name = "_acme-challenge", content = challenge, type = "TXT", ttl = 300 });
+ if (challenges.Count > 0)
+ {
+ var args = domainname.Split('.');
+ if (args.Length > 2)
+ name += "." + args[^3];
+ }
- return await SaveDNSAsync(domainname, records);
- }
+ // put in new challenges, or none, if challenges = empty array
+ foreach (var challenge in challenges)
+ records.Add(new DnsRecord() { Name = name, Content = challenge, Type = RecordTypeEnum.TXT, TTL = 300 });
+ return await SaveDNSAsync(domainname, records, ct);
+ }
}
\ No newline at end of file
diff --git a/src/Vimexx_API/Vimexx_API.csproj b/src/Vimexx_API/Vimexx_API.csproj
index e05aebc..2eb9743 100644
--- a/src/Vimexx_API/Vimexx_API.csproj
+++ b/src/Vimexx_API/Vimexx_API.csproj
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
- <ImplicitUsings>enable</ImplicitUsings>
- <Nullable>enable</Nullable>
- </PropertyGroup>
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
</Project>