Compare commits

11 Commits

Author SHA1 Message Date
ShaoHua
c78e7a248c reload readme doc 2026-01-01 23:15:00 +08:00
ShaoHua
c4e9811004 添加自动下载ssl证书 2026-01-01 18:47:51 +08:00
ShaoHua
6d66b6532b Supports running on docker 2024-06-28 00:10:01 +08:00
ShaoHua
08e7bf004b update to net8.update nuget lib 2024-06-13 00:13:34 +08:00
801061f4f2 if this dns record is not exits.create the dns record. 2023-07-16 23:50:15 +08:00
ee85ccc8b9 Bug 2023-05-31 23:22:59 +08:00
f027198279 update README.md 2023-05-29 00:09:15 +08:00
fe43eeb6c2 update README.md 2023-05-29 00:00:31 +08:00
7c3f039911 重写Job实现方式 2023-05-28 23:42:43 +08:00
f8df2fe468 change Platform to a EnumType 2023-04-23 00:03:52 +08:00
5ddf355799 add description in english 2023-04-22 23:50:34 +08:00
38 changed files with 1618 additions and 340 deletions

View File

@@ -1,25 +0,0 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -22,7 +22,7 @@ namespace Hua.DDNS.Test
.Build();
var sc = DIConfig.ConfigureServices(config);
var job = sc.GetService<AppJob>();
var job = sc.GetService<NewJob>();
job?.Execute(null);
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -41,7 +41,7 @@ namespace Hua.DDNS.Test.Start
services.AddSingleton<Url>();
services.AddSingleton<SqlHelper>();
services.AddTransient<IHttpHelper, HttpHelper>();
services.AddTransient<AppJob>();
services.AddTransient<NewJob>();
return services.BuildServiceProvider();
}
}

View File

@@ -0,0 +1,10 @@
namespace Hua.DDNS.Common.Config.Options
{
public class AliCloudOption
{
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string RegionId { get; set; }
public string Endpoint { get; set; }
}
}

View File

