Skip to content

Commit

Permalink
Merge branch 'master' of github.com:nidbCN/AliCdnSSLWorker
Browse files Browse the repository at this point in the history
  • Loading branch information
nidbCN committed Dec 10, 2024
2 parents 063a451 + 313b97b commit 8167bb4
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 126 deletions.
4 changes: 2 additions & 2 deletions AliCdnSSLWorker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<UserSecretsId>dotnet-AliCdnSSLWorker-69cd49e9-faf0-424c-980f-751b97c9a16a</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
<AssemblyVersion>8.0.2.3</AssemblyVersion>
<FileVersion>8.0.2.3</FileVersion>
<AssemblyVersion>8.0.3.0</AssemblyVersion>
<FileVersion>8.0.3.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
7 changes: 6 additions & 1 deletion Configs/CertConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

public record CertConfig
{
public required string CertSerchPath { get; init; }
public required string CertSearchPath { get; init; }

// ReSharper disable once StringLiteralTypo
public string CertFileName { get; set; } = "fullchain.pem";
public string PrivateKeyFileName { get; set; } = "privkey.pem";

public bool RecursionSearch { get; init; } = true;
public uint IntervalHour { get; init; } = 24;
public uint CacheTimeoutMin { get; init; } = 30;
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER root
WORKDIR /app
Expand Down
2 changes: 1 addition & 1 deletion Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
builder.Services.AddSingleton<AliCdnService>();
builder.Services.AddSingleton<CertScanService>();

builder.Services.AddHostedService<SSLWorker>();
builder.Services.AddHostedService<SslWorker>();
builder.Services.AddHostedService<ApiWorker>();

var host = builder.Build();
Expand Down
84 changes: 2 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,14 @@

![image](https://github.com/nidbCN/AliCdnSSLWorker/assets/36162655/cb36b8b9-063e-44a8-bd6c-02d312f1e5e9)


这玩意做了什么?
每隔一段时间读取一下你的证书,然后看看阿里云CDN上面的证书快过期没,过期的话就把它换了。如果阿里云上面这个域名没开SSL,但是你列表里面写了,也有证书那就给开了。

## 安装

配置文件如下图所示:

```json
{
"Logging": {
"LogLevel": {
"Default": "Debug", // 日志等级
"Microsoft.Hosting.Lifetime": "Information"
}
},
"CertConfig": {
"CertSerchPath": "/opt/letsencrypt/live", // 证书目录
"IntervalHour": "12", // 检测时间
"CacheTimeoutMin": "30", // 内部缓存过期时间
"DomainList": [ // 域名列表
"cdn1.gaein.cn",
"cdn2.gaein.cn"
// ...
]
},
"ApiConfig": {
"AccessKeyId": "LT**********************", // 阿里云 AK
"AccessKeySecret": "6L***********************", // 阿里云 AK
"Endpoint": "cdn.aliyuncs.com" // 参考https://api.aliyun.com/product/Cdn
}
}
```

如果你希望使用环境变量来传递配置,请直接按照配置文件中进行设置,在表示嵌套的对象时候使用 `__`,比如使用 `CERTCONFIG__CERTSERCHPATH` 来配置证书目录。

> 注意:环境变量的优先级低于配置文件,如果在配置文件中设置了证书目录则会覆盖掉你在环境变量中的设置。
## 使用

### docker & docker compose 部署

在项目源代码目录使用指令打包 docker 镜像:

```bash
docker build -t gaein/alicdn-ssl-worker:<your_version> .
```

如果出现类似提示,请在命令前加上 `sudo`

```bash
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json": dial unix /var/run/docker.sock: connect: permission denied
```
打包完成后使用 `docker images` 查看是否存在镜像
新建目录,使用 `docker-compose` 来部署,示例的文件如下:
```yaml
volumes:
ssl_data:
driver: local
driver_opts:
type: none
o: bind
device: './nginx/letsencrypt'
### 部署

configs:
alicdn-ssl.conf:
file: './alicdn-ssl/appsettings.json'

services:
alicdn-ssl:
image: 'gaein/alicdn-ssl-worker:v1.4'
container_name: 'alicdn-ssl'
environment:
TZ: 'Asia/Shanghai'
ports:
- 5057:5057
depends_on:
- nginx
restart: unless-stopped
volumes:
- ssl_data:/data/ssl
configs:
- source: alicdn-ssl.conf
target: '/app/appsettings.json'
```
教程见:[blog.gaein.cn](https://blog.gaein.cn/passages/auto-deploy-cert-to-alicdn/)

### WebAPI

Expand Down
2 changes: 1 addition & 1 deletion Services/AliCdnService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public bool TryGetHttpsCerts(out IEnumerable<DomainCertInfo> infos)
var resp = _apiClient.DescribeCdnHttpsDomainListWithOptions(req, _runtimeOptions);
if (resp.StatusCode == 200)
{
infos = resp.Body.CertInfos.CertInfo.Select(c => new DomainCertInfo()
infos = resp.Body.CertInfos.CertInfo.Select(c => new DomainCertInfo
{
Name = c.DomainName,
CertExpireTime = DateTime.Parse(c.CertExpireTime),
Expand Down
64 changes: 43 additions & 21 deletions Services/CertScanService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace AliCdnSSLWorker.Services;
public class CertScanService
{
private readonly ILogger<CertScanService> _logger;
private readonly CertConfig _options;
private readonly IOptions<CertConfig> _options;
private DateTime _lastScan;
private readonly HashSet<string> _domainList;

Expand All @@ -17,10 +17,9 @@ public class CertScanService
public CertScanService(ILogger<CertScanService> logger, IOptions<CertConfig> options)
{
_logger = logger;
_options = options;

_options = options.Value ?? throw new ArgumentNullException(nameof(options));

_domainList = _options.DomainList;
_domainList = _options.Value.DomainList;

ScanCertAsync().GetAwaiter().GetResult();
}
Expand All @@ -30,7 +29,7 @@ public CertScanService(ILogger<CertScanService> logger, IOptions<CertConfig> opt
ArgumentNullException.ThrowIfNull(domain);

var time = DateTime.Now - _lastScan;
if (time > TimeSpan.FromMinutes(_options.CacheTimeoutMin))
if (time > TimeSpan.FromMinutes(_options.Value.CacheTimeoutMin))
{
ScanCertAsync().Wait();
_lastScan = DateTime.Now;
Expand All @@ -42,20 +41,28 @@ public CertScanService(ILogger<CertScanService> logger, IOptions<CertConfig> opt

private async Task ScanCertAsync()
{
var dir = new DirectoryInfo(_options.CertSerchPath);
var dir = new DirectoryInfo(_options.Value.CertSearchPath);
if (!dir.Exists)
{
_logger.LogError("Dir {d} isn't exists!", dir.FullName);
return;
}

var dirList = dir.GetDirectories();
var dirList = _options.Value.RecursionSearch
? dir.GetDirectories()
: [dir];

foreach (var subDir in dirList)
{
var certFile = new FileInfo(Path.Combine(subDir.FullName, "cert.pem"));
var privateKeyFile = new FileInfo(Path.Combine(subDir.FullName, "privkey.pem"));
var certFile = subDir
.GetFiles()
.FirstOrDefault(f => f.Name == _options.Value.CertFileName);

var privateKeyFile = subDir
.GetFiles()
.FirstOrDefault(f => f.Name == _options.Value.PrivateKeyFileName);

if (!certFile.Exists || !privateKeyFile.Exists)
if (certFile is null || privateKeyFile is null)
{
_logger.LogWarning("Can not found cert or private key file, skip {d}", subDir.Name);
continue;
Expand All @@ -65,10 +72,10 @@ private async Task ScanCertAsync()
using var reader = new StreamReader(fs);
var line = await reader.ReadLineAsync();

const string CERT_BEGIN = "-----BEGIN CERTIFICATE-----";
const string CERT_END = "-----END CERTIFICATE-----";
const string certBeginFlag = "-----BEGIN CERTIFICATE-----";
const string certEndFlag = "-----END CERTIFICATE-----";

if (line is null || !line.Contains("-----BEGIN CERTIFICATE-----"))
if (line is null || !line.Contains(certBeginFlag))
{
_logger.LogWarning("Can not found BEGIN CERT flag, skip.");
continue;
Expand All @@ -77,9 +84,9 @@ private async Task ScanCertAsync()
// 是证书
var stringBuilder = new StringBuilder((int)certFile.Length);

while ((line = reader.ReadLine()) != null)
while (!string.IsNullOrWhiteSpace(line = await reader.ReadLineAsync()))
{
if (line.Contains("-----END CERTIFICATE-----"))
if (line.Contains(certEndFlag))
break;

stringBuilder.Append(line);
Expand All @@ -88,25 +95,40 @@ private async Task ScanCertAsync()
var certContent = stringBuilder.ToString();
var cert = new X509Certificate2(Convert.FromBase64String(certContent));

var commonName = cert.GetNameInfo(X509NameType.SimpleName, false);
var certDomain = cert.GetNameInfo(X509NameType.SimpleName, false);

if (!_domainList.Contains(commonName))
IList<string> matchedDomainList;

if (certDomain.StartsWith("*."))
{
matchedDomainList = _domainList
.Where(certDomain.EndsWith)
.ToArray();
}
else if (_domainList.Contains(certDomain))
{
matchedDomainList = new List<string> { certDomain };
}
else
{
// 不监听此域名
_logger.LogInformation("Domain {d} is not in list, skip.", commonName);
_logger.LogInformation("Domain {d} is not in list, skip.", certDomain);
continue;
}

_logger.LogInformation("Scan cert for domain {d}", commonName);
_logger.LogInformation("Domain {cert domain} cert matched for domain {match list}", certDomain, string.Join(',', matchedDomainList));

using var keyReader = new StreamReader(privateKeyFile.OpenRead());
var keyPem = await keyReader.ReadToEndAsync();

var certPem = stringBuilder.Insert(0, CERT_BEGIN + "\n").Append("\n" + CERT_END).ToString();
var certPem = await reader.ReadToEndAsync();

_logger.LogInformation("Success load cert, cert {c}, private {p}", certPem, keyPem[..24]);

_certList[commonName] = (certPem, keyPem);
foreach (var matchDomain in matchedDomainList)
{
_certList[matchDomain] = (certPem, keyPem);
}
}
}
}
28 changes: 12 additions & 16 deletions Workers/SSLWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

namespace AliCdnSSLWorker.Workers;

public class SSLWorker(ILogger<SSLWorker> logger,
public class SslWorker(ILogger<SslWorker> logger,
IOptions<CertConfig> options,
AliCdnService aliCdnService,
CertScanService certScanService) : BackgroundService
{
private readonly CertConfig _config = options.Value;
private readonly TimeSpan _interval = TimeSpan.FromHours(options.Value.IntervalHour);

public bool TryUpdate()
Expand All @@ -20,23 +19,21 @@ public bool TryUpdate()
return false;
}

var infoDict = infos
.ToDictionary(i => i.CertCommonName, i => i.CertExpireTime);
var now = DateTime.Now;

foreach (var domain in _config.DomainList)
// ReSharper disable once PossibleMultipleEnumeration
var willExpiredDomainList = infos
.Where(i => options.Value.DomainList.Contains(i.Name))
.Select(i => (i.Name, i.CertCommonName, i.CertExpireTime - now))
.Where(tuple => tuple.Item3 <= _interval)
.ToList();


foreach (var (domain, cn, expiredTime) in willExpiredDomainList)
{
if (infoDict.TryGetValue(domain, out var time))
{
var timeToExpiry = time - DateTime.Now;

if (timeToExpiry > _interval)
{
logger.LogInformation("Domain {cn} has {d}d,{h}hr,{m}min expire.No need refresh.", domain, timeToExpiry.Days, timeToExpiry.Hours, timeToExpiry.Minutes);
continue;
}
logger.LogInformation("CDN {cdn name} cert `{cn}` has {t:c} expire. Upload local cert.", domain, cn, expiredTime);

logger.LogInformation("Domain {cn} has {d}d,{h}hr,{m}min expire.Upload new.", domain, timeToExpiry.Days, timeToExpiry.Hours, timeToExpiry.Minutes);
}

logger.LogInformation("Update domain {d}", domain);
var certPair = certScanService.GetCertByDomain(domain);
Expand All @@ -48,7 +45,6 @@ public bool TryUpdate()

if (aliCdnService.TryUploadCert(domain, certPair.Value))
logger.LogInformation("Success upload cert for {d}.", domain);

}

return true;
Expand Down

0 comments on commit 8167bb4

Please sign in to comment.