feat: 支持 SSL 证书自动申请并重构 DDNS 任务逻辑

- 实现阿里/腾讯云 SSL 证书的全生命周期自动化管理。
- 重构 NewJob 为 DdnsJob,优化子域名匹配与记录自动创建逻辑。
- 更新项目配置结构,移除冗余的 AppJob 相关代码。
This commit is contained in:
ShaoHua
2026-04-08 21:45:36 +08:00
parent 10f156e9e2
commit 773c230e3d
15 changed files with 326 additions and 227 deletions
+3 -3
View File
@@ -18,17 +18,17 @@ namespace Hua.DDNS.Test
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(configPath, true)
.AddEnvironmentVariables()// 把环境变量也放到 Configuraiton当中
.AddEnvironmentVariables()// ѻҲŵ Configuraiton
.Build();
var sc = DIConfig.ConfigureServices(config);
var job = sc.GetService<NewJob>();
var job = sc.GetService<DdnsJob>();
job?.Execute(null);
}
catch (Exception e)
{
Assert.False(false, $"请求异常:{e.Message}");
Assert.False(false, $":{e.Message}");
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace Hua.DDNS.Test.Start
services.AddSingleton<Url>();
services.AddSingleton<SqlHelper>();
services.AddTransient<IHttpHelper, HttpHelper>();
services.AddTransient<NewJob>();
services.AddTransient<DdnsJob>();
return services.BuildServiceProvider();
}
}
@@ -81,6 +81,7 @@ namespace Hua.DDNS.DDNSProviders.Ali
{
foreach (var aliDomainRecord in records)
{
aliDomainRecord.Ip = newIp;
await _client.UpdateDomainRecordAsync(_mapper.Map<UpdateDomainRecordRequest>(aliDomainRecord));
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -70,6 +70,7 @@ namespace Hua.DDNS.DDNSProviders.Dnspod
var response = await _client.CreateRecord(new CreateRecordRequest()
{
Domain = _ddnsOption.Domain,
SubDomain = record.SubDomain,
Value = record.Ip,
RecordLine = "默认",
RecordType = record.RecordType,
@@ -58,13 +58,14 @@ namespace Hua.DDNS.DDNSProviders.Namesilo
}
return (from record in records.Cast<XmlNode>()
let subDomain = record.ParentNode.SelectSingleNode("host/text()").Value.Replace(_ddnsOption.Domain, "")
where _ddnsOption.SubDomainArray.Contains(subDomain)
let host = record.ParentNode.SelectSingleNode("host/text()").Value
let subDomain = host.Replace($".{_ddnsOption.Domain}", "").Replace(_ddnsOption.Domain, "")
where _ddnsOption.SubDomainArray.Contains(subDomain) || (subDomain == "" && _ddnsOption.SubDomainArray.Contains("@"))
select new DnsRecord
{
Id = record.ParentNode.SelectSingleNode("record_id/text()").Value,
Ip = record.ParentNode.SelectSingleNode("value/text()").Value,
Host = record.ParentNode.SelectSingleNode("host/text()").Value,
Host = host,
Domain = _ddnsOption.Domain,
TTL = record.ParentNode.SelectSingleNode("ttl/text()").Value,
SubDomain = subDomain,
@@ -78,9 +79,9 @@ namespace Hua.DDNS.DDNSProviders.Namesilo
/// <returns>创建后的解析记录信息</returns>
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord dnsRecord)
{
var host = dnsRecord.Host[..(dnsRecord.Host.Length - dnsRecord.Domain.Length - 1)];
var host = dnsRecord.SubDomain == "@" ? "" : dnsRecord.SubDomain;
//https://www.namesilo.com/api/dnsAddRecord?version=1&type=xml&key=12345&domain=namesilo.com&rrtype=A&rrhost=test&rrvalue=55.55.55.55&rrttl=7207
var url = $"https://www.namesilo.com/api/dnsUpdateRecord?version=1&type=xml&key={_namesiloOption.ApiKey}&domain={dnsRecord.Domain}&rrtype={dnsRecord.RecordType}&rrid={dnsRecord.Id}&rrhost={host}&rrvalue={dnsRecord.Ip}&rrttl={dnsRecord.TTL}";
var url = $"https://www.namesilo.com/api/dnsAddRecord?version=1&type=xml&key={_namesiloOption.ApiKey}&domain={dnsRecord.Domain}&rrtype={dnsRecord.RecordType}&rrhost={host}&rrvalue={dnsRecord.Ip}&rrttl={dnsRecord.TTL}";
using var client = new HttpClient();
{
@@ -94,12 +95,13 @@ namespace Hua.DDNS.DDNSProviders.Namesilo
if (status == null)
{
await Console.Error.WriteLineAsync($"Failed to create record: '{JsonConvert.SerializeObject(dnsRecord)}'");
return null;
}
if (status.Value == "300")
if (status.Value != "300")
{
await Console.Error.WriteLineAsync($"Failed to create record: '{JsonConvert.SerializeObject(dnsRecord)}', Status: {status.Value}");
return null;
//continue;
}
}
return dnsRecord;
@@ -117,7 +119,7 @@ namespace Hua.DDNS.DDNSProviders.Namesilo
{
using var client = new HttpClient();
{
var host = dnsRecord.Host[..(dnsRecord.Host.Length - dnsRecord.Domain.Length - 1)];
var host = dnsRecord.SubDomain == "@" ? "" : dnsRecord.SubDomain;
var request =
$"https://www.namesilo.com/api/dnsUpdateRecord?version=1&type=xml&key={_namesiloOption.ApiKey}&domain={dnsRecord.Domain}&rrid={dnsRecord.Id}&rrhost={host}&rrvalue={newIp}&rrttl={dnsRecord.TTL}";
//Console.WriteLine(request);
@@ -127,17 +129,12 @@ namespace Hua.DDNS.DDNSProviders.Namesilo
var reply = new XmlDocument();
reply.LoadXml(content);
var status = reply.SelectSingleNode("/namesilo/reply/code/text()");
if (status == null)
if (status == null || status.Value != "300")
{
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'.");
continue; //return false;
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'. Status: {status?.Value}");
continue;
}
if (status.Value == "300") continue;
}
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'.");
continue; //return false;
}
return records;
-138
View File
@@ -1,138 +0,0 @@
//using Hua.DDNS.Common;
//using Hua.DDNS.Common.Config;
//using Hua.DDNS.Common.Config.Options;
//using Hua.DDNS.Common.Http;
//using Hua.DDNS.Start;
//using Quartz;
//using System.Net;
//using AlibabaCloud.OpenApiClient.Models;
//using AlibabaCloud.SDK.Alidns20150109.Models;
//using Hua.DotNet.Code.Extension;
//using TencentCloud.Common;
//using TencentCloud.Common.Profile;
//using TencentCloud.Dnspod.V20210323;
//using TencentCloud.Dnspod.V20210323.Models;
//using Tea;
//using Tea.Utils;
//using Hua.DDNS.DDNSProviders;
//namespace Hua.DDNS.Jobs
//{
// [DisallowConcurrentExecution]
// public class AppJob : IJob, IDisposable
// {
// private readonly ILogger<AppJob> _logger;
// private readonly SettingProvider _settingProvider;
// private readonly DdnsOption _ddnsOption;
// private readonly IHttpHelper _httpHelper;
// public string CurrentIpv4Address;
// public AppJob(ILogger<AppJob> logger,SettingProvider settingProvider, IHttpHelper httpHelper)
// {
// _logger = logger;
// _settingProvider = settingProvider;
// _httpHelper = httpHelper;
// _ddnsOption = _settingProvider.App.DDNS;
// }
// public async Task Execute(IJobExecutionContext context)
// {
// _logger.LogInformation("开始任务执行");
// try
// {
// var oldIp = (await Dns.GetHostEntryAsync($"{_ddnsOption.SubDomainArray.First()}.{_ddnsOption.Domain}")).AddressList.First();
// CurrentIpv4Address = await _httpHelper.GetCurrentPublicIpv4();
// if (CurrentIpv4Address!=oldIp.ToString())
// {
// await UpdateDns();
// }
// }
// catch (Exception e)
// {
// _logger.LogError(e,e.Message);
// }
// finally
// {
// _logger.LogInformation("任务执行完成");
// }
// }
// private async Task UpdateDns()
// {
// //更新Ip记录
// switch (_ddnsOption.Platform)
// {
// case PlatformEnum.Namesilo:
// case PlatformEnum.Tencent:
// var _dnspodClient = new DnspodClient(
// // 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
// // 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
// new Credential { SecretId = _ddnsOption.Id, SecretKey = _ddnsOption.Key },
// "",
// // 实例化一个client选项,可选的,没有特殊需求可以跳过
// new ClientProfile()
// {
// // 实例化一个http选项,可选的,没有特殊需求可以跳过
// HttpProfile = new HttpProfile { Endpoint = ("dnspod.tencentcloudapi.com") }
// });
// //获取域名解析记录
// var describeRecordList = await _dnspodClient.DescribeRecordList(new DescribeRecordListRequest() { Domain = _ddnsOption.Domain });
// var record = describeRecordList.RecordList.FirstOrDefault(m =>
// m.Value == CurrentIpv4Address && _ddnsOption.SubDomainArray.Any(n => m.Name == n));
// if (record!=null && record.Value == CurrentIpv4Address) return;//如果记录已经变更,不调用更新接口
// await _dnspodClient.ModifyRecordBatch(new ModifyRecordBatchRequest()
// {
// RecordIdList =
// describeRecordList.RecordList
// .Where(m => m.Value != CurrentIpv4Address && _ddnsOption.SubDomainArray.Any(n => m.Name == n))
// .Select(m => m.RecordId)
// .ToArray(),
// Change = "value",
// ChangeTo = CurrentIpv4Address
// });
// break;
// case PlatformEnum.Ali:
// var aliClient = new AlibabaCloud.SDK.Alidns20150109.Client(new Config()
// {
// // 您的 AccessKey ID
// AccessKeyId = _ddnsOption.Id,
// // 您的 AccessKey Secret
// AccessKeySecret = _ddnsOption.Key,
// Endpoint = "alidns.cn-beijing.aliyuncs.com",
// });
// var aliDescribeRecordList = (await aliClient.DescribeDomainRecordsAsync(new DescribeDomainRecordsRequest()
// {
// DomainName = _ddnsOption.Domain
// })).Body.DomainRecords.Record;
// foreach (var aliDomainRecord in aliDescribeRecordList
// .Where(m => m.Value != CurrentIpv4Address && _ddnsOption.SubDomainArray.Any(n => m.RR == n)))
// {
// await aliClient.UpdateDomainRecordAsync(new UpdateDomainRecordRequest()
// {
// RecordId = aliDomainRecord.RecordId,
// RR = aliDomainRecord.RR,
// Type = aliDomainRecord.Type,
// Value = CurrentIpv4Address,
// });
// _logger.LogInformation($"Update SubDomain[{aliDomainRecord.RR}.{aliDomainRecord.DomainName}] Value {aliDomainRecord.Value} To {CurrentIpv4Address}");
// }
// break;
// }
// }
// public void Dispose()
// {
// _logger.LogInformation("AppJob已销毁");
// }
// }
//}
-10
View File
@@ -1,10 +0,0 @@
namespace Hua.DDNS.Jobs
{
/// <summary>
/// Job上下文
/// </summary>
public class AppJobContext
{
}
}
@@ -26,12 +26,12 @@ using System.Net.Sockets;
namespace Hua.DDNS.Jobs
{
/// <summary>
/// 新的 DDNS 任务类,用于定期检查并更新域名解析记录
/// DDNS 任务类,用于定期检查并更新域名解析记录
/// </summary>
[DisallowConcurrentExecution]
public class NewJob : IJob, IDisposable
public class DdnsJob : IJob, IDisposable
{
private readonly ILogger<NewJob> _logger;
private readonly ILogger<DdnsJob> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly DdnsOption _ddnsOption;
private readonly IHttpHelper _httpHelper;
@@ -48,7 +48,7 @@ namespace Hua.DDNS.Jobs
/// <param name="httpHelper">Http 助手</param>
/// <param name="ddnsOption">DDNS 配置选项</param>
/// <param name="serviceProvider">服务提供者</param>
public NewJob(ILogger<NewJob> logger,IHttpHelper httpHelper,IOptions<DdnsOption> ddnsOption, IServiceProvider serviceProvider)
public DdnsJob(ILogger<DdnsJob> logger,IHttpHelper httpHelper,IOptions<DdnsOption> ddnsOption, IServiceProvider serviceProvider)
{
_logger = logger;
_httpHelper = httpHelper;
@@ -73,25 +73,47 @@ namespace Hua.DDNS.Jobs
};
newIp = await _httpHelper.GetCurrentPublicIpv4();
if (_ddnsOption.SubDomainArray == null || _ddnsOption.SubDomainArray.Length == 0)
{
_logger.LogWarning("未配置 SubDomainArray,跳过执行。");
return;
}
try
{
//1. 获取当前机器ip
var domain = $"{_ddnsOption.SubDomainArray.First()}.{_ddnsOption.Domain}";
IPAddress oldIp = null;
oldIp = (await Dns.GetHostEntryAsync(domain)).AddressList.First();
// 1. 获取所有 DNS 记录
var dnsRecordList = (await ddnsProvider!.GetRecordListAsync())?.ToList() ?? new List<DnsRecord>();
//1.1 如果当前dns记录与实际dns记录一致,跳出本次执行
if (newIp == oldIp.ToString()) return;
// 2. 找出需要更新的记录(在配置中且 IP 不一致)
var recordsToUpdate = dnsRecordList
.Where(m => _ddnsOption.SubDomainArray.Contains(m.SubDomain) && m.Ip != newIp)
.ToList();
if (recordsToUpdate.Any())
{
_logger.LogInformation("发现 {Count} 个子域名需要更新 IP 为 {NewIp}", recordsToUpdate.Count, newIp);
await ddnsProvider.ModifyRecordListAsync(newIp, recordsToUpdate);
}
var dnsRecordList = await ddnsProvider!.GetRecordListAsync();
var record = dnsRecordList.FirstOrDefault(m =>
m.Ip == newIp && _ddnsOption.SubDomainArray.Any(n => m.SubDomain == n));
if (record != null && record.Ip == newIp) return; //如果记录已经变更,不调用更新接口
// 3. 找出需要新增的记录(在配置中但不在 DNS 记录中)
var existingSubDomains = dnsRecordList.Select(m => m.SubDomain).ToHashSet();
var subDomainsToCreate = _ddnsOption.SubDomainArray
.Where(s => !existingSubDomains.Contains(s))
.ToList();
//3.比较并更新
await ddnsProvider.ModifyRecordListAsync(newIp,
dnsRecordList.Where(m => m.Ip != newIp && _ddnsOption.SubDomainArray.Any(n => m.SubDomain == n)));
foreach (var subDomain in subDomainsToCreate)
{
_logger.LogInformation("正在为子域名 {SubDomain} 创建新的解析记录,IP: {NewIp}", subDomain, newIp);
await ddnsProvider.CreateDnsRecordAsync(new DnsRecord
{
Ip = newIp,
SubDomain = subDomain,
Domain = _ddnsOption.Domain,
Host = string.IsNullOrEmpty(subDomain) || subDomain == "@" ? _ddnsOption.Domain : $"{subDomain}.{_ddnsOption.Domain}",
RecordType = "A", // 默认 A 记录
TTL = "3600" // 默认 TTL
});
}
}
catch (SocketException e)
{
@@ -120,7 +142,7 @@ namespace Hua.DDNS.Jobs
/// </summary>
public void Dispose()
{
_logger.LogInformation("AppJob已销毁");
_logger.LogInformation("DdnsJob已销毁");
}
}
}
+87 -16
View File
@@ -5,11 +5,12 @@ using Hua.DDNS.SslProviders.Tencent;
using Quartz;
using Microsoft.Extensions.Options;
using System.Security.Cryptography.X509Certificates;
using Hua.DDNS.DDNSProviders;
namespace Hua.DDNS.Jobs
{
/// <summary>
/// SSL 证书下载任务类,用于定期检查并更新 SSL 证书
/// SSL 证书管理任务类,用于定期检查、申请并更新 SSL 证书
/// </summary>
[DisallowConcurrentExecution]
public class SslDownloadJob : IJob, IDisposable
@@ -17,6 +18,7 @@ namespace Hua.DDNS.Jobs
private readonly ILogger<SslDownloadJob> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly SslDownloadOption _sslDownloadOption;
private readonly DdnsOption _ddnsOption;
/// <summary>
/// 构造函数
@@ -24,14 +26,17 @@ namespace Hua.DDNS.Jobs
/// <param name="logger">日志对象</param>
/// <param name="serviceProvider">服务提供者</param>
/// <param name="sslDownloadOption">SSL 下载配置选项</param>
/// <param name="ddnsOption">DDNS 配置选项</param>
public SslDownloadJob(
ILogger<SslDownloadJob> logger,
IServiceProvider serviceProvider,
IOptions<SslDownloadOption> sslDownloadOption)
IOptions<SslDownloadOption> sslDownloadOption,
IOptions<DdnsOption> ddnsOption)
{
_logger = logger;
_serviceProvider = serviceProvider;
_sslDownloadOption = sslDownloadOption.Value;
_ddnsOption = ddnsOption.Value;
}
/// <summary>
@@ -42,27 +47,27 @@ namespace Hua.DDNS.Jobs
{
if (!_sslDownloadOption.Enabled)
{
_logger.LogInformation("SSL下载任务已禁用,跳过执行");
_logger.LogInformation("SSL管理任务已禁用,跳过执行");
return;
}
_logger.LogInformation("开始SSL文件下载任务");
_logger.LogInformation("开始SSL文件管理任务");
try
{
ISslDownloadProvider? sslProvider = _sslDownloadOption.Platform switch
ISslManagementProvider? sslProvider = _sslDownloadOption.Platform switch
{
SslPlatformEnum.Ali => _serviceProvider.GetService(typeof(AliSslProvider)) as ISslDownloadProvider,
SslPlatformEnum.Tencent => _serviceProvider.GetService(typeof(TencentSslProvider)) as ISslDownloadProvider,
SslPlatformEnum.Ali => _serviceProvider.GetService(typeof(AliSslProvider)) as ISslManagementProvider,
SslPlatformEnum.Tencent => _serviceProvider.GetService(typeof(TencentSslProvider)) as ISslManagementProvider,
_ => null
};
if (sslProvider == null)
{
_logger.LogError($"未找到 SSL 下载提供者: {_sslDownloadOption.Platform}");
_logger.LogError($"未找到 SSL 管理提供者: {_sslDownloadOption.Platform}");
return;
}
;
if (!Directory.Exists(_sslDownloadOption.SavePath))
{
Directory.CreateDirectory(_sslDownloadOption.SavePath);
@@ -70,22 +75,72 @@ namespace Hua.DDNS.Jobs
}
var certificates = await sslProvider.GetCertificatesAsync();
_logger.LogInformation($"获取到 {certificates.Count} 个{_sslDownloadOption.Platform} SSL 证书");
var downloadTasks = new List<Task>();
foreach (var item in _sslDownloadOption.DownloadItems)
// 合并 DownloadItems 和 DdnsOption.SubDomainArray
var itemsToProcess = _sslDownloadOption.DownloadItems?.ToList() ?? new List<SslDownloadItem>();
if (_ddnsOption.SubDomainArray != null && !string.IsNullOrEmpty(_ddnsOption.Domain))
{
foreach (var sub in _ddnsOption.SubDomainArray)
{
var fullDomain = (string.IsNullOrEmpty(sub) || sub == "@")
? _ddnsOption.Domain
: $"{sub}.{_ddnsOption.Domain}";
if (!itemsToProcess.Any(i => i.Domain == fullDomain))
{
itemsToProcess.Add(new SslDownloadItem
{
Domain = fullDomain,
FileName = $"{fullDomain}.pem"
});
}
}
}
foreach (var item in itemsToProcess)
{
var matchingCertificates = certificates.Where(c => c.Domain == item.Domain).ToList();
if (matchingCertificates.Count == 0)
{
_logger.LogWarning($"未找到域名 {item.Domain} 的证书,跳过下载");
_logger.LogWarning($"未找到域名 {item.Domain} 的证书,尝试自动申请");
var newCertId = await sslProvider.ApplyCertificateAsync(item.Domain);
if (!string.IsNullOrEmpty(newCertId))
{
_logger.LogInformation($"域名 {item.Domain} 证书申请已提交,ID: {newCertId},重新获取证书列表");
// 重新获取列表以获取新申请的证书信息
certificates = await sslProvider.GetCertificatesAsync();
matchingCertificates = certificates.Where(c => c.Domain == item.Domain).ToList();
}
}
// 检查是否有正在验证中的证书
var isPending = _sslDownloadOption.Platform switch
{
SslPlatformEnum.Ali => matchingCertificates.Any(c => c.StatusMsg == "CHECK" || c.StatusMsg == "PAYED"),
SslPlatformEnum.Tencent => matchingCertificates.Any(c => c.Status == 0 || c.Status == 4 || c.Status == 5 || c.Status == 8),
_ => false
};
if (isPending && !matchingCertificates.Any(c => IsDownloadable(c, _sslDownloadOption.Platform)))
{
_logger.LogInformation($"域名 {item.Domain} 的证书正在申请/验证中,跳过下载");
continue;
}
var certificate = matchingCertificates.OrderByDescending(c => c.CertEndTime).First();
var downloadableCertificates = matchingCertificates.Where(c => IsDownloadable(c, _sslDownloadOption.Platform)).ToList();
if (downloadableCertificates.Count == 0)
{
_logger.LogWarning($"域名 {item.Domain} 没有可下载的已签发证书,跳过下载");
continue;
}
var certificate = downloadableCertificates.OrderByDescending(c => c.CertEndTime).First();
var localCertExpiry = GetLocalCertificateExpiry(item);
var daysUntilExpiry = (certificate.CertEndTime - DateTime.Now).Days;
if (localCertExpiry == null)
{
@@ -115,14 +170,30 @@ namespace Hua.DDNS.Jobs
await Task.WhenAll(downloadTasks);
_logger.LogInformation($"SSL文件下载任务完成,共下载 {downloadTasks.Count} 个文件");
_logger.LogInformation($"SSL文件管理任务完成,共下载 {downloadTasks.Count} 个文件");
}
catch (Exception ex)
{
_logger.LogError(ex, "SSL文件下载任务执行失败");
_logger.LogError(ex, "SSL文件管理任务执行失败");
}
}
/// <summary>
/// 判断证书是否可以下载(已签发)
/// </summary>
/// <param name="certificate">证书信息</param>
/// <param name="platform">所属平台</param>
/// <returns>是否可下载</returns>
private bool IsDownloadable(SslCertificate certificate, SslPlatformEnum platform)
{
return platform switch
{
SslPlatformEnum.Ali => certificate.StatusMsg == "ISSUED",
SslPlatformEnum.Tencent => certificate.Status == 1,
_ => false
};
}
/// <summary>
/// 获取本地证书的过期时间
/// </summary>
@@ -163,7 +234,7 @@ namespace Hua.DDNS.Jobs
/// <param name="certificateId">证书 ID</param>
/// <param name="domain">域名</param>
/// <returns>Task</returns>
private async Task DownloadFileAsync(ISslDownloadProvider provider, SslDownloadItem item, string certificateId, string domain)
private async Task DownloadFileAsync(ISslManagementProvider provider, SslDownloadItem item, string certificateId, string domain)
{
try
{
@@ -0,0 +1,12 @@
namespace Hua.DDNS.SslProviders.Ali
{
public class AliSslApplyOption
{
public string? ProductCode { get; set; }
public string? Username { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? ValidateType { get; set; }
public Dictionary<string, string>? Tags { get; set; }
}
}
+106 -9
View File
@@ -5,16 +5,18 @@ using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.SslProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text;
namespace Hua.DDNS.SslProviders.Ali
{
/// <summary>
/// 阿里云 SSL 证书下载提供者
/// 阿里云 SSL 证书管理提供者
/// </summary>
public class AliSslProvider : ISslDownloadProvider
public class AliSslProvider : ISslManagementProvider
{
private readonly Client _client;
private readonly AliCloudOption _aliCloudOption;
private readonly AliSslApplyOption _aliSslApplyOption;
private readonly ILogger<AliSslProvider> _logger;
/// <summary>
@@ -24,10 +26,12 @@ namespace Hua.DDNS.SslProviders.Ali
/// <param name="aliCloudOption">阿里云配置</param>
public AliSslProvider(
ILogger<AliSslProvider> logger,
IOptions<AliCloudOption> aliCloudOption)
IOptions<AliCloudOption> aliCloudOption,
IOptions<AliSslApplyOption> aliSslApplyOption)
{
_logger = logger;
_aliCloudOption = aliCloudOption.Value;
_aliSslApplyOption = aliSslApplyOption.Value;
var config = new Config
{
@@ -65,7 +69,7 @@ namespace Hua.DDNS.SslProviders.Ali
CertificateId = cert.CertificateId.ToString(),
Domain = cert.Domain,
Alias = cert.Name,
CertEndTime = string.IsNullOrEmpty(cert.EndDate) ? DateTime.MinValue : DateTime.Parse(cert.EndDate),
CertEndTime = string.IsNullOrEmpty(cert.EndDate) ? DateTime.MaxValue : DateTime.Parse(cert.EndDate),
StatusMsg = cert.Status
});
}
@@ -92,9 +96,44 @@ namespace Hua.DDNS.SslProviders.Ali
{
try
{
// TODO: 阿里云证书下载逻辑
_logger.LogWarning($"阿里云 SSL 证书下载功能待实现: {certificateId}");
return false;
if (!long.TryParse(certificateId, out var certId))
{
throw new ArgumentException("Invalid certificate ID format", nameof(certificateId));
}
var request = new GetUserCertificateDetailRequest
{
CertId = certId
};
var response = await _client.GetUserCertificateDetailAsync(request);
if (response?.Body == null)
{
_logger.LogError($"阿里云证书详情返回为空: {certificateId}");
return false;
}
if (string.IsNullOrWhiteSpace(response.Body.Cert) || string.IsNullOrWhiteSpace(response.Body.Key))
{
_logger.LogError($"阿里云证书内容为空: {certificateId}");
return false;
}
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
var certFileName = Path.GetFileNameWithoutExtension(fileName);
var certSavePath = Path.Combine(savePath, $"{certFileName}_bundle.crt");
var keySavePath = Path.Combine(savePath, $"{certFileName}.key");
await File.WriteAllTextAsync(certSavePath, response.Body.Cert, new UTF8Encoding(false));
await File.WriteAllTextAsync(keySavePath, response.Body.Key, new UTF8Encoding(false));
_logger.LogInformation($"阿里云证书保存成功: {certSavePath}");
_logger.LogInformation($"阿里云私钥保存成功: {keySavePath}");
return true;
}
catch (Exception ex)
{
@@ -113,15 +152,24 @@ namespace Hua.DDNS.SslProviders.Ali
{
var certificates = await GetCertificatesAsync();
// 筛选过期的证书
var expiredCertificates = certificates.Where(c => c.CertEndTime != DateTime.MinValue && c.CertEndTime < DateTime.Now).ToList();
var expiredCertificates = certificates.Where(c =>
c.CertEndTime != DateTime.MaxValue &&
c.CertEndTime < DateTime.Now &&
c.StatusMsg != "CHECK").ToList();
foreach (var cert in expiredCertificates)
{
try
{
if (!long.TryParse(cert.CertificateId, out var certId))
{
_logger.LogWarning($"阿里云证书 ID 格式不正确: {cert.CertificateId}");
continue;
}
var deleteRequest = new DeleteUserCertificateRequest
{
CertId = long.Parse(cert.CertificateId)
CertId = certId
};
await _client.DeleteUserCertificateAsync(deleteRequest);
_logger.LogInformation($"已删除阿里云过期证书: {cert.Domain} ({cert.CertificateId}), 过期时间: {cert.CertEndTime}");
@@ -138,5 +186,54 @@ namespace Hua.DDNS.SslProviders.Ali
throw;
}
}
/// <summary>
/// 异步申请 SSL 证书
/// </summary>
/// <param name="domain">域名</param>
/// <returns>申请结果,成功返回证书 ID,失败返回 null</returns>
public async Task<string?> ApplyCertificateAsync(string domain)
{
try
{
_logger.LogInformation($"开始为域名 {domain} 申请阿里云免费 SSL 证书");
if (string.IsNullOrWhiteSpace(_aliSslApplyOption.ProductCode) ||
string.IsNullOrWhiteSpace(_aliSslApplyOption.Username) ||
string.IsNullOrWhiteSpace(_aliSslApplyOption.Phone) ||
string.IsNullOrWhiteSpace(_aliSslApplyOption.Email) ||
string.IsNullOrWhiteSpace(_aliSslApplyOption.ValidateType))
{
_logger.LogError($"阿里云证书申请配置不完整,无法申请证书: {domain}");
return null;
}
var request = new CreateCertificateRequestRequest
{
Domain = domain,
ProductCode = _aliSslApplyOption.ProductCode,
Username = _aliSslApplyOption.Username,
Phone = _aliSslApplyOption.Phone,
Email = _aliSslApplyOption.Email,
ValidateType = _aliSslApplyOption.ValidateType
};
var response = await _client.CreateCertificateRequestAsync(request);
var orderId = response?.Body?.OrderId;
if (orderId.HasValue)
{
_logger.LogInformation($"阿里云 SSL 证书申请提交成功: {domain}, OrderId: {orderId.Value}");
return orderId.Value.ToString();
}
_logger.LogError($"阿里云 SSL 证书申请失败: {domain}");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"申请阿里云 SSL 证书时发生错误: {domain}");
return null;
}
}
}
}
@@ -1,9 +1,9 @@
namespace Hua.DDNS.SslProviders
{
/// <summary>
/// SSL 证书下载提供者接口
/// SSL 证书管理提供者接口
/// </summary>
public interface ISslDownloadProvider
public interface ISslManagementProvider
{
/// <summary>
/// 异步获取 SSL 证书列表
@@ -20,6 +20,13 @@ namespace Hua.DDNS.SslProviders
/// <returns>下载成功返回 true,否则返回 false</returns>
Task<bool> DownloadCertificateAsync(string certificateId, string savePath, string fileName);
/// <summary>
/// 异步申请 SSL 证书
/// </summary>
/// <param name="domain">域名</param>
/// <returns>申请结果,成功返回证书 ID,失败返回 null</returns>
Task<string?> ApplyCertificateAsync(string domain);
/// <summary>
/// 异步清理无效证书
/// </summary>
@@ -11,9 +11,9 @@ using System.IO.Compression;
namespace Hua.DDNS.SslProviders.Tencent
{
/// <summary>
/// 腾讯云 SSL 证书下载提供者
/// 腾讯云 SSL 证书管理提供者
/// </summary>
public class TencentSslProvider : ISslDownloadProvider
public class TencentSslProvider : ISslManagementProvider
{
private readonly SslClient _client;
private readonly TencentCloudOption _tencentCloudOption;
@@ -66,8 +66,8 @@ namespace Hua.DDNS.SslProviders.Tencent
CertificateId = cert.CertificateId,
Domain = cert.Domain,
Alias = cert.Alias,
CertBeginTime = DateTime.Parse(cert.CertBeginTime),
CertEndTime = DateTime.Parse(cert.CertEndTime),
CertBeginTime = string.IsNullOrEmpty(cert.CertBeginTime) ? DateTime.MinValue : DateTime.Parse(cert.CertBeginTime),
CertEndTime = string.IsNullOrEmpty(cert.CertEndTime) ? DateTime.MaxValue : DateTime.Parse(cert.CertEndTime),
Status = (int)(cert.Status ?? 0),
StatusMsg = cert.StatusMsg
});
@@ -202,7 +202,10 @@ namespace Hua.DDNS.SslProviders.Tencent
{
var certificates = await GetCertificatesAsync();
// 清理已过期的证书 (Status 为 10 或者当前时间已过过期时间)
var expiredCertificates = certificates.Where(c => c.CertEndTime < DateTime.Now || c.Status == 10).ToList();
// 排除过期时间为 MaxValue 的情况(即未签发的证书)
var expiredCertificates = certificates.Where(c =>
(c.CertEndTime != DateTime.MaxValue && c.CertEndTime < DateTime.Now) ||
c.Status == 10).ToList();
foreach (var cert in expiredCertificates)
{
@@ -227,5 +230,40 @@ namespace Hua.DDNS.SslProviders.Tencent
throw;
}
}
/// <summary>
/// 异步申请 SSL 证书
/// </summary>
/// <param name="domain">域名</param>
/// <returns>申请结果,成功返回证书 ID,失败返回 null</returns>
public async Task<string?> ApplyCertificateAsync(string domain)
{
try
{
_logger.LogInformation($"开始为域名 {domain} 申请腾讯云免费 SSL 证书");
var request = new ApplyCertificateRequest
{
DomainName = domain,
DvAuthMethod = "DNS" // 默认使用 DNS 验证
};
var response = await _client.ApplyCertificate(request);
if (response != null && !string.IsNullOrEmpty(response.CertificateId))
{
_logger.LogInformation($"腾讯云 SSL 证书申请提交成功: {domain}, 证书 ID: {response.CertificateId}");
return response.CertificateId;
}
_logger.LogError($"腾讯云 SSL 证书申请失败: {domain}");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"申请腾讯云 SSL 证书时发生错误: {domain}");
return null;
}
}
}
}
+8 -7
View File
@@ -69,6 +69,7 @@ namespace Hua.DDNS.Start
services.Configure<TencentCloudOption>(hostContext.Configuration.GetSection("TencentCloud"));
services.Configure<AliCloudOption>(hostContext.Configuration.GetSection("AliCloud"));
services.Configure<SslDownloadOption>(hostContext.Configuration.GetSection("SslDownload"));
services.Configure<Hua.DDNS.SslProviders.Ali.AliSslApplyOption>(hostContext.Configuration.GetSection("SslDownload:AliApply"));
// 配置依赖注入和 Quartz
ConfigDi(hostContext, services);
@@ -119,19 +120,19 @@ namespace Hua.DDNS.Start
q.UseInMemoryStore();
q.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 10; });
// 配置 DDNS 任务 (NewJob)
var appJobKey = new JobKey("NewJob", "NewJobGroup");
q.AddJob<NewJob>(j => j
// 配置 DDNS 任务 (DdnsJob)
var appJobKey = new JobKey("DdnsJob", "DdnsJobGroup");
q.AddJob<DdnsJob>(j => j
.StoreDurably()
.WithIdentity(appJobKey)
.WithDescription("NewJob")
.WithDescription("DdnsJob")
);
q.AddTrigger(t => t
.WithIdentity("NewJob Trigger")
.WithIdentity("DdnsJob Trigger")
.ForJob(appJobKey)
.WithCronSchedule(hostContext.Configuration.GetSection("App:AppJob:Corn").Value)
.WithDescription("NewJob trigger")
.WithDescription("DdnsJob trigger")
.StartNow()
);
@@ -166,4 +167,4 @@ namespace Hua.DDNS.Start
});
}
}
}
}
+1 -1
View File
@@ -195,7 +195,7 @@ Hua.DDNS/
│ └── ...
├── Jobs/ # Scheduled jobs
│ ├── AppJob.cs
│ ├── NewJob.cs
│ ├── DdnsJob.cs
│ └── SslDownloadJob.cs
├── Models/ # Data models
├── SslProviders/ # SSL certificate providers