@@ -1,8 +1,18 @@
namespace Hua.DDNS.Common.Config.Options
using Hua.DDNS.DDNSProviders;
namespace Hua.DDNS.Common.Config.Options
{
/// <summary>
/// app configuration class
/// </summary>
public class AppOption
{
public DomainOption Domain { get; set; }
/// <summary>
/// domain configuration
/// </summary>
public DdnsOption DDNS { get; set; }
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Hua.DDNS.Common.Config.Options
{
public class DomainOption
{
/// <summary>
/// 平台
/// </summary>
public string Platform { get; set; }
/// <summary>
/// Id
/// </summary>
public string Id { get; set; }
/// <summary>
/// Key
/// </summary>
public string Key { get; set; }
/// <summary>
/// 域名
/// </summary>
public string domain { get; set; }
/// <summary>
/// 子域列表
/// </summary>
public string[] subDomainArray { get; set; }
/// <summary>
/// 解析记录类型
/// </summary>
public string type { get; set; }
/// <summary>
/// 间隔时间 秒
/// </summary>
public string time { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Hua.DDNS.Common.Config.Options
{
/// <summary>
/// DDNS Platform
/// </summary>
public enum PlatformEnum
{
/// <summary>
/// Ali
/// </summary>
Ali = 1,
/// <summary>
/// Tencent
/// </summary>
Tencent,
/// <summary>
/// Namesilo
/// </summary>
Namesilo
}
}

View File

@@ -0,0 +1,24 @@
namespace Hua.DDNS.Common.Config.Options
{
public enum SslPlatformEnum
{
Ali = 1,
Tencent = 2
}
public class SslDownloadOption
{
public bool Enabled { get; set; }
public string Corn { get; set; }
public SslPlatformEnum Platform { get; set; }
public string SavePath { get; set; }
public int ExpireDays { get; set; }
public List<SslDownloadItem> DownloadItems { get; set; }
}
public class SslDownloadItem
{
public string Domain { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using Hua.DDNS.DDNSProviders.Dnspod;
namespace Hua.DDNS.Common.Config.Options
{
public class TencentCloudOption
{
public string SecretId { get; set; }
public string SecretKey { get; set; }
public string Region { get; set; }
public DnspodOption Dnspod { get; set; }
}
}

View File

@@ -2,13 +2,19 @@
namespace Hua.DDNS.Common.Config
{
/// <summary>
/// this is a strongly-typed configuration provider
/// </summary>
public class SettingProvider
{
private readonly AppOption _app;
private readonly IConfiguration _configuration;
public SettingProvider(IConfiguration configuration)
{
_configuration = configuration;
_app = new AppOption();
configuration.GetSection("App").Bind(_app);
_configuration.GetSection("App").Bind(_app);
}
public AppOption App => _app;

View File

@@ -8,10 +8,12 @@ namespace Hua.DDNS.Common.Http
{
private static ILogger<HttpHelper> _logger;
private static HttpClientHandler _handler;
private IConfiguration _configuration;
public HttpHelper(ILogger<HttpHelper> logger)
public HttpHelper(ILogger<HttpHelper> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
_handler = new HttpClientHandler();
}
@@ -85,31 +87,45 @@ namespace Hua.DDNS.Common.Http
#region
/// <summary>
/// http下载文件 (仅支持小文件)
/// http下载文件
/// </summary>
/// <param name="url">下载文件地址</param>
/// <param name="localPath">文件存放地址,包含文件名</param>
/// <returns></returns>
public bool DownloadFile(string url, string localPath)
{
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
var request = WebRequest.Create(url) as HttpWebRequest;
Stream stream = new FileStream(localPath, FileMode.CreateNew);
try
{
// 设置参数
//发送请求并获取相应回应数据
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
var request = WebRequest.Create(url) as HttpWebRequest;
var response = request?.GetResponse() as HttpWebResponse;
//直到request.GetResponse()程序才开始向目标网页发送Post请求
var responseStream = response?.GetResponseStream();
//创建本地文件写入流
stream.Close();
responseStream?.Close();
if (responseStream == null)
{
_logger.LogError($"下载文件失败: {url}, 无法获取响应流");
return false;
}
var directory = Path.GetDirectoryName(localPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using var fileStream = new FileStream(localPath, FileMode.Create);
responseStream.CopyTo(fileStream);
fileStream.Close();
responseStream.Close();
response?.Close();
_logger.LogInformation($"文件下载成功: {url} -> {localPath}");
return true;
}
catch (Exception ex)
{
_logger.LogError($"下载文件失败: {url}, 错误: {ex.Message}");
return false;
}
}
@@ -118,12 +134,12 @@ namespace Hua.DDNS.Common.Http
/// <summary>
/// 获得当前机器的公网 IP
/// 获得当前机器的公网 Ip
/// </summary>
public async Task<string> GetCurrentPublicIpv4()
{
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "http://175.24.175.136:8008/WebUtil/GetIp");
using var request = new HttpRequestMessage(HttpMethod.Get, _configuration["App:GetIpv4Url"]);
using var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}

View File

@@ -0,0 +1,9 @@
namespace Hua.DDNS.DDNSProviders.Ali
{
/// <summary>
/// domain configuration Ali
/// </summary>
public class AliDdnsOption
{
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AlibabaCloud.OpenApiClient.Models;
using AlibabaCloud.SDK.Alidns20150109;
using AlibabaCloud.SDK.Alidns20150109.Models;
using AlibabaCloud.TeaUtil.Models;
using AutoMapper;
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.Models;
using Microsoft.Extensions.Options;
namespace Hua.DDNS.DDNSProviders.Ali
{
/// <summary>
/// DDNSProvider for Ali
/// </summary>
public class AliDdnsProvider : IDdnsProvider
{
private readonly Client _client;
private readonly AliCloudOption _aliCloudOption;
private readonly DdnsOption _ddnsOption;
private readonly IMapper _mapper;
public AliDdnsProvider(IOptions<AliCloudOption> aliCloudOption, IMapper mapper,IOptions<DdnsOption> ddnsOption)
{
_aliCloudOption = aliCloudOption.Value;
_ddnsOption = ddnsOption.Value;
_mapper = mapper;
_client = new Client(new Config()
{
AccessKeyId = _aliCloudOption.AccessKeyId,
AccessKeySecret = _aliCloudOption.AccessKeySecret,
Endpoint = _aliCloudOption.Endpoint,
});
}
/// <summary>
/// 获取解析记录列表
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<DnsRecord>?> GetRecordListAsync()
{
var record = (await _client.DescribeDomainRecordsAsync(new DescribeDomainRecordsRequest()
{
DomainName = _ddnsOption.Domain
})).Body.DomainRecords.Record;
return _mapper.Map<IEnumerable<DnsRecord>>(record);
}
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord record)
{
var rep = await _client.AddDomainRecordAsync(_mapper.Map<AddDomainRecordRequest>(record));
return record;
}
/// <summary>
/// 变更解析记录列表
/// </summary>
/// <param name="newIp"></param>
/// <param name="records"></param>
/// <returns></returns>
public async Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records)
{
foreach (var aliDomainRecord in records)
{
await _client.UpdateDomainRecordAsync(_mapper.Map<UpdateDomainRecordRequest>(aliDomainRecord));
}
return records;
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.DDNSProviders.Namesilo;
namespace Hua.DDNS.DDNSProviders
{
/// <summary>
/// domain configuration class
/// </summary>
public class DdnsOption
{
/// <summary>
/// platform from 1 Ali 2 Tencent 3
/// </summary>
public PlatformEnum Platform { get; set; }
/// <summary>
/// domain
/// </summary>
public string Domain { get; set; }
/// <summary>
/// sub domain, eg. www,git
/// </summary>
public string[] SubDomainArray { get; set; }
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.Models;
using Microsoft.Extensions.Options;
using TencentCloud.Common.Profile;
using TencentCloud.Common;
using TencentCloud.Dnspod.V20210323;
using System.Net;
using TencentCloud.Dnspod.V20210323.Models;
using Newtonsoft.Json.Linq;
namespace Hua.DDNS.DDNSProviders.Dnspod
{
/// <summary>
/// DdnsProvider for Dnspod
/// <remarks></remarks>
/// </summary>
public class DnspodDdnsProvider : IDdnsProvider
{
private readonly DnspodClient _client;
private readonly TencentCloudOption _tencentCloudOption;
private readonly DdnsOption _ddnsOption;
private readonly IMapper _mapper;
public DnspodDdnsProvider(IMapper mapper, IOptions<TencentCloudOption> tencentCloudOption, IOptions<DdnsOption> ddnsOption)
{
_mapper = mapper;
_tencentCloudOption = tencentCloudOption.Value;
_ddnsOption = ddnsOption.Value;
_client = new DnspodClient(
new Credential { SecretId = _tencentCloudOption.SecretId, SecretKey = _tencentCloudOption.SecretKey },
"",
new ClientProfile()
{
HttpProfile = new HttpProfile { Endpoint = _tencentCloudOption.Dnspod.Endpoint }
});
}
public async Task<IEnumerable<DnsRecord>?> GetRecordListAsync()
{
var recordList = (await _client.DescribeRecordList(new DescribeRecordListRequest() { Domain = _ddnsOption.Domain })).RecordList;
return _mapper.Map<IEnumerable<DnsRecord>>(recordList);
}
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord record)
{
var response = await _client.CreateRecord(new CreateRecordRequest()
{
Domain = _ddnsOption.Domain,
Value = record.Ip,
RecordLine = "默认",
RecordType = record.RecordType,
TTL = Convert.ToUInt32(record.TTL),
});
return record;
}
public async Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records)
{
var rep = await _client.ModifyRecordBatch(new ModifyRecordBatchRequest()
{
RecordIdList = records.Select(m => (ulong?)Convert.ToUInt64(m.Id)).ToArray(),
Change = "value",
ChangeTo = newIp
});
return records;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Hua.DDNS.DDNSProviders.Dnspod
{
/// <summary>
/// domain configuration Dnspod
/// </summary>
public class DnspodOption
{
/// <summary>
/// Endpoint dnspod.tencentcloudapi.com
/// </summary>
public string Endpoint { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Hua.DDNS.Models;
namespace Hua.DDNS.DDNSProviders
{
/// <summary>
/// Dynamic domain name resolution provider
/// </summary>
public interface IDdnsProvider
{
/// <summary>
/// 获取域名解析记录列表
/// </summary>
/// <returns></returns>
Task<IEnumerable<DnsRecord>?> GetRecordListAsync();
/// <summary>
/// 创建解析记录
/// </summary>
/// <param name="record"></param>
/// <returns></returns>
Task<DnsRecord> CreateDnsRecordAsync(DnsRecord record);
/// <summary>
/// 修改域名解析记录
/// </summary>
/// <param name="newIp"></param>
/// <param name="records"></param>
/// <returns></returns>
Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records);
}
}

View File

@@ -0,0 +1,126 @@
using System.Xml;
using AutoMapper;
using Hua.DDNS.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Hua.DDNS.DDNSProviders.Namesilo
{
/// <summary>
/// DDNSProvider for namesilo
/// </summary>
public class NamesiloDdnsProvider : IDdnsProvider
{
public readonly NamesiloOption _namesiloOption;
public readonly DdnsOption _ddnsOption;
public NamesiloDdnsProvider(IOptions<NamesiloOption> namesiloOption, IOptions<DdnsOption> ddnsOption)
{
_ddnsOption = ddnsOption.Value;
_namesiloOption = namesiloOption.Value;
}
public async Task<IEnumerable<DnsRecord>?> GetRecordListAsync()
{
var client = new HttpClient();
var response =
await client.GetAsync($"https://www.namesilo.com/api/dnsListRecords?version=1&type=xml&key={_namesiloOption.ApiKey}&domain={_ddnsOption.Domain}");
var content = response.Content.ReadAsStringAsync().Result;
var reply = new XmlDocument();
reply.LoadXml(content);
var status = reply.SelectSingleNode("/namesilo/reply/code/text()");
if (status == null)
{
return null;
}
if (status.Value != "300")
{
throw new Exception($"Failed to retrieve value. Check API key.{status}");
}
var records = reply.SelectNodes($"/namesilo/reply/resource_record/host");
if (records == null)
{
return new List<DnsRecord>();
}
return (from record in records.Cast<XmlNode>()
let subDomain = record.ParentNode.SelectSingleNode("host/text()").Value.Replace(_ddnsOption.Domain, "")
where _ddnsOption.SubDomainArray.Contains(subDomain)
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,
Domain = _ddnsOption.Domain,
TTL = record.ParentNode.SelectSingleNode("ttl/text()").Value,
SubDomain = subDomain,
}).ToList();
}
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord dnsRecord)
{
var host = dnsRecord.Host[..(dnsRecord.Host.Length - dnsRecord.Domain.Length - 1)];
//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}";
using var client = new HttpClient();
{
var response = await client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
var reply = new XmlDocument();
reply.LoadXml(content);
var status = reply.SelectSingleNode("/namesilo/reply/code/text()");
if (status == null)
{
await Console.Error.WriteLineAsync($"Failed to create record: '{JsonConvert.SerializeObject(dnsRecord)}'");
}
if (status.Value == "300")
{
return null;
//continue;
}
}
return dnsRecord;
}
public async Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records)
{
foreach (var dnsRecord in records)
{
using var client = new HttpClient();
{
var host = dnsRecord.Host[..(dnsRecord.Host.Length - dnsRecord.Domain.Length - 1)];
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);
var response = await client.GetAsync(request);
var content = await response.Content.ReadAsStringAsync();
var reply = new XmlDocument();
reply.LoadXml(content);
var status = reply.SelectSingleNode("/namesilo/reply/code/text()");
if (status == null)
{
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'.");
continue; //return false;
}
if (status.Value == "300") continue;
}
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'.");
continue; //return false;
}
return records;
}
}
}

View File

@@ -0,0 +1,16 @@
namespace Hua.DDNS.DDNSProviders.Namesilo
{
/// <summary>
/// Namesilo Option
/// </summary>
public class NamesiloOption
{
/// <summary>
/// API key
/// </summary>
public string ApiKey { get; set; }
}
}

View File

@@ -3,15 +3,15 @@
#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed.
#For more information, please see https://aka.ms/containercompat
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Hua.DDNS/Hua.DDNS.csproj", "Hua.DDNS/"]
COPY ["/Hua.DDNS.csproj", "Hua.DDNS/"]
RUN dotnet restore "Hua.DDNS/Hua.DDNS.csproj"
COPY . .
WORKDIR "/src/Hua.DDNS"
WORKDIR "/Hua.DDNS"
RUN dotnet build "Hua.DDNS.csproj" -c Release -o /app/build
FROM build AS publish

View File

@@ -1,41 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Hua.DDNS-C4DADDFF-6D5B-4BD5-AB11-02F07B517CAC</UserSecretsId>
<DockerDefaultTargetOS>Windows</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Hua.DDNS-C4DADDFF-6D5B-4BD5-AB11-02F07B517CAC</UserSecretsId>
<SelfContained>true</SelfContained>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Alidns20150109" Version="3.0.0" />
<PackageReference Include="Hua.DotNet.Code" Version="0.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
<PackageReference Include="Npgsql" Version="6.0.3" />
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.4.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.4.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="TencentCloudSDK.Dnspod" Version="3.0.623" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Alidns20150109" Version="3.5.0" />
<PackageReference Include="AlibabaCloud.SDK.Cas20200407" Version="3.0.4" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Hua.DotNet.Code" Version="0.0.15" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
<PackageReference Include="Npgsql" Version="8.0.3" />
<PackageReference Include="QRCoder" Version="1.5.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.9.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="TencentCloudSDK.Dnspod" Version="3.0.1024" />
<PackageReference Include="TencentCloudSDK.Ssl" Version="3.0.1371" />
</ItemGroup>
<ItemGroup>
<None Update="InstallServiceByNssm.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="nssm.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="InstallServiceByNssm.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="nssm.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,136 +1,138 @@
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 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 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 DomainOption _domainOption;
private readonly IHttpHelper _httpHelper;
public string CurrentIpv4Address;
//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;
_domainOption = _settingProvider.App.Domain;
// 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($"{_domainOption.subDomainArray.First()}.{_domainOption.domain}")).AddressList.First();
CurrentIpv4Address = await _httpHelper.GetCurrentPublicIpv4();
// }
// 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("任务执行完成");
}
}
// if (CurrentIpv4Address!=oldIp.ToString())
// {
// await UpdateDns();
// }
// }
// catch (Exception e)
// {
// _logger.LogError(e,e.Message);
// }
// finally
// {
// _logger.LogInformation("任务执行完成");
// }
// }
private async Task UpdateDns()
{
//更新Ip记录
switch (_domainOption.Platform)
{
case "Tencent":
var _dnspodClient = new DnspodClient(
// 实例化一个认证对象入参需要传入腾讯云账户secretIdsecretKey,此处还需注意密钥对的保密
// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
new Credential { SecretId = _domainOption.Id, SecretKey = _domainOption.Key },
"",
// 实例化一个client选项可选的没有特殊需求可以跳过
new ClientProfile()
{
// 实例化一个http选项可选的没有特殊需求可以跳过
HttpProfile = new HttpProfile { Endpoint = ("dnspod.tencentcloudapi.com") }
});
// private async Task UpdateDns()
// {
// //更新Ip记录
// switch (_ddnsOption.Platform)
// {
// case PlatformEnum.Namesilo:
// case PlatformEnum.Tencent:
// var _dnspodClient = new DnspodClient(
// // 实例化一个认证对象入参需要传入腾讯云账户secretIdsecretKey,此处还需注意密钥对的保密
// // 密钥可前往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 = _domainOption.domain });
var record = describeRecordList.RecordList.FirstOrDefault(m =>
m.Value == CurrentIpv4Address && _domainOption.subDomainArray.Any(n => m.Name == n));
if (record!=null && record.Value == CurrentIpv4Address) return;//如果记录已经变更,不调用更新接口
// //获取域名解析记录
// 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 && _domainOption.subDomainArray.Any(n => m.Name == n))
.Select(m => m.RecordId)
.ToArray(),
Change = "value",
ChangeTo = CurrentIpv4Address
});
// 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 "Ali":
var aliClient = new AlibabaCloud.SDK.Alidns20150109.Client(new Config()
{
// 您的 AccessKey ID
AccessKeyId = _domainOption.Id,
// 您的 AccessKey Secret
AccessKeySecret = _domainOption.Key,
Endpoint = "alidns.cn-beijing.aliyuncs.com",
});
// 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 = _domainOption.domain
})).Body.DomainRecords.Record;
// var aliDescribeRecordList = (await aliClient.DescribeDomainRecordsAsync(new DescribeDomainRecordsRequest()
// {
// DomainName = _ddnsOption.Domain
// })).Body.DomainRecords.Record;
foreach (var aliDomainRecord in aliDescribeRecordList
.Where(m => m.Value != CurrentIpv4Address && _domainOption.subDomainArray.Any(n => m.RR == n)))
{
await aliClient.UpdateDomainRecordAsync(new UpdateDomainRecordRequest()
{
RecordId = aliDomainRecord.RecordId,
RR = aliDomainRecord.RR,
Type = aliDomainRecord.Type,
Value = CurrentIpv4Address,
});
_logger.LogInformation($"更新域名解析记录{aliDomainRecord.Value} To {CurrentIpv4Address}");
}
break;
}
}
// 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已销毁");
}
}
}
// public void Dispose()
// {
// _logger.LogInformation("AppJob已销毁");
// }
// }
//}

106
Hua.DDNS/Jobs/NewJob.cs Normal file
View File

@@ -0,0 +1,106 @@
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.DDNS.DDNSProviders;
using Hua.DDNS.DDNSProviders.Ali;
using Hua.DDNS.DDNSProviders.Dnspod;
using Hua.DDNS.DDNSProviders.Namesilo;
using Hua.DDNS.Models;
using Hua.DotNet.Code.Extension;
using Microsoft.Extensions.Options;
using TencentCloud.Common;
using TencentCloud.Common.Profile;
using TencentCloud.Dnspod.V20210323;
using TencentCloud.Dnspod.V20210323.Models;
using Tea;
using Tea.Utils;
using System.Net.Sockets;
namespace Hua.DDNS.Jobs
{
[DisallowConcurrentExecution]
public class NewJob : IJob, IDisposable
{
private readonly ILogger<NewJob> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly DdnsOption _ddnsOption;
private readonly IHttpHelper _httpHelper;
public string newIp;
public NewJob(ILogger<NewJob> logger,IHttpHelper httpHelper,IOptions<DdnsOption> ddnsOption, IServiceProvider serviceProvider)
{
_logger = logger;
_httpHelper = httpHelper;
_serviceProvider = serviceProvider;
_ddnsOption = ddnsOption.Value;
}
public async Task Execute(IJobExecutionContext context)
{
//2.获取DNS记录
IDdnsProvider? ddnsProvider = _ddnsOption.Platform switch
{
PlatformEnum.Namesilo => _serviceProvider.GetRequiredService<NamesiloDdnsProvider>(),
PlatformEnum.Tencent => _serviceProvider.GetRequiredService<DnspodDdnsProvider>(),
PlatformEnum.Ali => _serviceProvider.GetRequiredService<AliDdnsProvider>(),
_ => null
};
newIp = await _httpHelper.GetCurrentPublicIpv4();
try
{
//1. 获取当前机器ip
var domain = $"{_ddnsOption.SubDomainArray.First()}.{_ddnsOption.Domain}";
IPAddress oldIp = null;
oldIp = (await Dns.GetHostEntryAsync(domain)).AddressList.First();
//1.1 如果当前dns记录与实际dns记录一致跳出本次执行
if (newIp == oldIp.ToString()) return;
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.比较并更新
await ddnsProvider.ModifyRecordListAsync(newIp,
dnsRecordList.Where(m => m.Ip != newIp && _ddnsOption.SubDomainArray.Any(n => m.SubDomain == n)));
}
catch (SocketException e)
{
if (e.Message.Contains("不知道这样的主机"))
{
await ddnsProvider.CreateDnsRecordAsync(new DnsRecord()
{
Ip = newIp,
Host = _ddnsOption.SubDomainArray.First(),
Domain = _ddnsOption.Domain,
});
}
}
catch (Exception e)
{
_logger.LogError(e,e.Message);
}
finally
{
_logger.LogInformation("任务执行完成");
}
}
public void Dispose()
{
_logger.LogInformation("AppJob已销毁");
}
}
}

View File

@@ -0,0 +1,133 @@
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.SslProviders;
using Hua.DDNS.SslProviders.Ali;
using Hua.DDNS.SslProviders.Tencent;
using Quartz;
using Microsoft.Extensions.Options;
namespace Hua.DDNS.Jobs
{
[DisallowConcurrentExecution]
public class SslDownloadJob : IJob, IDisposable
{
private readonly ILogger<SslDownloadJob> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly SslDownloadOption _sslDownloadOption;
public SslDownloadJob(
ILogger<SslDownloadJob> logger,
IServiceProvider serviceProvider,
IOptions<SslDownloadOption> sslDownloadOption)
{
_logger = logger;
_serviceProvider = serviceProvider;
_sslDownloadOption = sslDownloadOption.Value;
}
public async Task Execute(IJobExecutionContext context)
{
if (!_sslDownloadOption.Enabled)
{
_logger.LogInformation("SSL下载任务已禁用跳过执行");
return;
}
_logger.LogInformation("开始SSL文件下载任务");
try
{
ISslDownloadProvider? sslProvider = _sslDownloadOption.Platform switch
{
SslPlatformEnum.Ali => _serviceProvider.GetService(typeof(AliSslProvider)) as ISslDownloadProvider,
SslPlatformEnum.Tencent => _serviceProvider.GetService(typeof(TencentSslProvider)) as ISslDownloadProvider,
_ => null
};
if (sslProvider == null)
{
_logger.LogError($"未找到 SSL 下载提供者: {_sslDownloadOption.Platform}");
return;
}
;
if (!Directory.Exists(_sslDownloadOption.SavePath))
{
Directory.CreateDirectory(_sslDownloadOption.SavePath);
_logger.LogInformation($"创建目录: {_sslDownloadOption.SavePath}");
}
var certificates = await sslProvider.GetCertificatesAsync();
var downloadTasks = new List<Task>();
foreach (var item in _sslDownloadOption.DownloadItems)
{
var matchingCertificates = certificates.Where(c => c.Domain == item.Domain).ToList();
if (matchingCertificates.Count == 0)
{
_logger.LogWarning($"未找到域名 {item.Domain} 的证书,跳过下载");
continue;
}
var certificate = matchingCertificates.OrderByDescending(c => c.CertEndTime).First();
var daysUntilExpiry = (certificate.CertEndTime - DateTime.Now).Days;
if (daysUntilExpiry <= _sslDownloadOption.ExpireDays)
{
_logger.LogInformation($"证书 {certificate.CertificateId} ({certificate.Domain}) 将在 {daysUntilExpiry} 天后过期,开始下载");
downloadTasks.Add(DownloadFileAsync(sslProvider, item, certificate.CertificateId, certificate.Domain));
}
else
{
_logger.LogInformation($"证书 {certificate.CertificateId} ({certificate.Domain}) 还有 {daysUntilExpiry} 天过期,跳过下载");
}
}
await Task.WhenAll(downloadTasks);
_logger.LogInformation($"SSL文件下载任务完成共下载 {downloadTasks.Count} 个文件");
}
catch (Exception ex)
{
_logger.LogError(ex, "SSL文件下载任务执行失败");
}
}
private async Task DownloadFileAsync(ISslDownloadProvider provider, SslDownloadItem item, string certificateId, string domain)
{
try
{
var domainPath = Path.Combine(_sslDownloadOption.SavePath, domain);
if (!Directory.Exists(domainPath))
{
Directory.CreateDirectory(domainPath);
_logger.LogInformation($"创建域名目录: {domainPath}");
}
var localPath = Path.Combine(domainPath, item.FileName);
_logger.LogInformation($"开始下载证书: {certificateId} -> {localPath}");
var success = await provider.DownloadCertificateAsync(certificateId, domainPath, item.FileName);
if (success)
{
_logger.LogInformation($"证书下载成功: {domain}/{item.FileName}");
}
else
{
_logger.LogError($"证书下载失败: {domain}/{item.FileName}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"下载证书时发生错误: {item.FileName}");
}
}
public void Dispose()
{
_logger.LogInformation("SslDownloadJob已销毁");
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Hua.DDNS.Models;
public class DnsRecord
{
public string Id { get; set; }
public string Ip { get; set; }
public string Host { get; set; }
public string SubDomain { get; set; }
public string Domain { get; set; }
public string TTL { get; set; } = "10";
public string RecordType { get; set; } = "A";
public DnsRecord(string ip,string domain)
{
Ip = ip;
Host = domain;
}
public DnsRecord()
{
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AlibabaCloud.SDK.Alidns20150109.Models;
using AutoMapper;
using TencentCloud.Dnspod.V20210323.Models;
namespace Hua.DDNS.Models
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<DescribeDomainRecordsResponseBody.DescribeDomainRecordsResponseBodyDomainRecords.DescribeDomainRecordsResponseBodyDomainRecordsRecord, DnsRecord>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.RecordId))
.ForMember(dest => dest.RecordType, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Ip, opt => opt.MapFrom(src => src.Value))
.ForMember(dest => dest.SubDomain, opt => opt.MapFrom(src => src.RR))
;
CreateMap<DnsRecord, UpdateDomainRecordRequest> ()
.ForMember(dest => dest.RecordId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.RecordType))
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.Ip))
.ForMember(dest => dest.RR, opt => opt.MapFrom(src => src.SubDomain))
;
CreateMap<RecordListItem, DnsRecord>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.RecordId))
.ForMember(dest => dest.RecordType, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Ip, opt => opt.MapFrom(src => src.Value))
.ForMember(dest => dest.SubDomain, opt => opt.MapFrom(src => src.Name))
;
CreateMap<DnsRecord, AddDomainRecordRequest>()
//.ForMember(dest => dest., opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.RecordType))
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.Ip))
.ForMember(dest => dest.RR, opt => opt.MapFrom(src => src.SubDomain))
.ForMember(dest => dest.DomainName, opt => opt.MapFrom(src => src.Domain))
;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Hua.DDNS.SslProviders.Ali
{
public class AliSslOption
{
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string Endpoint { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
using AlibabaCloud.SDK.Cas20200407;
using AlibabaCloud.SDK.Cas20200407.Models;
using AlibabaCloud.OpenApiClient.Models;
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.SslProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Hua.DDNS.SslProviders.Ali
{
public class AliSslProvider : ISslDownloadProvider
{
private readonly Client _client;
private readonly AliCloudOption _aliCloudOption;
private readonly ILogger<AliSslProvider> _logger;
public AliSslProvider(
ILogger<AliSslProvider> logger,
IOptions<AliCloudOption> aliCloudOption)
{
_logger = logger;
_aliCloudOption = aliCloudOption.Value;
var config = new Config
{
AccessKeyId = _aliCloudOption.AccessKeyId,
AccessKeySecret = _aliCloudOption.AccessKeySecret,
Endpoint = _aliCloudOption.Endpoint
};
_client = new Client(config);
}
public async Task<List<SslCertificate>> GetCertificatesAsync()
{
try
{
_logger.LogWarning("阿里云 SSL 证书列表功能待实现");
return new List<SslCertificate>();
}
catch (Exception ex)
{
_logger.LogError(ex, "获取阿里云 SSL 证书列表失败");
throw;
}
}
public async Task<bool> DownloadCertificateAsync(string certificateId, string savePath, string fileName)
{
try
{
_logger.LogWarning($"阿里云 SSL 证书下载功能待实现: {certificateId}");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"下载证书失败: {certificateId}");
return false;
}
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Hua.DDNS.SslProviders
{
public interface ISslDownloadProvider
{
Task<List<SslCertificate>> GetCertificatesAsync();
Task<bool> DownloadCertificateAsync(string certificateId, string savePath, string fileName);
}
public class SslCertificate
{
public string CertificateId { get; set; }
public string Domain { get; set; }
public string Alias { get; set; }
public DateTime CertBeginTime { get; set; }
public DateTime CertEndTime { get; set; }
public int Status { get; set; }
public string StatusMsg { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using Hua.DDNS.Common.Config.Options;
namespace Hua.DDNS.SslProviders.Tencent
{
public class TencentSslOption
{
public string SecretId { get; set; }
public string SecretKey { get; set; }
public string Region { get; set; }
}
}

View File

@@ -0,0 +1,176 @@
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.SslProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TencentCloud.Common;
using TencentCloud.Common.Profile;
using TencentCloud.Ssl.V20191205;
using TencentCloud.Ssl.V20191205.Models;
using System.IO.Compression;
namespace Hua.DDNS.SslProviders.Tencent
{
public class TencentSslProvider : ISslDownloadProvider
{
private readonly SslClient _client;
private readonly TencentCloudOption _tencentCloudOption;
private readonly ILogger<TencentSslProvider> _logger;
public TencentSslProvider(
ILogger<TencentSslProvider> logger,
IOptions<TencentCloudOption> tencentCloudOption)
{
_logger = logger;
_tencentCloudOption = tencentCloudOption.Value;
_client = new SslClient(
new Credential { SecretId = _tencentCloudOption.SecretId, SecretKey = _tencentCloudOption.SecretKey },
_tencentCloudOption.Region,
new ClientProfile()
{
HttpProfile = new HttpProfile { Endpoint = "ssl.tencentcloudapi.com" }
});
}
public async Task<List<SslCertificate>> GetCertificatesAsync()
{
try
{
var request = new DescribeCertificatesRequest
{
Limit = 100,
Offset = 0
};
var response = await _client.DescribeCertificates(request);
var certificates = new List<SslCertificate>();
if (response.Certificates != null)
{
foreach (var cert in response.Certificates)
{
certificates.Add(new SslCertificate
{
CertificateId = cert.CertificateId,
Domain = cert.Domain,
Alias = cert.Alias,
CertBeginTime = DateTime.Parse(cert.CertBeginTime),
CertEndTime = DateTime.Parse(cert.CertEndTime),
Status = (int)(cert.Status ?? 0),
StatusMsg = cert.StatusMsg
});
}
}
_logger.LogInformation($"获取到 {certificates.Count} 个腾讯云 SSL 证书");
return certificates;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取腾讯云 SSL 证书列表失败");
throw;
}
}
public async Task<bool> DownloadCertificateAsync(string certificateId, string savePath, string fileName)
{
string tempZipPath = null;
string tempExtractPath = null;
try
{
var request = new DownloadCertificateRequest
{
CertificateId = certificateId
};
var response = await _client.DownloadCertificate(request);
if (response == null || string.IsNullOrEmpty(response.Content))
{
_logger.LogError($"证书内容为空: {certificateId}");
return false;
}
var zipBytes = Convert.FromBase64String(response.Content);
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
tempZipPath = Path.Combine(Path.GetTempPath(), $"ssl_{Guid.NewGuid()}.zip");
await File.WriteAllBytesAsync(tempZipPath, zipBytes);
_logger.LogInformation($"证书ZIP文件临时保存成功: {tempZipPath}");
tempExtractPath = Path.Combine(Path.GetTempPath(), $"ssl_extract_{Guid.NewGuid()}");
ZipFile.ExtractToDirectory(tempZipPath, tempExtractPath);
_logger.LogInformation($"证书ZIP文件解压成功: {tempExtractPath}");
var nginxPath = Path.Combine(tempExtractPath, "Nginx");
if (!Directory.Exists(nginxPath))
{
_logger.LogError($"ZIP文件中未找到Nginx目录");
return false;
}
var certFiles = Directory.GetFiles(nginxPath, "*.crt");
var keyFiles = Directory.GetFiles(nginxPath, "*.key");
if (certFiles.Length == 0 || keyFiles.Length == 0)
{
_logger.LogError($"Nginx目录中未找到证书文件或私钥文件");
return false;
}
var certFile = certFiles[0];
var keyFile = keyFiles[0];
var certFileName = Path.GetFileNameWithoutExtension(fileName);
var certSavePath = Path.Combine(savePath, $"{certFileName}_bundle.crt");
var keySavePath = Path.Combine(savePath, $"{certFileName}.key");
File.Copy(certFile, certSavePath, true);
File.Copy(keyFile, keySavePath, true);
_logger.LogInformation($"证书文件保存成功: {certSavePath}");
_logger.LogInformation($"私钥文件保存成功: {keySavePath}");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"下载证书失败: {certificateId}");
return false;
}
finally
{
try
{
if (tempExtractPath != null && Directory.Exists(tempExtractPath))
{
Directory.Delete(tempExtractPath, true);
_logger.LogInformation($"清理临时解压目录: {tempExtractPath}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"清理临时解压目录失败: {tempExtractPath}");
}
try
{
if (tempZipPath != null && File.Exists(tempZipPath))
{
File.Delete(tempZipPath);
_logger.LogInformation($"清理临时ZIP文件: {tempZipPath}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"清理临时ZIP文件失败: {tempZipPath}");
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
namespace Hua.DDNS.Start
{
public class Cache
{
/// <summary>
/// Tocken
/// </summary>
public static string Tocken { get; set; } = null;
}
}

View File

@@ -1,9 +1,20 @@
using System.Configuration;
using System.Reflection;
using Hua.DDNS.Common;
using Hua.DDNS.Common.Config;
using Hua.DDNS.Common.Config.Options;
using Hua.DDNS.Common.Http;
using Hua.DDNS.DDNSProviders;
using Hua.DDNS.DDNSProviders.Ali;
using Hua.DDNS.DDNSProviders.Dnspod;
using Hua.DDNS.DDNSProviders.Namesilo;
using Hua.DDNS.Jobs;
using Hua.DDNS.SslProviders;
using Hua.DDNS.SslProviders.Ali;
using Hua.DDNS.SslProviders.Tencent;
using Quartz;
using Serilog;
using Serilog.Extensions.Logging;
namespace Hua.DDNS.Start
{
@@ -11,6 +22,14 @@ namespace Hua.DDNS.Start
{
public static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
Path.Combine("Log\\log-.log"),
rollingInterval: RollingInterval.Day)
.CreateLogger();
await CreateHostBuilder(args).Build().RunAsync();
}
@@ -18,15 +37,23 @@ namespace Hua.DDNS.Start
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.UseSerilog()
.ConfigureAppConfiguration((context, config) =>
{
// clear all config provider
config.Sources.Clear();
config
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true,
reloadOnChange: true);
})
.ConfigureServices((hostContext, services) =>
{
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
Path.Combine("Log\\log-.log"),
rollingInterval: RollingInterval.Day)
.CreateLogger();
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.Configure<DdnsOption>(hostContext.Configuration.GetSection("DDNS"));
services.Configure<NamesiloOption>(hostContext.Configuration.GetSection("Namesilo"));
services.Configure<TencentCloudOption>(hostContext.Configuration.GetSection("TencentCloud"));
services.Configure<AliCloudOption>(hostContext.Configuration.GetSection("AliCloud"));
services.Configure<SslDownloadOption>(hostContext.Configuration.GetSection("SslDownload"));
ConfigDi(hostContext, services);
ConfigQuartz(hostContext, services);
});
@@ -34,10 +61,14 @@ namespace Hua.DDNS.Start
public static IServiceProvider ConfigDi(HostBuilderContext hostContext, IServiceCollection services)
{
services.AddSingleton<SettingProvider>();
//services.AddSingleton<SyncECMFileProvider>();
services.AddSingleton<Url>();
services.AddSingleton<SqlHelper>();
services.AddTransient<IHttpHelper, HttpHelper>();
services.AddTransient<NamesiloDdnsProvider>();
services.AddTransient<AliDdnsProvider>();
services.AddTransient<DnspodDdnsProvider>();
services.AddTransient<AliSslProvider>();
services.AddTransient<TencentSslProvider>();
return services.BuildServiceProvider();
}
@@ -64,20 +95,41 @@ namespace Hua.DDNS.Start
q.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 10; });
//configure jobs with code
var appJobKey = new JobKey("AppJob", "AppJobGroup");
q.AddJob<AppJob>(j => j
var appJobKey = new JobKey("NewJob", "NewJobGroup");
q.AddJob<NewJob>(j => j
.StoreDurably()
.WithIdentity(appJobKey)
.WithDescription("AppJob")
.WithDescription("NewJob")
);
q.AddTrigger(t => t
.WithIdentity("AppJob Trigger")
.WithIdentity("NewJob Trigger")
.ForJob(appJobKey)
.WithCronSchedule(hostContext.Configuration.GetSection("App:AppJob:Corn").Value)
.WithDescription("AppJob trigger")
.WithDescription("NewJob trigger")
.StartNow()
);
var sslDownloadJobKey = new JobKey("SslDownloadJob", "SslDownloadJobGroup");
q.AddJob<SslDownloadJob>(j => j
.StoreDurably()
.WithIdentity(sslDownloadJobKey)
.WithDescription("SslDownloadJob")
);
var sslCorn = hostContext.Configuration.GetSection("SslDownload:Corn").Value;
var sslEnabled = hostContext.Configuration.GetSection("SslDownload:Enabled").Get<bool>();
if (sslEnabled && !string.IsNullOrEmpty(sslCorn))
{
q.AddTrigger(t => t
.WithIdentity("SslDownloadJob Trigger")
.ForJob(sslDownloadJobKey)
.WithCronSchedule(sslCorn)
.WithDescription("SslDownloadJob trigger")
.StartNow()
);
}
});
// Quartz.Extensions.Hosting hosting

View File

@@ -11,22 +11,30 @@
"App": {
"AppJob": {
"Corn": "* * * * * ?" //https://cron.qqe2.com/
},
"Domain": {
"Platform": "Ali",
// Access Id/Secret Id
"Id": "Id",
// Access Key/Secret Key
"Key": "Key",
// 主域名
"domain": "demo.cn",
// 子域名前缀
"subDomainArray": [ "bjb", "git"],
// 记录类型
"type": "A",
//间隔时间 秒
"time": "30"
}
},
"DDNS": {
"Platform": 3, //1 Ali 2 Tencent 3 Namesilo
// 主域名
"Domain": "we965.com",
// 子域名前缀
"SubDomainArray": [ "git", "webutil", "dev" ],
// 记录类型
"type": "A",
//间隔时间 秒
"time": "30"
},
"Namesilo": {
"ApiKey": "1111"
},
"Dnspod": {
"Id": "1111",
"Key": "1111",
"Endpoint": "1111"
},
"Ali": {
"Id": "1111",
"Key": "1111",
"Endpoint": "1111"
}
}

View File

@@ -8,25 +8,59 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"App": {
"AppJob": {
"Corn": "* * * * * ?" //https://cron.qqe2.com/
"App": {
"GetIpv4Url": "http://47.108.74.59:8001/api/NetWork/GetIp",
"AppJob": {
"Corn": "0/5 * * * * ?" //https://cron.qqe2.com/
}
},
"Domain": {
"Platform": "Ali",
// Access Id/Secret Id
"Id": "Id",
// Access Key/Secret Key
"Key": "Key",
// 主域名
"domain": "demo.cn",
// 子域名前缀
"subDomainArray": [ "bjb", "git" ],
// 记录类型
"type": "A",
//间隔时间 秒
"time": "30"
"DDNS": {
"Platform": 2, //1 Ali 2 Tencent 3 Namesilo
// 主域名
"Domain": "we965.cn",
// 子域名前缀
"SubDomainArray": [ "git", "webutil", "dev" ],
// 记录类型
"type": "A",
//间隔时间 秒
"time": "30"
},
"SslDownload": {
"Enabled": true,
"Corn": "0/5 * * * * ?",
"Platform": 2,
"SavePath": "D:\\Paths\\ssl",
"ExpireDays": 10000,
"DownloadItems": [
{
"Domain": "git.we965.cn",
"FileName": "git.we965.cn.pem"
},
{
"Domain": "webutil.we965.cn",
"FileName": "webutil.we965.cn.pem"
},
{
"Domain": "dev.we965.cn",
"FileName": "dev.we965.cn.pem"
}
]
},
"TencentCloud": {
"SecretId": "1111",
"SecretKey": "11111",
"Region": "ap-guangzhou",
"Dnspod": {
"Endpoint": "dnspod.tencentcloudapi.com"
}
},
"AliCloud": {
"AccessKeyId": "1111",
"AccessKeySecret": "1111",
"RegionId": "cn-hangzhou",
"Endpoint": "1111"
},
"Namesilo": {
"ApiKey": "1111"
}
}

216
README.md
View File

@@ -1,23 +1,71 @@
## Purpose
# Hua.DDNS
A system service for dynamic update DNS record by `net6` with `Quartz.Net`. only Support [DnsPod](https://docs.dnspod.cn/api/add-domain/)、[AlibabaCloud]([阿里云 OpenAPI 开发者门户 (aliyun.com)](https://next.api.aliyun.com/document/Alidns/2015-01-09)).
A dynamic DNS update system service built with .NET 8.0 and Quartz.Net.
## Deploy
## Features
1. Copy the folder `\bin\Debug\net6.0` to a new path and open it .
2. Configure the `App` option in `appsetting.json` file.
3. In Windows system, configure the service name in `InstallServiceByNssm.bat` file , and then double click the BAT file.
- **Multi-Platform Support**: Supports multiple DNS providers
- [DnsPod](https://docs.dnspod.cn/api/add-domain/) (Tencent Cloud)
- [Alibaba Cloud DNS](https://next.api.aliyun.com/document/Alidns/2015-01-09)
- [Namesilo](https://www.namesilo.com/api-reference)
- **SSL Certificate Download**: Automatic SSL certificate download from Alibaba Cloud and Tencent Cloud
- **Scheduled Tasks**: Flexible task scheduling using Quartz.Net with Cron expressions
- **Windows Service**: Can run as a Windows service using NSSM
- **Docker Support**: Ready for containerized deployment
- **Logging**: Comprehensive logging with Serilog
- **Database Support**: PostgreSQL for data persistence
## Building
Check and configure the `App` option in `appsetting.json` file, and then click the `Hua.DDNS.sln` file open the solution.
## Requirements
- .NET 8.0 SDK (for building)
- .NET 8.0 Runtime (for running)
- PostgreSQL (optional, for database features)
- Windows (for Windows Service mode) or Linux/Docker
## Installation
### Windows Service Deployment
1. Build the project:
```bash
dotnet publish -c Release -o ./publish
```
2. Copy the `publish` folder to your desired location
3. Configure the `appsettings.json` file (see Configuration section below)
4. Edit `InstallServiceByNssm.bat` to set your preferred service name and path
5. Run `InstallServiceByNssm.bat` as administrator to install the service
6. The service will start automatically
### Docker Deployment
1. Build the Docker image:
```bash
docker build -t hua.ddns .
```
2. Run the container:
```bash
docker run -d \
-v /path/to/appsettings.json:/app/appsettings.json \
-v /path/to/logs:/app/Log \
hua.ddns
```
## Configuration
Example of config in `appsetting.json`
### Basic Configuration
Configure the `appsettings.json` file with your settings:
```json
{
"ConnectionStrings": {
"pgConnection": "Host=127.0.0.1;Port=5432;Database=Worker;Username=Worker;Password=123456;"//LogDbConnection
"pgConnection": "Host=127.0.0.1;Port=5432;Database=Worker;Username=Worker;Password=123456;"
},
"Logging": {
"LogLevel": {
@@ -26,18 +74,140 @@ Example of config in `appsetting.json`
}
},
"App": {
"GetIpv4Url": "http://47.108.74.59:8001/api/NetWork/GetIp",
"AppJob": {
"Corn": "0/15 * * * * ? " //a corn expression which defined strike time and frequency.this is a util website for generate an corn expression https://cron.qqe2.com/
},
"Domain": {
"Platform": "Ali", //platform from 'Tencent' or 'Ali'
"Id": "Id",//get the id and key from https://dc.console.aliyun.com/ Or https://console.cloud.tencent.com/cam/capi
"Key": "Key",
"domain": "demo.cn",
"subDomainArray": [ "www", "@","git"],
"type": "A",//this is not using
"time": "30"//this is not using
"Corn": "0/5 * * * * ?"
}
}
},
"DDNS": {
"Platform": 2,
"Domain": "example.com",
"SubDomainArray": [ "www", "api", "dev" ],
"type": "A",
"time": "30"
},
"SslDownload": {
"Enabled": true,
"Corn": "0 0 2 * * ?",
"Platform": 2,
"SavePath": "D:\\Paths\\ssl",
"ExpireDays": 10000,
"DownloadItems": [
{
"Domain": "www.example.com",
"FileName": "www.example.com.pem"
}
]
},
"TencentCloud": {
"SecretId": "your-secret-id",
"SecretKey": "your-secret-key",
"Region": "ap-guangzhou",
"Dnspod": {
"Endpoint": "dnspod.tencentcloudapi.com"
}
},
"AliCloud": {
"AccessKeyId": "your-access-key-id",
"AccessKeySecret": "your-access-key-secret",
"RegionId": "cn-hangzhou",
"Endpoint": "alidns.cn-hangzhou.aliyuncs.com"
},
"Namesilo": {
"ApiKey": "your-api-key"
}
}
```
### Configuration Details
#### DDNS Configuration
- **Platform**: DNS provider selection
- `1`: Alibaba Cloud
- `2`: Tencent Cloud (DnsPod)
- `3`: Namesilo
- **Domain**: Your main domain name
- **SubDomainArray**: List of subdomains to update
- **type**: DNS record type (A, AAAA, etc.)
- **time**: Update interval in seconds
#### SSL Download Configuration
- **Enabled**: Enable/disable SSL certificate download
- **Corn**: Cron expression for download schedule
- **Platform**: SSL provider (same as DDNS Platform)
- **SavePath**: Directory to save SSL certificates
- **ExpireDays**: Days before certificate expiration to trigger download
- **DownloadItems**: List of domains and filenames for certificates
#### Cron Expressions
Use [Cron Expression Generator](https://cron.qqe2.com/) to create custom schedules.
Examples:
- `0/5 * * * * ?` - Every 5 seconds
- `0 0 2 * * ?` - Every day at 2:00 AM
- `0 0 * * * ?` - Every hour
## Building
### Prerequisites
- .NET 8.0 SDK
### Build Steps
1. Clone the repository:
```bash
git clone <repository-url>
cd Hua.DDNS
```
2. Restore dependencies:
```bash
dotnet restore
```
3. Build the solution:
```bash
dotnet build -c Release
```
4. Run the application:
```bash
dotnet run --project Hua.DDNS/Hua.DDNS.csproj
```
## Project Structure
```
Hua.DDNS/
├── Common/ # Common utilities and helpers
│ ├── Config/ # Configuration classes and options
│ ├── Http/ # HTTP helper classes
│ ├── FileHelper.cs
│ └── SqlHelper.cs
├── DDNSProviders/ # DNS provider implementations
│ ├── Ali/ # Alibaba Cloud DNS provider
│ ├── Dnspod/ # Tencent Cloud DNS provider
│ ├── Namesilo/ # Namesilo DNS provider
│ └── ...
├── Jobs/ # Scheduled jobs
│ ├── AppJob.cs
│ ├── NewJob.cs
│ └── SslDownloadJob.cs
├── Models/ # Data models
├── SslProviders/ # SSL certificate providers
│ ├── Ali/ # Alibaba Cloud SSL provider
│ └── Tencent/ # Tencent Cloud SSL provider
└── Start/ # Application entry point
```
## License
This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details.
## Support
For issues and questions, please open an issue on the project repository.

3
buildImages.ps1 Normal file
View File

@@ -0,0 +1,3 @@
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
cd ./Hua.DDNS
docker build -t hua.ddns:latest -f Dockerfile ..