diff --git a/Hua.DDNS.Test/AppJobTest.cs b/Hua.DDNS.Test/AppJobTest.cs index a4996df..5afa2dd 100644 --- a/Hua.DDNS.Test/AppJobTest.cs +++ b/Hua.DDNS.Test/AppJobTest.cs @@ -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(); + var job = sc.GetService(); job?.Execute(null); } catch (Exception e) { - Assert.False(false, $"�����쳣:{e.Message}"); + Assert.False(false, $"�����쳣:{e.Message}"); } } } diff --git a/Hua.DDNS.Test/Start/DIConfig.cs b/Hua.DDNS.Test/Start/DIConfig.cs index 132e4dd..5669dea 100644 --- a/Hua.DDNS.Test/Start/DIConfig.cs +++ b/Hua.DDNS.Test/Start/DIConfig.cs @@ -41,7 +41,7 @@ namespace Hua.DDNS.Test.Start services.AddSingleton(); services.AddSingleton(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); return services.BuildServiceProvider(); } } diff --git a/Hua.DDNS/DDNSProviders/Ali/AliDDNSProvider.cs b/Hua.DDNS/DDNSProviders/Ali/AliDDNSProvider.cs index 14febf7..de2c563 100644 --- a/Hua.DDNS/DDNSProviders/Ali/AliDDNSProvider.cs +++ b/Hua.DDNS/DDNSProviders/Ali/AliDDNSProvider.cs @@ -81,6 +81,7 @@ namespace Hua.DDNS.DDNSProviders.Ali { foreach (var aliDomainRecord in records) { + aliDomainRecord.Ip = newIp; await _client.UpdateDomainRecordAsync(_mapper.Map(aliDomainRecord)); } diff --git a/Hua.DDNS/DDNSProviders/Dnspod/DnspodDDNSProvider.cs b/Hua.DDNS/DDNSProviders/Dnspod/DnspodDDNSProvider.cs index fe39866..3860835 100644 --- a/Hua.DDNS/DDNSProviders/Dnspod/DnspodDDNSProvider.cs +++ b/Hua.DDNS/DDNSProviders/Dnspod/DnspodDDNSProvider.cs @@ -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, diff --git a/Hua.DDNS/DDNSProviders/Namesilo/NamesiloDDNSProvider.cs b/Hua.DDNS/DDNSProviders/Namesilo/NamesiloDDNSProvider.cs index 0fa57c2..29c48bb 100644 --- a/Hua.DDNS/DDNSProviders/Namesilo/NamesiloDDNSProvider.cs +++ b/Hua.DDNS/DDNSProviders/Namesilo/NamesiloDDNSProvider.cs @@ -58,13 +58,14 @@ namespace Hua.DDNS.DDNSProviders.Namesilo } return (from record in records.Cast() - 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 /// 创建后的解析记录信息 public async Task 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; diff --git a/Hua.DDNS/Jobs/AppJob.cs b/Hua.DDNS/Jobs/AppJob.cs deleted file mode 100644 index f805200..0000000 --- a/Hua.DDNS/Jobs/AppJob.cs +++ /dev/null @@ -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 _logger; -// private readonly SettingProvider _settingProvider; -// private readonly DdnsOption _ddnsOption; -// private readonly IHttpHelper _httpHelper; -// public string CurrentIpv4Address; - - - -// public AppJob(ILogger 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已销毁"); -// } -// } -//} \ No newline at end of file diff --git a/Hua.DDNS/Jobs/AppJobContext.cs b/Hua.DDNS/Jobs/AppJobContext.cs deleted file mode 100644 index ae01e28..0000000 --- a/Hua.DDNS/Jobs/AppJobContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Hua.DDNS.Jobs -{ - - /// - /// Job上下文 - /// - public class AppJobContext - { - } -} diff --git a/Hua.DDNS/Jobs/NewJob.cs b/Hua.DDNS/Jobs/DdnsJob.cs similarity index 56% rename from Hua.DDNS/Jobs/NewJob.cs rename to Hua.DDNS/Jobs/DdnsJob.cs index f6252e3..9fa3413 100644 --- a/Hua.DDNS/Jobs/NewJob.cs +++ b/Hua.DDNS/Jobs/DdnsJob.cs @@ -26,12 +26,12 @@ using System.Net.Sockets; namespace Hua.DDNS.Jobs { /// - /// 新的 DDNS 任务类,用于定期检查并更新域名解析记录 + /// DDNS 任务类,用于定期检查并更新域名解析记录 /// [DisallowConcurrentExecution] - public class NewJob : IJob, IDisposable + public class DdnsJob : IJob, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly DdnsOption _ddnsOption; private readonly IHttpHelper _httpHelper; @@ -48,7 +48,7 @@ namespace Hua.DDNS.Jobs /// Http 助手 /// DDNS 配置选项 /// 服务提供者 - public NewJob(ILogger logger,IHttpHelper httpHelper,IOptions ddnsOption, IServiceProvider serviceProvider) + public DdnsJob(ILogger logger,IHttpHelper httpHelper,IOptions 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(); - //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 /// public void Dispose() { - _logger.LogInformation("AppJob已销毁"); + _logger.LogInformation("DdnsJob已销毁"); } } } \ No newline at end of file diff --git a/Hua.DDNS/Jobs/SslDownloadJob.cs b/Hua.DDNS/Jobs/SslDownloadJob.cs index 4a0df61..b8a045c 100644 --- a/Hua.DDNS/Jobs/SslDownloadJob.cs +++ b/Hua.DDNS/Jobs/SslDownloadJob.cs @@ -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 { /// - /// SSL 证书下载任务类,用于定期检查并更新 SSL 证书 + /// SSL 证书管理任务类,用于定期检查、申请并更新 SSL 证书 /// [DisallowConcurrentExecution] public class SslDownloadJob : IJob, IDisposable @@ -17,6 +18,7 @@ namespace Hua.DDNS.Jobs private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly SslDownloadOption _sslDownloadOption; + private readonly DdnsOption _ddnsOption; /// /// 构造函数 @@ -24,14 +26,17 @@ namespace Hua.DDNS.Jobs /// 日志对象 /// 服务提供者 /// SSL 下载配置选项 + /// DDNS 配置选项 public SslDownloadJob( ILogger logger, IServiceProvider serviceProvider, - IOptions sslDownloadOption) + IOptions sslDownloadOption, + IOptions ddnsOption) { _logger = logger; _serviceProvider = serviceProvider; _sslDownloadOption = sslDownloadOption.Value; + _ddnsOption = ddnsOption.Value; } /// @@ -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(); - foreach (var item in _sslDownloadOption.DownloadItems) + // 合并 DownloadItems 和 DdnsOption.SubDomainArray + var itemsToProcess = _sslDownloadOption.DownloadItems?.ToList() ?? new List(); + 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文件管理任务执行失败"); } } + /// + /// 判断证书是否可以下载(已签发) + /// + /// 证书信息 + /// 所属平台 + /// 是否可下载 + private bool IsDownloadable(SslCertificate certificate, SslPlatformEnum platform) + { + return platform switch + { + SslPlatformEnum.Ali => certificate.StatusMsg == "ISSUED", + SslPlatformEnum.Tencent => certificate.Status == 1, + _ => false + }; + } + /// /// 获取本地证书的过期时间 /// @@ -163,7 +234,7 @@ namespace Hua.DDNS.Jobs /// 证书 ID /// 域名 /// Task - 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 { diff --git a/Hua.DDNS/SslProviders/Ali/AliSslApplyOption.cs b/Hua.DDNS/SslProviders/Ali/AliSslApplyOption.cs new file mode 100644 index 0000000..137b751 --- /dev/null +++ b/Hua.DDNS/SslProviders/Ali/AliSslApplyOption.cs @@ -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? Tags { get; set; } + } +} diff --git a/Hua.DDNS/SslProviders/Ali/AliSslProvider.cs b/Hua.DDNS/SslProviders/Ali/AliSslProvider.cs index 6a5dbb9..df78724 100644 --- a/Hua.DDNS/SslProviders/Ali/AliSslProvider.cs +++ b/Hua.DDNS/SslProviders/Ali/AliSslProvider.cs @@ -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 { /// - /// 阿里云 SSL 证书下载提供者 + /// 阿里云 SSL 证书管理提供者 /// - public class AliSslProvider : ISslDownloadProvider + public class AliSslProvider : ISslManagementProvider { private readonly Client _client; private readonly AliCloudOption _aliCloudOption; + private readonly AliSslApplyOption _aliSslApplyOption; private readonly ILogger _logger; /// @@ -24,10 +26,12 @@ namespace Hua.DDNS.SslProviders.Ali /// 阿里云配置 public AliSslProvider( ILogger logger, - IOptions aliCloudOption) + IOptions aliCloudOption, + IOptions 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; } } + + /// + /// 异步申请 SSL 证书 + /// + /// 域名 + /// 申请结果,成功返回证书 ID,失败返回 null + public async Task 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; + } + } } } diff --git a/Hua.DDNS/SslProviders/ISslDownloadProvider.cs b/Hua.DDNS/SslProviders/ISslManagementProvider.cs similarity index 83% rename from Hua.DDNS/SslProviders/ISslDownloadProvider.cs rename to Hua.DDNS/SslProviders/ISslManagementProvider.cs index 3962ed4..d73e4a2 100644 --- a/Hua.DDNS/SslProviders/ISslDownloadProvider.cs +++ b/Hua.DDNS/SslProviders/ISslManagementProvider.cs @@ -1,9 +1,9 @@ namespace Hua.DDNS.SslProviders { /// - /// SSL 证书下载提供者接口 + /// SSL 证书管理提供者接口 /// - public interface ISslDownloadProvider + public interface ISslManagementProvider { /// /// 异步获取 SSL 证书列表 @@ -20,6 +20,13 @@ namespace Hua.DDNS.SslProviders /// 下载成功返回 true,否则返回 false Task DownloadCertificateAsync(string certificateId, string savePath, string fileName); + /// + /// 异步申请 SSL 证书 + /// + /// 域名 + /// 申请结果,成功返回证书 ID,失败返回 null + Task ApplyCertificateAsync(string domain); + /// /// 异步清理无效证书 /// diff --git a/Hua.DDNS/SslProviders/Tencent/TencentSslProvider.cs b/Hua.DDNS/SslProviders/Tencent/TencentSslProvider.cs index c8e54d3..e23b568 100644 --- a/Hua.DDNS/SslProviders/Tencent/TencentSslProvider.cs +++ b/Hua.DDNS/SslProviders/Tencent/TencentSslProvider.cs @@ -11,9 +11,9 @@ using System.IO.Compression; namespace Hua.DDNS.SslProviders.Tencent { /// - /// 腾讯云 SSL 证书下载提供者 + /// 腾讯云 SSL 证书管理提供者 /// - 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; } } + + /// + /// 异步申请 SSL 证书 + /// + /// 域名 + /// 申请结果,成功返回证书 ID,失败返回 null + public async Task 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; + } + } } } diff --git a/Hua.DDNS/Start/Program.cs b/Hua.DDNS/Start/Program.cs index 3d88c96..1ceafbc 100644 --- a/Hua.DDNS/Start/Program.cs +++ b/Hua.DDNS/Start/Program.cs @@ -69,6 +69,7 @@ namespace Hua.DDNS.Start services.Configure(hostContext.Configuration.GetSection("TencentCloud")); services.Configure(hostContext.Configuration.GetSection("AliCloud")); services.Configure(hostContext.Configuration.GetSection("SslDownload")); + services.Configure(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(j => j + // 配置 DDNS 任务 (DdnsJob) + var appJobKey = new JobKey("DdnsJob", "DdnsJobGroup"); + q.AddJob(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 }); } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index c056422..423c436 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ Hua.DDNS/ │ └── ... ├── Jobs/ # Scheduled jobs │ ├── AppJob.cs -│ ├── NewJob.cs +│ ├── DdnsJob.cs │ └── SslDownloadJob.cs ├── Models/ # Data models ├── SslProviders/ # SSL certificate providers