feat: 支持 SSL 证书自动申请并重构 DDNS 任务逻辑
- 实现阿里/腾讯云 SSL 证书的全生命周期自动化管理。 - 重构 NewJob 为 DdnsJob,优化子域名匹配与记录自动创建逻辑。 - 更新项目配置结构,移除冗余的 AppJob 相关代码。
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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已销毁");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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已销毁");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user