Files
dashcaddy/desktop/Services/DnsClient.cs
T
Hermes 86e4c9fc81 wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park.
Preserved from fragile git stash to named branch 2026-08-23.
NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not
cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree.
Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
2026-08-22 23:50:34 -07:00

382 lines
14 KiB
C#

using System.Net.Http.Json;
using DashCaddy.Desktop.Models;
namespace DashCaddy.Desktop.Services;
public enum DnsProviderType
{
None, // No DNS management - use existing DNS
Technitium,
CoreDNS,
Cloudflare,
Route53,
Custom
}
public class DnsProviderConfig
{
public DnsProviderType Type { get; set; }
public string Name { get; set; }
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
public string ZoneId { get; set; }
public Dictionary<string, string> Extra { get; set; } = new();
}
public class DnsClient
{
private readonly HttpClient _httpClient;
private readonly DnsProviderConfig _config;
public DnsProviderType ProviderType => _config?.Type ?? DnsProviderType.None;
public DnsClient(DnsProviderConfig config)
{
_config = config;
if (config?.Type != DnsProviderType.None)
{
_httpClient = new HttpClient { BaseAddress = new Uri(config.BaseUrl) };
if (!string.IsNullOrEmpty(config.ApiKey))
{
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
}
}
}
public static DnsClient Create(DnsProviderType type, Dictionary<string, string> settings)
{
if (type == DnsProviderType.None)
{
return null;
}
var config = type switch
{
DnsProviderType.Technitium => new DnsProviderConfig
{
Type = DnsProviderType.Technitium,
Name = "Technitium DNS",
BaseUrl = settings.GetValueOrDefault("technitium_url", "http://localhost:5380/api"),
ApiKey = settings.GetValueOrDefault("technitium_api_key", ""),
ZoneId = settings.GetValueOrDefault("technitium_zone", "")
},
DnsProviderType.CoreDNS => new DnsProviderConfig
{
Type = DnsProviderType.CoreDNS,
Name = "CoreDNS",
BaseUrl = settings.GetValueOrDefault("coredns_api_url", "http://localhost:8080"),
ApiKey = "",
ZoneId = settings.GetValueOrDefault("coredns_zone", "local")
},
DnsProviderType.Cloudflare => new DnsProviderConfig
{
Type = DnsProviderType.Cloudflare,
Name = "Cloudflare",
BaseUrl = "https://api.cloudflare.com/client/v4",
ApiKey = settings.GetValueOrDefault("cloudflare_api_token", ""),
ZoneId = settings.GetValueOrDefault("cloudflare_zone_id", "")
},
DnsProviderType.Route53 => new DnsProviderConfig
{
Type = DnsProviderType.Route53,
Name = "AWS Route 53",
BaseUrl = "https://route53.amazonaws.com",
ApiKey = settings.GetValueOrDefault("aws_access_key", ""),
Extra = new Dictionary<string, string>
{
["secret_key"] = settings.GetValueOrDefault("aws_secret_key", ""),
["region"] = settings.GetValueOrDefault("aws_region", "us-east-1")
}
},
_ => new DnsProviderConfig
{
Type = DnsProviderType.Custom,
Name = "Custom DNS",
BaseUrl = settings.GetValueOrDefault("custom_dns_url", ""),
ApiKey = settings.GetValueOrDefault("custom_dns_key", "")
}
};
return new DnsClient(config);
}
public async Task<List<DnsRecord>> GetRecordsAsync(string zone = null)
{
if (_config?.Type == DnsProviderType.None) return new List<DnsRecord>();
zone ??= _config?.ZoneId;
if (string.IsNullOrEmpty(zone)) return new List<DnsRecord>();
return _config.Type switch
{
DnsProviderType.Technitium => await GetTechnitiumRecordsAsync(zone),
DnsProviderType.CoreDNS => await GetCoreDnsRecordsAsync(zone),
DnsProviderType.Cloudflare => await GetCloudflareRecordsAsync(zone),
DnsProviderType.Route53 => await GetRoute53RecordsAsync(zone),
_ => new List<DnsRecord>()
};
}
public async Task<DnsRecord> CreateRecordAsync(string zone, DnsRecord record)
{
if (_config?.Type == DnsProviderType.None) return record;
return _config.Type switch
{
DnsProviderType.Technitium => await CreateTechnitiumRecordAsync(zone, record),
DnsProviderType.CoreDNS => await CreateCoreDnsRecordAsync(zone, record),
DnsProviderType.Cloudflare => await CreateCloudflareRecordAsync(zone, record),
DnsProviderType.Route53 => await CreateRoute53RecordAsync(zone, record),
_ => record
};
}
public async Task<DnsRecord> UpdateRecordAsync(string zone, string recordId, DnsRecord record)
{
if (_config?.Type == DnsProviderType.None) return record;
return _config.Type switch
{
DnsProviderType.Technitium => await UpdateTechnitiumRecordAsync(zone, recordId, record),
DnsProviderType.CoreDNS => await UpdateCoreDnsRecordAsync(zone, recordId, record),
DnsProviderType.Cloudflare => await UpdateCloudflareRecordAsync(zone, recordId, record),
DnsProviderType.Route53 => await UpdateRoute53RecordAsync(zone, recordId, record),
_ => record
};
}
public async Task DeleteRecordAsync(string zone, string recordId)
{
if (_config?.Type == DnsProviderType.None) return;
switch (_config.Type)
{
case DnsProviderType.Technitium:
await DeleteTechnitiumRecordAsync(zone, recordId);
break;
case DnsProviderType.CoreDNS:
await DeleteCoreDnsRecordAsync(zone, recordId);
break;
case DnsProviderType.Cloudflare:
await DeleteCloudflareRecordAsync(zone, recordId);
break;
case DnsProviderType.Route53:
await DeleteRoute53RecordAsync(zone, recordId);
break;
}
}
// ─── Technitium ───
private async Task<List<DnsRecord>> GetTechnitiumRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/zones/{zone}/records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TechnitiumZoneResponse>();
return result?.Records?.Select(r => new DnsRecord
{
Id = r.Id,
Name = r.Name,
Type = r.Type,
Content = r.Value,
Ttl = r.Ttl
}).ToList() ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateTechnitiumRecordAsync(string zone, DnsRecord record)
{
var req = new TechnitiumRecordRequest
{
Name = record.Name,
Type = record.Type,
Value = record.Content,
Ttl = record.Ttl
};
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/records", req);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TechnitiumRecordResponse>();
record.Id = result?.Record?.Id;
return record;
}
private async Task<DnsRecord> UpdateTechnitiumRecordAsync(string zone, string recordId, DnsRecord record)
{
var req = new TechnitiumRecordRequest
{
Name = record.Name,
Type = record.Type,
Value = record.Content,
Ttl = record.Ttl
};
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/records/{recordId}", req);
response.EnsureSuccessStatusCode();
return record;
}
private async Task DeleteTechnitiumRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/zones/{zone}/records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── CoreDNS (via API wrapper) ───
private async Task<List<DnsRecord>> GetCoreDnsRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/api/v1/dns/zones/{zone}/records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<DnsRecord>>>();
return result?.Data ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateCoreDnsRecordAsync(string zone, DnsRecord record)
{
var response = await _httpClient.PostAsJsonAsync($"/api/v1/dns/zones/{zone}/records", record);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
return result?.Data ?? record;
}
private async Task<DnsRecord> UpdateCoreDnsRecordAsync(string zone, string recordId, DnsRecord record)
{
var response = await _httpClient.PutAsJsonAsync($"/api/v1/dns/zones/{zone}/records/{recordId}", record);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
return result?.Data ?? record;
}
private async Task DeleteCoreDnsRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/api/v1/dns/zones/{zone}/records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── Cloudflare ───
private async Task<List<DnsRecord>> GetCloudflareRecordsAsync(string zone)
{
var response = await _httpClient.GetAsync($"/zones/{zone}/dns_records");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CloudflareResponse>();
return result?.Result?.Select(r => new DnsRecord
{
Id = r.Id,
Name = r.Name,
Type = r.Type,
Content = r.Content,
Ttl = r.Ttl ?? 300,
Proxied = r.Proxied ?? false
}).ToList() ?? new List<DnsRecord>();
}
private async Task<DnsRecord> CreateCloudflareRecordAsync(string zone, DnsRecord record)
{
var req = new CloudflareRecordRequest
{
Type = record.Type,
Name = record.Name,
Content = record.Content,
Ttl = record.Ttl,
Proxied = record.Proxied
};
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/dns_records", req);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CloudflareSingleResponse>();
record.Id = result?.Result?.Id;
return record;
}
private async Task<DnsRecord> UpdateCloudflareRecordAsync(string zone, string recordId, DnsRecord record)
{
var req = new CloudflareRecordRequest
{
Type = record.Type,
Name = record.Name,
Content = record.Content,
Ttl = record.Ttl,
Proxied = record.Proxied
};
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/dns_records/{recordId}", req);
response.EnsureSuccessStatusCode();
return record;
}
private async Task DeleteCloudflareRecordAsync(string zone, string recordId)
{
var response = await _httpClient.DeleteAsync($"/zones/{zone}/dns_records/{recordId}");
response.EnsureSuccessStatusCode();
}
// ─── Route 53 ───
private async Task<List<DnsRecord>> GetRoute53RecordsAsync(string zone)
{
return new List<DnsRecord>();
}
private async Task<DnsRecord> CreateRoute53RecordAsync(string zone, DnsRecord record) => record;
private async Task<DnsRecord> UpdateRoute53RecordAsync(string zone, string recordId, DnsRecord record) => record;
private async Task DeleteRoute53RecordAsync(string zone, string recordId) { }
// ─── Response DTOs ───
private class ApiResponse<T>
{
public bool Success { get; set; }
public T Data { get; set; }
public string Error { get; set; }
}
private class TechnitiumZoneResponse
{
public List<TechnitiumRecord> Records { get; set; }
}
private class TechnitiumRecord
{
public string Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public int Ttl { get; set; }
}
private class TechnitiumRecordRequest
{
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public int Ttl { get; set; }
}
private class TechnitiumRecordResponse
{
public TechnitiumRecord Record { get; set; }
}
private class CloudflareResponse
{
public bool Success { get; set; }
public List<CloudflareRecord> Result { get; set; }
}
private class CloudflareRecord
{
public string Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Content { get; set; }
public int? Ttl { get; set; }
public bool? Proxied { get; set; }
}
private class CloudflareRecordRequest
{
public string Type { get; set; }
public string Name { get; set; }
public string Content { get; set; }
public int Ttl { get; set; }
public bool Proxied { get; set; }
}
private class CloudflareSingleResponse
{
public bool Success { get; set; }
public CloudflareRecord Result { get; set; }
}
}