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).
382 lines
14 KiB
C#
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; }
|
|
}
|
|
} |