Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84c0199297 | |||
| 773c230e3d | |||
| 10f156e9e2 | |||
| 054ca35e71 | |||
| c78e7a248c | |||
| c4e9811004 | |||
| 6d66b6532b | |||
| 08e7bf004b | |||
| 801061f4f2 | |||
| ee85ccc8b9 | |||
| f027198279 | |||
| fe43eeb6c2 | |||
| 7c3f039911 | |||
| f8df2fe468 | |||
| 5ddf355799 |
@@ -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
|
||||
@@ -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<AppJob>();
|
||||
var job = sc.GetService<DdnsJob>();
|
||||
|
||||
job?.Execute(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.False(false, $"请求异常:{e.Message}");
|
||||
Assert.False(false, $"�����쳣:{e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net10.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>
|
||||
|
||||
@@ -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<DdnsJob>();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"pgConnection": "Host=127.0.0.1;Port=5432;Database=Worker;Username=Worker;Password=123456;"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"App": {
|
||||
"GetIpv4Url": "http://47.108.74.59:8001/api/NetWork/GetIp",
|
||||
"AppJob": {
|
||||
"Corn": "0/5 * * * * ?" //https://cron.qqe2.com/
|
||||
}
|
||||
},
|
||||
"DDNS": {
|
||||
"Platform": 2, //1 Ali 2 Tencent 3 Namesilo
|
||||
// 主域名
|
||||
"Domain": "we965.cn",
|
||||
// 子域名前缀
|
||||
"SubDomainArray": [ "mp", "git", "webutil", "dev", "sftp" ],
|
||||
// 记录类型
|
||||
"type": "A",
|
||||
//间隔时间 秒
|
||||
"time": "30"
|
||||
},
|
||||
"SslDownload": {
|
||||
"Enabled": true,
|
||||
"Corn": "0/5 * * * * ?", //https://cron.qqe2.com/
|
||||
"Platform": 2,
|
||||
"SavePath": "D:\\Paths\\ssl",
|
||||
"ExpireDays": 5,
|
||||
"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": "AKIDy35008NYm6T1v3R3gGtU1UIHOe0NizON",
|
||||
"SecretKey": "1sXQmASfmmlwAXuDh8fVYUOLI7mJagbQ",
|
||||
"Region": "ap-guangzhou",
|
||||
"Dnspod": {
|
||||
"Endpoint": "dnspod.tencentcloudapi.com"
|
||||
}
|
||||
},
|
||||
"AliCloud": {
|
||||
"AccessKeyId": "1111",
|
||||
"AccessKeySecret": "1111",
|
||||
"RegionId": "cn-hangzhou",
|
||||
"Endpoint": "1111"
|
||||
},
|
||||
"Namesilo": {
|
||||
"ApiKey": "1111"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32616.157
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hua.DDNS", "Hua.DDNS\Hua.DDNS.csproj", "{EBC77B5D-87D5-4923-84A6-93DB2248DEB0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hua.DDNS.Test", "Hua.DDNS.Test\Hua.DDNS.Test.csproj", "{BB544060-5ABF-4A3C-965B-BE7CA7BD61E7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{EBC77B5D-87D5-4923-84A6-93DB2248DEB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EBC77B5D-87D5-4923-84A6-93DB2248DEB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EBC77B5D-87D5-4923-84A6-93DB2248DEB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EBC77B5D-87D5-4923-84A6-93DB2248DEB0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BB544060-5ABF-4A3C-965B-BE7CA7BD61E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BB544060-5ABF-4A3C-965B-BE7CA7BD61E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BB544060-5ABF-4A3C-965B-BE7CA7BD61E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BB544060-5ABF-4A3C-965B-BE7CA7BD61E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D8744CA6-C2DB-4287-B8B3-E5917B12391D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="Hua.DDNS.Test/Hua.DDNS.Test.csproj" />
|
||||
<Project Path="Hua.DDNS/Hua.DDNS.csproj" />
|
||||
</Solution>
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Hua.DDNS.Common.Config.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// 阿里云配置选项
|
||||
/// </summary>
|
||||
public class AliCloudOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 访问密钥 ID
|
||||
/// </summary>
|
||||
public string AccessKeyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问密钥私钥
|
||||
/// </summary>
|
||||
public string AccessKeySecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域 ID (如 cn-hangzhou)
|
||||
/// </summary>
|
||||
public string RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问端点 (可选)
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
namespace Hua.DDNS.Common.Config.Options
|
||||
using Hua.DDNS.DDNSProviders;
|
||||
|
||||
namespace Hua.DDNS.Common.Config.Options
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序全局配置类
|
||||
/// </summary>
|
||||
public class AppOption
|
||||
{
|
||||
public DomainOption Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DDNS 相关配置
|
||||
/// </summary>
|
||||
public DdnsOption DDNS { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 平台类型枚举
|
||||
/// </summary>
|
||||
public enum PlatformEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 阿里云
|
||||
/// </summary>
|
||||
Ali = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云
|
||||
/// </summary>
|
||||
Tencent,
|
||||
|
||||
/// <summary>
|
||||
/// Namesilo
|
||||
/// </summary>
|
||||
Namesilo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace Hua.DDNS.Common.Config.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// SSL 证书来源平台枚举
|
||||
/// </summary>
|
||||
public enum SslPlatformEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 阿里云
|
||||
/// </summary>
|
||||
Ali = 1,
|
||||
/// <summary>
|
||||
/// 腾讯云
|
||||
/// </summary>
|
||||
Tencent = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSL 证书下载配置选项
|
||||
/// </summary>
|
||||
public class SslDownloadOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cron 定时表达式
|
||||
/// </summary>
|
||||
public string Corn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 云平台类型
|
||||
/// </summary>
|
||||
public SslPlatformEnum Platform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书保存本地路径
|
||||
/// </summary>
|
||||
public string SavePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期天数阈值 (提前多少天开始下载更新)
|
||||
/// </summary>
|
||||
public int ExpireDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 需要下载的证书列表
|
||||
/// </summary>
|
||||
public List<SslDownloadItem> DownloadItems { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSL 证书下载项详情
|
||||
/// </summary>
|
||||
public class SslDownloadItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 保存到本地的文件名 (不含后缀)
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Hua.DDNS.DDNSProviders.Dnspod;
|
||||
|
||||
namespace Hua.DDNS.Common.Config.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯云配置选项
|
||||
/// </summary>
|
||||
public class TencentCloudOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 密钥 ID
|
||||
/// </summary>
|
||||
public string SecretId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥私钥
|
||||
/// </summary>
|
||||
public string SecretKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域 (如 ap-guangzhou)
|
||||
/// </summary>
|
||||
public string Region { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dnspod 相关配置 (已并入腾讯云)
|
||||
/// </summary>
|
||||
public DnspodOption Dnspod { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,30 @@
|
||||
using Hua.DDNS.Common.Config.Options;
|
||||
using Hua.DDNS.Common.Config.Options;
|
||||
|
||||
namespace Hua.DDNS.Common.Config
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 强类型配置提供者,用于方便地访问应用程序配置
|
||||
/// </summary>
|
||||
public class SettingProvider
|
||||
{
|
||||
private readonly AppOption _app;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="configuration">配置对象</param>
|
||||
public SettingProvider(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_app = new AppOption();
|
||||
configuration.GetSection("App").Bind(_app);
|
||||
_configuration.GetSection("App").Bind(_app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取应用程序配置选项
|
||||
/// </summary>
|
||||
public AppOption App => _app;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
namespace Hua.DDNS.Common
|
||||
namespace Hua.DDNS.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件操作助手类
|
||||
/// </summary>
|
||||
public class FileHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 如果文件存在则删除
|
||||
/// </summary>
|
||||
/// <param name="path">文件路径</param>
|
||||
public static void DeleteIfExists(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Hua.DDNS.Common.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Http 请求助手类,用于执行各种 HTTP 请求
|
||||
/// </summary>
|
||||
public class HttpHelper: IHttpHelper
|
||||
{
|
||||
private static ILogger<HttpHelper> _logger;
|
||||
private static HttpClientHandler _handler;
|
||||
private IConfiguration _configuration;
|
||||
|
||||
public HttpHelper(ILogger<HttpHelper> logger)
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="logger">日志对象</param>
|
||||
/// <param name="configuration">配置对象</param>
|
||||
public HttpHelper(ILogger<HttpHelper> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_handler = new HttpClientHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 HttpClient 实例
|
||||
/// </summary>
|
||||
/// <returns>HttpClient 实例</returns>
|
||||
public HttpClient GetHttpClient()
|
||||
{
|
||||
return new HttpClient(_handler){};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostAsync
|
||||
/// 异步执行 Post 请求并解析 JSON 结果
|
||||
/// </summary>
|
||||
/// <typeparam name="TIn"></typeparam>
|
||||
/// <typeparam name="TOut"></typeparam>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="timeOut">超时时间</param>
|
||||
/// <returns></returns>
|
||||
/// <typeparam name="TIn">输入数据类型</typeparam>
|
||||
/// <typeparam name="TOut">输出结果类型</typeparam>
|
||||
/// <param name="url">请求地址</param>
|
||||
/// <param name="input">输入数据</param>
|
||||
/// <param name="timeOut">超时时间 (秒)</param>
|
||||
/// <returns>解析后的结果对象</returns>
|
||||
public async Task<TOut?> PostAsync<TIn, TOut>(string url, TIn input, int timeOut = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetHttpClient();
|
||||
client.Timeout = new TimeSpan(0, 10, timeOut);
|
||||
client.Timeout = TimeSpan.FromSeconds(timeOut);
|
||||
_logger.LogDebug($"Post:{url}\n[{JsonConvert.SerializeObject(input)}]");
|
||||
var result = await client.PostAsync(url, JsonContent.Create(input));
|
||||
var strResult = await result.Content.ReadAsStringAsync();
|
||||
@@ -53,18 +67,18 @@ namespace Hua.DDNS.Common.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostAsync
|
||||
/// 异步执行 Get 请求并解析 JSON 结果
|
||||
/// </summary>
|
||||
/// <typeparam name="TOut"></typeparam>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="timeOut"></param>
|
||||
/// <returns></returns>
|
||||
/// <typeparam name="TOut">输出结果类型</typeparam>
|
||||
/// <param name="url">请求地址</param>
|
||||
/// <param name="timeOut">超时时间 (秒)</param>
|
||||
/// <returns>解析后的结果对象</returns>
|
||||
public async Task<TOut?> GetAsync<TOut>(string url,int timeOut = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetHttpClient();
|
||||
client.Timeout = new TimeSpan(0, 10, timeOut);
|
||||
client.Timeout = TimeSpan.FromSeconds(timeOut);
|
||||
_logger.LogDebug($"Get:{url}");
|
||||
var result = await client.GetAsync(url);
|
||||
var strResult = await result.Content.ReadAsStringAsync();
|
||||
@@ -85,31 +99,45 @@ namespace Hua.DDNS.Common.Http
|
||||
#region 下载文件
|
||||
|
||||
/// <summary>
|
||||
/// http下载文件 (仅支持小文件)
|
||||
/// 通过 HTTP 下载文件
|
||||
/// </summary>
|
||||
/// <param name="url">下载文件地址</param>
|
||||
/// <param name="localPath">文件存放地址,包含文件名</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="localPath">文件本地存放路径,包含文件名</param>
|
||||
/// <returns>下载成功返回 true,否则返回 false</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,24 +146,62 @@ namespace Hua.DDNS.Common.Http
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获得当前机器的公网 IP
|
||||
/// 获取当前机器的公网 IPv4 地址
|
||||
/// </summary>
|
||||
/// <returns>IPv4 地址字符串</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Http 助手接口
|
||||
/// </summary>
|
||||
public interface IHttpHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前机器的公网 IPv4 地址
|
||||
/// </summary>
|
||||
/// <returns>IPv4 地址字符串</returns>
|
||||
public Task<string> GetCurrentPublicIpv4();
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行 Post 请求并解析 JSON 结果
|
||||
/// </summary>
|
||||
/// <typeparam name="TIn">输入数据类型</typeparam>
|
||||
/// <typeparam name="TOut">输出结果类型</typeparam>
|
||||
/// <param name="url">请求地址</param>
|
||||
/// <param name="input">输入数据</param>
|
||||
/// <param name="timeOut">超时时间 (秒)</param>
|
||||
/// <returns>解析后的结果对象</returns>
|
||||
public Task<TOut?> PostAsync<TIn, TOut>(string url, TIn input, int timeOut = 10);
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行 Get 请求并解析 JSON 结果
|
||||
/// </summary>
|
||||
/// <typeparam name="TOut">输出结果类型</typeparam>
|
||||
/// <param name="url">请求地址</param>
|
||||
/// <param name="timeOut">超时时间 (秒)</param>
|
||||
/// <returns>解析后的结果对象</returns>
|
||||
public Task<TOut?> GetAsync<TOut>(string url, int timeOut = 10);
|
||||
|
||||
/// <summary>
|
||||
/// 通过 HTTP 下载文件
|
||||
/// </summary>
|
||||
/// <param name="url">下载文件地址</param>
|
||||
/// <param name="fileFullName">文件本地存放路径,包含文件名</param>
|
||||
/// <returns>下载成功返回 true,否则返回 false</returns>
|
||||
public bool DownloadFile(string url, string fileFullName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 HttpClient 实例
|
||||
/// </summary>
|
||||
/// <returns>HttpClient 实例</returns>
|
||||
public HttpClient GetHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
namespace Hua.DDNS.Common.Http
|
||||
namespace Hua.DDNS.Common.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Http 请求返回结果包装类
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public class HttpResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回的数据内容
|
||||
/// </summary>
|
||||
public virtual T Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据描述信息
|
||||
/// </summary>
|
||||
public string DataDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结果状态码 (例如 200 为成功)
|
||||
/// </summary>
|
||||
public int Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回的消息提示
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
using System.Data;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
|
||||
namespace Hua.DDNS.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库操作助手类 (基于 Npgsql 和 Dapper)
|
||||
/// </summary>
|
||||
public class SqlHelper
|
||||
{
|
||||
|
||||
private readonly string _connectionString;
|
||||
private readonly ILogger<SqlHelper> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="configuration">配置对象</param>
|
||||
/// <param name="logger">日志对象</param>
|
||||
public SqlHelper(IConfiguration configuration, ILogger<SqlHelper> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -19,10 +27,10 @@ namespace Hua.DDNS.Common
|
||||
/// <summary>
|
||||
/// 查询所有结果
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="strSql"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
/// <typeparam name="T">实体类型</typeparam>
|
||||
/// <param name="strSql">SQL 语句</param>
|
||||
/// <returns>结果列表</returns>
|
||||
/// <exception cref="Exception">数据库连接或查询异常</exception>
|
||||
public List<T> GetList<T>(string strSql)
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Hua.DDNS.DDNSProviders.Ali
|
||||
{
|
||||
/// <summary>
|
||||
/// domain configuration Ali
|
||||
/// </summary>
|
||||
public class AliDdnsOption
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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>
|
||||
/// 阿里云 DDNS 解析提供者
|
||||
/// </summary>
|
||||
public class AliDdnsProvider : IDdnsProvider
|
||||
{
|
||||
private readonly Client _client;
|
||||
private readonly AliCloudOption _aliCloudOption;
|
||||
private readonly DdnsOption _ddnsOption;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="aliCloudOption">阿里云配置</param>
|
||||
/// <param name="mapper">对象映射器</param>
|
||||
/// <param name="ddnsOption">DDNS 配置</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步在阿里云上创建新的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="record">解析记录信息</param>
|
||||
/// <returns>创建后的解析记录信息</returns>
|
||||
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord record)
|
||||
{
|
||||
var rep = await _client.AddDomainRecordAsync(_mapper.Map<AddDomainRecordRequest>(record));
|
||||
return record;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步批量修改阿里云上的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="newIp">新的 IP 地址</param>
|
||||
/// <param name="records">需要修改的解析记录列表</param>
|
||||
/// <returns>修改后的解析记录列表</returns>
|
||||
public async Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records)
|
||||
{
|
||||
foreach (var aliDomainRecord in records)
|
||||
{
|
||||
aliDomainRecord.Ip = newIp;
|
||||
await _client.UpdateDomainRecordAsync(_mapper.Map<UpdateDomainRecordRequest>(aliDomainRecord));
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CleanInvalidCertificatesAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// DDNS 配置选项类
|
||||
/// </summary>
|
||||
public class DdnsOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 云平台类型 (1: 阿里云, 2: 腾讯云, 3: Namesilo)
|
||||
/// </summary>
|
||||
public PlatformEnum Platform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 根域名 (例如: example.com)
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 需要更新的子域名列表 (例如: ["www", "api", "@"])
|
||||
/// </summary>
|
||||
public string[] SubDomainArray { get; set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
/// Dnspod (腾讯云) DDNS 解析提供者
|
||||
/// </summary>
|
||||
public class DnspodDdnsProvider : IDdnsProvider
|
||||
{
|
||||
|
||||
private readonly DnspodClient _client;
|
||||
private readonly TencentCloudOption _tencentCloudOption;
|
||||
private readonly DdnsOption _ddnsOption;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="mapper">对象映射器</param>
|
||||
/// <param name="tencentCloudOption">腾讯云配置</param>
|
||||
/// <param name="ddnsOption">DDNS 配置</param>
|
||||
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 }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 Dnspod 上的域名解析记录列表
|
||||
/// </summary>
|
||||
/// <returns>解析记录列表</returns>
|
||||
public async Task<IEnumerable<DnsRecord>?> GetRecordListAsync()
|
||||
{
|
||||
var recordList = (await _client.DescribeRecordList(new DescribeRecordListRequest() { Domain = _ddnsOption.Domain })).RecordList;
|
||||
|
||||
return _mapper.Map<IEnumerable<DnsRecord>>(recordList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步在 Dnspod 上创建新的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="record">解析记录信息</param>
|
||||
/// <returns>创建后的解析记录信息</returns>
|
||||
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord record)
|
||||
{
|
||||
var response = await _client.CreateRecord(new CreateRecordRequest()
|
||||
{
|
||||
Domain = _ddnsOption.Domain,
|
||||
SubDomain = record.SubDomain,
|
||||
Value = record.Ip,
|
||||
RecordLine = "默认",
|
||||
RecordType = record.RecordType,
|
||||
TTL = Convert.ToUInt32(record.TTL),
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步批量修改 Dnspod 上的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="newIp">新的 IP 地址</param>
|
||||
/// <param name="records">需要修改的解析记录列表</param>
|
||||
/// <returns>修改后的解析记录列表</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CleanInvalidCertificatesAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Hua.DDNS.DDNSProviders.Dnspod
|
||||
{
|
||||
/// <summary>
|
||||
/// Dnspod 配置选项
|
||||
/// </summary>
|
||||
public class DnspodOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 接口访问端点 (例如: dnspod.tencentcloudapi.com)
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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>
|
||||
/// 动态域名解析 (DDNS) 提供者接口
|
||||
/// </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>
|
||||
/// 异步批量修改域名解析记录 (通常用于更新 IP)
|
||||
/// </summary>
|
||||
/// <param name="newIp">新的 IP 地址</param>
|
||||
/// <param name="records">需要修改的解析记录列表</param>
|
||||
/// <returns>修改后的解析记录列表</returns>
|
||||
Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records);
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CleanInvalidCertificatesAsync();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
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>
|
||||
/// Namesilo DDNS 解析提供者
|
||||
/// </summary>
|
||||
public class NamesiloDdnsProvider : IDdnsProvider
|
||||
{
|
||||
public readonly NamesiloOption _namesiloOption;
|
||||
public readonly DdnsOption _ddnsOption;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="namesiloOption">Namesilo 配置</param>
|
||||
/// <param name="ddnsOption">DDNS 配置</param>
|
||||
public NamesiloDdnsProvider(IOptions<NamesiloOption> namesiloOption, IOptions<DdnsOption> ddnsOption)
|
||||
{
|
||||
_ddnsOption = ddnsOption.Value;
|
||||
_namesiloOption = namesiloOption.Value;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 Namesilo 上的域名解析记录列表
|
||||
/// </summary>
|
||||
/// <returns>解析记录列表</returns>
|
||||
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 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 = host,
|
||||
Domain = _ddnsOption.Domain,
|
||||
TTL = record.ParentNode.SelectSingleNode("ttl/text()").Value,
|
||||
SubDomain = subDomain,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步在 Namesilo 上创建新的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="dnsRecord">解析记录信息</param>
|
||||
/// <returns>创建后的解析记录信息</returns>
|
||||
public async Task<DnsRecord> CreateDnsRecordAsync(DnsRecord dnsRecord)
|
||||
{
|
||||
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/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();
|
||||
{
|
||||
|
||||
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)}'");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.Value != "300")
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Failed to create record: '{JsonConvert.SerializeObject(dnsRecord)}', Status: {status.Value}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return dnsRecord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步批量修改 Namesilo 上的域名解析记录
|
||||
/// </summary>
|
||||
/// <param name="newIp">新的 IP 地址</param>
|
||||
/// <param name="records">需要修改的解析记录列表</param>
|
||||
/// <returns>修改后的解析记录列表</returns>
|
||||
public async Task<IEnumerable<DnsRecord>> ModifyRecordListAsync(string newIp, IEnumerable<DnsRecord> records)
|
||||
{
|
||||
foreach (var dnsRecord in records)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
{
|
||||
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);
|
||||
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 || status.Value != "300")
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Failed to update record: '{dnsRecord.Id}' with Ip: '{newIp}'. Status: {status?.Value}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CleanInvalidCertificatesAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Hua.DDNS.DDNSProviders.Namesilo
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Namesilo 配置选项
|
||||
/// </summary>
|
||||
public class NamesiloOption
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// API 密钥
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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
|
||||
|
||||
+39
-37
@@ -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>net10.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>
|
||||
|
||||
@@ -1,136 +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;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
|
||||
public AppJob(ILogger<AppJob> logger,SettingProvider settingProvider, IHttpHelper httpHelper)
|
||||
{
|
||||
_logger = logger;
|
||||
_settingProvider = settingProvider;
|
||||
_httpHelper = httpHelper;
|
||||
_domainOption = _settingProvider.App.Domain;
|
||||
|
||||
|
||||
}
|
||||
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();
|
||||
|
||||
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(
|
||||
// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
|
||||
// 密钥可前往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") }
|
||||
});
|
||||
|
||||
//获取域名解析记录
|
||||
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;//如果记录已经变更,不调用更新接口
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
var aliDescribeRecordList = (await aliClient.DescribeDomainRecordsAsync(new DescribeDomainRecordsRequest()
|
||||
{
|
||||
DomainName = _domainOption.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;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogInformation("AppJob已销毁");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Hua.DDNS.Jobs
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Job上下文
|
||||
/// </summary>
|
||||
public class AppJobContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// DDNS 任务类,用于定期检查并更新域名解析记录
|
||||
/// </summary>
|
||||
[DisallowConcurrentExecution]
|
||||
public class DdnsJob : IJob, IDisposable
|
||||
{
|
||||
private readonly ILogger<DdnsJob> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly DdnsOption _ddnsOption;
|
||||
private readonly IHttpHelper _httpHelper;
|
||||
|
||||
/// <summary>
|
||||
/// 新的 IP 地址
|
||||
/// </summary>
|
||||
public string newIp;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="logger">日志对象</param>
|
||||
/// <param name="httpHelper">Http 助手</param>
|
||||
/// <param name="ddnsOption">DDNS 配置选项</param>
|
||||
/// <param name="serviceProvider">服务提供者</param>
|
||||
public DdnsJob(ILogger<DdnsJob> logger,IHttpHelper httpHelper,IOptions<DdnsOption> ddnsOption, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpHelper = httpHelper;
|
||||
_serviceProvider = serviceProvider;
|
||||
_ddnsOption = ddnsOption.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行任务逻辑
|
||||
/// </summary>
|
||||
/// <param name="context">作业执行上下文</param>
|
||||
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();
|
||||
|
||||
if (_ddnsOption.SubDomainArray == null || _ddnsOption.SubDomainArray.Length == 0)
|
||||
{
|
||||
_logger.LogWarning("未配置 SubDomainArray,跳过执行。");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 获取所有 DNS 记录
|
||||
var dnsRecordList = (await ddnsProvider!.GetRecordListAsync())?.ToList() ?? new List<DnsRecord>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 3. 找出需要新增的记录(在配置中但不在 DNS 记录中)
|
||||
var existingSubDomains = dnsRecordList.Select(m => m.SubDomain).ToHashSet();
|
||||
var subDomainsToCreate = _ddnsOption.SubDomainArray
|
||||
.Where(s => !existingSubDomains.Contains(s))
|
||||
.ToList();
|
||||
|
||||
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)
|
||||
{
|
||||
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("任务执行完成");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogInformation("DdnsJob已销毁");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
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;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Hua.DDNS.DDNSProviders;
|
||||
|
||||
namespace Hua.DDNS.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// SSL 证书管理任务类,用于定期检查、申请并更新 SSL 证书
|
||||
/// </summary>
|
||||
[DisallowConcurrentExecution]
|
||||
public class SslDownloadJob : IJob, IDisposable
|
||||
{
|
||||
private readonly ILogger<SslDownloadJob> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly SslDownloadOption _sslDownloadOption;
|
||||
private readonly DdnsOption _ddnsOption;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <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<DdnsOption> ddnsOption)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_sslDownloadOption = sslDownloadOption.Value;
|
||||
_ddnsOption = ddnsOption.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行任务逻辑
|
||||
/// </summary>
|
||||
/// <param name="context">作业执行上下文</param>
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
if (!_sslDownloadOption.Enabled)
|
||||
{
|
||||
_logger.LogInformation("SSL管理任务已禁用,跳过执行");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("开始SSL文件管理任务");
|
||||
|
||||
try
|
||||
{
|
||||
ISslManagementProvider? sslProvider = _sslDownloadOption.Platform switch
|
||||
{
|
||||
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}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(_sslDownloadOption.SavePath))
|
||||
{
|
||||
Directory.CreateDirectory(_sslDownloadOption.SavePath);
|
||||
_logger.LogInformation($"创建目录: {_sslDownloadOption.SavePath}");
|
||||
}
|
||||
|
||||
var certificates = await sslProvider.GetCertificatesAsync();
|
||||
_logger.LogInformation($"获取到 {certificates.Count} 个{_sslDownloadOption.Platform} SSL 证书");
|
||||
var downloadTasks = new List<Task>();
|
||||
|
||||
// 合并 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} 的证书,尝试自动申请");
|
||||
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 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);
|
||||
|
||||
if (localCertExpiry == null)
|
||||
{
|
||||
_logger.LogInformation($"本地证书文件不存在,开始下载: {item.Domain}");
|
||||
downloadTasks.Add(DownloadFileAsync(sslProvider, item, certificate.CertificateId, certificate.Domain));
|
||||
}
|
||||
else
|
||||
{
|
||||
var localDaysUntilExpiry = (localCertExpiry.Value - DateTime.Now).Days;
|
||||
|
||||
if (localDaysUntilExpiry <= _sslDownloadOption.ExpireDays)
|
||||
{
|
||||
_logger.LogInformation($"本地证书 {item.Domain} 将在 {localDaysUntilExpiry} 天后过期,开始更新");
|
||||
downloadTasks.Add(DownloadFileAsync(sslProvider, item, certificate.CertificateId, certificate.Domain));
|
||||
}
|
||||
else if (certificate.CertEndTime > localCertExpiry.Value)
|
||||
{
|
||||
_logger.LogInformation($"云端证书 {item.Domain} 过期时间更晚,开始更新 (本地: {localCertExpiry:yyyy-MM-dd}, 云端: {certificate.CertEndTime:yyyy-MM-dd})");
|
||||
downloadTasks.Add(DownloadFileAsync(sslProvider, item, certificate.CertificateId, certificate.Domain));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"本地证书 {item.Domain} 还有 {localDaysUntilExpiry} 天过期,跳过下载");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(downloadTasks);
|
||||
|
||||
_logger.LogInformation($"SSL文件管理任务完成,共下载 {downloadTasks.Count} 个文件");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_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>
|
||||
/// <param name="item">下载项</param>
|
||||
/// <returns>过期时间,如果不存在则返回 null</returns>
|
||||
private DateTime? GetLocalCertificateExpiry(SslDownloadItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var domainPath = Path.Combine(_sslDownloadOption.SavePath, item.Domain);
|
||||
var certFileName = Path.GetFileNameWithoutExtension(item.FileName);
|
||||
var certPath = Path.Combine(domainPath, $"{certFileName}_bundle.crt");
|
||||
|
||||
if (!File.Exists(certPath))
|
||||
{
|
||||
_logger.LogInformation($"本地证书文件不存在: {certPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var certBytes = File.ReadAllBytes(certPath);
|
||||
var certificate = new X509Certificate2(certBytes);
|
||||
|
||||
_logger.LogInformation($"本地证书 {item.Domain} 过期时间: {certificate.NotAfter}");
|
||||
return certificate.NotAfter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"读取本地证书过期时间失败: {item.FileName}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载证书文件
|
||||
/// </summary>
|
||||
/// <param name="provider">SSL 提供者</param>
|
||||
/// <param name="item">下载项</param>
|
||||
/// <param name="certificateId">证书 ID</param>
|
||||
/// <param name="domain">域名</param>
|
||||
/// <returns>Task</returns>
|
||||
private async Task DownloadFileAsync(ISslManagementProvider 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogInformation("SslDownloadJob已销毁");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Hua.DDNS.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 域名解析记录实体类
|
||||
/// </summary>
|
||||
public class DnsRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析记录 ID
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IP 地址
|
||||
/// </summary>
|
||||
public string Ip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主机记录 (例如: www)
|
||||
/// </summary>
|
||||
public string Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子域名 (通常与 Host 相同)
|
||||
/// </summary>
|
||||
public string SubDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 根域名 (例如: example.com)
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生存时间 (TTL)
|
||||
/// </summary>
|
||||
public string TTL { get; set; } = "10";
|
||||
|
||||
/// <summary>
|
||||
/// 记录类型 (A, AAAA, CNAME 等)
|
||||
/// </summary>
|
||||
public string RecordType { get; set; } = "A";
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="ip">IP 地址</param>
|
||||
/// <param name="domain">域名</param>
|
||||
public DnsRecord(string ip,string domain)
|
||||
{
|
||||
Ip = ip;
|
||||
Host = domain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认构造函数
|
||||
/// </summary>
|
||||
public DnsRecord()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// AutoMapper 对象映射配置类
|
||||
/// </summary>
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数,配置映射规则
|
||||
/// </summary>
|
||||
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))
|
||||
;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
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;
|
||||
using System.Text;
|
||||
|
||||
namespace Hua.DDNS.SslProviders.Ali
|
||||
{
|
||||
/// <summary>
|
||||
/// 阿里云 SSL 证书管理提供者
|
||||
/// </summary>
|
||||
public class AliSslProvider : ISslManagementProvider
|
||||
{
|
||||
private readonly Client _client;
|
||||
private readonly AliCloudOption _aliCloudOption;
|
||||
private readonly AliSslApplyOption _aliSslApplyOption;
|
||||
private readonly ILogger<AliSslProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="logger">日志对象</param>
|
||||
/// <param name="aliCloudOption">阿里云配置</param>
|
||||
public AliSslProvider(
|
||||
ILogger<AliSslProvider> logger,
|
||||
IOptions<AliCloudOption> aliCloudOption,
|
||||
IOptions<AliSslApplyOption> aliSslApplyOption)
|
||||
{
|
||||
_logger = logger;
|
||||
_aliCloudOption = aliCloudOption.Value;
|
||||
_aliSslApplyOption = aliSslApplyOption.Value;
|
||||
|
||||
var config = new Config
|
||||
{
|
||||
AccessKeyId = _aliCloudOption.AccessKeyId,
|
||||
AccessKeySecret = _aliCloudOption.AccessKeySecret,
|
||||
Endpoint = _aliCloudOption.Endpoint
|
||||
};
|
||||
|
||||
_client = new Client(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取阿里云上的 SSL 证书列表
|
||||
/// </summary>
|
||||
/// <returns>证书列表</returns>
|
||||
public async Task<List<SslCertificate>> GetCertificatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new ListUserCertificateOrderRequest
|
||||
{
|
||||
OrderType = "CERT",
|
||||
Status = "ISSUED" // 也可以不设置,获取所有
|
||||
};
|
||||
|
||||
var response = await _client.ListUserCertificateOrderAsync(request);
|
||||
var certificates = new List<SslCertificate>();
|
||||
|
||||
if (response.Body.CertificateOrderList != null)
|
||||
{
|
||||
foreach (var cert in response.Body.CertificateOrderList)
|
||||
{
|
||||
certificates.Add(new SslCertificate
|
||||
{
|
||||
CertificateId = cert.CertificateId.ToString(),
|
||||
Domain = cert.Domain,
|
||||
Alias = cert.Name,
|
||||
CertEndTime = string.IsNullOrEmpty(cert.EndDate) ? DateTime.MaxValue : DateTime.Parse(cert.EndDate),
|
||||
StatusMsg = cert.Status
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"获取到 {certificates.Count} 个阿里云 SSL 证书");
|
||||
return certificates;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取阿里云 SSL 证书列表失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载阿里云上的指定的 SSL 证书
|
||||
/// </summary>
|
||||
/// <param name="certificateId">证书 ID</param>
|
||||
/// <param name="savePath">本地保存目录路径</param>
|
||||
/// <param name="fileName">保存的文件名 (不含后缀)</param>
|
||||
/// <returns>下载成功返回 true,否则返回 false</returns>
|
||||
public async Task<bool> DownloadCertificateAsync(string certificateId, string savePath, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogError(ex, $"下载证书失败: {certificateId}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task CleanInvalidCertificatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var certificates = await GetCertificatesAsync();
|
||||
// 筛选过期的证书
|
||||
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 = certId
|
||||
};
|
||||
await _client.DeleteUserCertificateAsync(deleteRequest);
|
||||
_logger.LogInformation($"已删除阿里云过期证书: {cert.Domain} ({cert.CertificateId}), 过期时间: {cert.CertEndTime}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"删除阿里云证书失败: {cert.CertificateId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "清理阿里云无效证书失败");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace Hua.DDNS.SslProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// SSL 证书管理提供者接口
|
||||
/// </summary>
|
||||
public interface ISslManagementProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取 SSL 证书列表
|
||||
/// </summary>
|
||||
/// <returns>证书信息列表</returns>
|
||||
Task<List<SslCertificate>> GetCertificatesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载指定的 SSL 证书
|
||||
/// </summary>
|
||||
/// <param name="certificateId">证书 ID</param>
|
||||
/// <param name="savePath">本地保存目录路径</param>
|
||||
/// <param name="fileName">保存的文件名 (不含后缀)</param>
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
Task CleanInvalidCertificatesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSL 证书信息类
|
||||
/// </summary>
|
||||
public class SslCertificate
|
||||
{
|
||||
/// <summary>
|
||||
/// 证书 ID
|
||||
/// </summary>
|
||||
public string CertificateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书关联的域名
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书别名
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书生效时间
|
||||
/// </summary>
|
||||
public DateTime CertBeginTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书过期时间
|
||||
/// </summary>
|
||||
public DateTime CertEndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态码
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态消息
|
||||
/// </summary>
|
||||
public string StatusMsg { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯云 SSL 证书管理提供者
|
||||
/// </summary>
|
||||
public class TencentSslProvider : ISslManagementProvider
|
||||
{
|
||||
private readonly SslClient _client;
|
||||
private readonly TencentCloudOption _tencentCloudOption;
|
||||
private readonly ILogger<TencentSslProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="logger">日志对象</param>
|
||||
/// <param name="tencentCloudOption">腾讯云配置</param>
|
||||
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" }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取腾讯云上的 SSL 证书列表
|
||||
/// </summary>
|
||||
/// <returns>证书信息列表</returns>
|
||||
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 = 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"获取到 {certificates.Count} 个腾讯云 SSL 证书");
|
||||
return certificates;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取腾讯云 SSL 证书列表失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载腾讯云上的指定的 SSL 证书
|
||||
/// </summary>
|
||||
/// <param name="certificateId">证书 ID</param>
|
||||
/// <param name="savePath">本地保存目录路径</param>
|
||||
/// <param name="fileName">保存的文件名 (不含后缀)</param>
|
||||
/// <returns>下载成功返回 true,否则返回 false</returns>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步清理无效证书
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task CleanInvalidCertificatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var certificates = await GetCertificatesAsync();
|
||||
// 清理已过期的证书 (Status 为 10 或者当前时间已过过期时间)
|
||||
// 排除过期时间为 MaxValue 的情况(即未签发的证书)
|
||||
var expiredCertificates = certificates.Where(c =>
|
||||
(c.CertEndTime != DateTime.MaxValue && c.CertEndTime < DateTime.Now) ||
|
||||
c.Status == 10).ToList();
|
||||
|
||||
foreach (var cert in expiredCertificates)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleteRequest = new DeleteCertificateRequest
|
||||
{
|
||||
CertificateId = cert.CertificateId
|
||||
};
|
||||
await _client.DeleteCertificate(deleteRequest);
|
||||
_logger.LogInformation($"已删除腾讯云过期证书: {cert.Domain} ({cert.CertificateId}), 过期时间: {cert.CertEndTime}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"删除腾讯云证书失败: {cert.CertificateId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "清理腾讯云无效证书失败");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Hua.DDNS.Start
|
||||
{
|
||||
public class Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Tocken
|
||||
/// </summary>
|
||||
public static string Tocken { get; set; } = null;
|
||||
|
||||
}
|
||||
}
|
||||
+106
-30
@@ -1,94 +1,170 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序入口类
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 主入口方法
|
||||
/// </summary>
|
||||
/// <param name="args">命令行参数</param>
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// 配置 Serilog 日志
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File(
|
||||
Path.Combine("Log\\log-.log"),
|
||||
rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
await CreateHostBuilder(args).Build().RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建并配置宿主构造器
|
||||
/// </summary>
|
||||
/// <param name="args">命令行参数</param>
|
||||
/// <returns>IHostBuilder 实例</returns>
|
||||
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService()
|
||||
.UseSerilog()
|
||||
.UseWindowsService() // 支持作为 Windows 服务运行
|
||||
.UseSerilog() // 使用 Serilog 替代默认日志
|
||||
.ConfigureAppConfiguration((context, config) =>
|
||||
{
|
||||
// 清理并重新配置配置源
|
||||
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"));
|
||||
services.Configure<Hua.DDNS.SslProviders.Ali.AliSslApplyOption>(hostContext.Configuration.GetSection("SslDownload:AliApply"));
|
||||
|
||||
// 配置依赖注入和 Quartz
|
||||
ConfigDi(hostContext, services);
|
||||
ConfigQuartz(hostContext, services);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// 配置依赖注入 (DI)
|
||||
/// </summary>
|
||||
/// <param name="hostContext">宿主上下文</param>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <returns>IServiceProvider 实例</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Quartz 定时任务
|
||||
/// </summary>
|
||||
/// <param name="hostContext">宿主上下文</param>
|
||||
/// <param name="services">服务集合</param>
|
||||
private static void ConfigQuartz(HostBuilderContext hostContext, IServiceCollection services)
|
||||
{
|
||||
// if you are using persistent job store, you might want to alter some options
|
||||
// 配置 Quartz 选项
|
||||
services.Configure<QuartzOptions>(options =>
|
||||
{
|
||||
options.Scheduling.IgnoreDuplicates = true; // default: false
|
||||
options.Scheduling.OverWriteExistingData = true; // default: true
|
||||
options.Scheduling.IgnoreDuplicates = true; // 忽略重复任务
|
||||
options.Scheduling.OverWriteExistingData = true; // 覆盖现有数据
|
||||
});
|
||||
|
||||
// base configuration for DI
|
||||
// 添加 Quartz 服务
|
||||
services.AddQuartz(q =>
|
||||
{
|
||||
// handy when part of cluster or you want to otherwise identify ltiple schedulers
|
||||
q.SchedulerId = "Hua.DDNS.Demo";
|
||||
// this is default configuration if you don't alter it
|
||||
q.UseMicrosoftDependencyInjectionJobFactory();
|
||||
|
||||
// these are the defaults
|
||||
q.UseSimpleTypeLoader();
|
||||
q.UseInMemoryStore();
|
||||
q.UseDefaultThreadPool(tp => { tp.MaxConcurrency = 10; });
|
||||
|
||||
//configure jobs with code
|
||||
var appJobKey = new JobKey("AppJob", "AppJobGroup");
|
||||
q.AddJob<AppJob>(j => j
|
||||
// 配置 DDNS 任务 (DdnsJob)
|
||||
var appJobKey = new JobKey("DdnsJob", "DdnsJobGroup");
|
||||
q.AddJob<DdnsJob>(j => j
|
||||
.StoreDurably()
|
||||
.WithIdentity(appJobKey)
|
||||
.WithDescription("AppJob")
|
||||
.WithDescription("DdnsJob")
|
||||
);
|
||||
|
||||
q.AddTrigger(t => t
|
||||
.WithIdentity("AppJob Trigger")
|
||||
.WithIdentity("DdnsJob Trigger")
|
||||
.ForJob(appJobKey)
|
||||
.WithCronSchedule(hostContext.Configuration.GetSection("App:AppJob:Corn").Value)
|
||||
.WithDescription("AppJob trigger")
|
||||
.WithDescription("DdnsJob trigger")
|
||||
.StartNow()
|
||||
);
|
||||
|
||||
// 配置 SSL 下载任务 (SslDownloadJob)
|
||||
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
|
||||
// 添加 Quartz 托管服务
|
||||
services.AddQuartzHostedService(options =>
|
||||
{
|
||||
// when shutting down we want jobs to complete gracefully
|
||||
options.WaitForJobsToComplete = true;
|
||||
|
||||
// when we need to init another IHostedServices first
|
||||
options.StartDelay = TimeSpan.FromSeconds(10);
|
||||
options.WaitForJobsToComplete = true; // 优雅退出
|
||||
options.StartDelay = TimeSpan.FromSeconds(10); // 启动延迟
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+52
-18
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
│ ├── DdnsJob.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.
|
||||
@@ -0,0 +1,3 @@
|
||||
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
cd ./Hua.DDNS
|
||||
docker build -t hua.ddns:latest -f Dockerfile ..
|
||||
Reference in New Issue
Block a user