更新阿里云SSL证书接口

This commit is contained in:
net909
2026-04-11 18:16:15 +08:00
parent b1b0655cc0
commit 72492e9fd9
10 changed files with 433 additions and 64 deletions

82
.github/docker/Dockerfile vendored Normal file
View File

@@ -0,0 +1,82 @@
ARG ALPINE_VERSION=3.19
FROM alpine:${ALPINE_VERSION}
# Setup document root
WORKDIR /app/www
# Install packages and remove default server definition
RUN apk add --no-cache \
bash \
curl \
nginx \
php82 \
php82-ctype \
php82-curl \
php82-dom \
php82-fileinfo \
php82-fpm \
php82-ftp \
php82-gd \
php82-gettext \
php82-intl \
php82-iconv \
php82-mbstring \
php82-mysqli \
php82-opcache \
php82-openssl \
php82-phar \
php82-sodium \
php82-session \
php82-simplexml \
php82-tokenizer \
php82-xml \
php82-xmlreader \
php82-xmlwriter \
php82-zip \
php82-pdo \
php82-pdo_mysql \
php82-pdo_sqlite \
php82-pecl-swoole \
php82-pecl-ssh2 \
supervisor
RUN rm -rf /var/cache/apk/* /tmp/*
# Configure nginx - http
COPY config/nginx.conf /etc/nginx/nginx.conf
# Configure PHP-FPM
ENV PHP_INI_DIR /etc/php82
COPY config/fpm-pool.conf ${PHP_INI_DIR}/php-fpm.d/www.conf
COPY config/php.ini ${PHP_INI_DIR}/conf.d/custom.ini
# Configure supervisord
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Add application
RUN mkdir -p /usr/src && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
# Install composer
RUN wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
RUN composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader
RUN adduser -D -s /sbin/nologin -g www www && chown -R www.www /usr/src/www /var/lib/nginx /var/log/nginx
# crontab
RUN echo "* * * * * cd /app/www && /usr/bin/php82 think certtask" | crontab -u www -
COPY config/run_tasks.sh /app/run_tasks.sh
RUN chmod +x /app/run_tasks.sh
# copy entrypoint script
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["sh", "/entrypoint.sh"]
# Expose the port nginx is reachable on
EXPOSE 80
# Let supervisord start nginx & php-fpm
CMD /usr/sbin/crond && /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
# Configure a healthcheck to validate that everything is up&running
HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1/fpm-ping || exit 1

21
.github/docker/config/fpm-pool.conf vendored Normal file
View File

@@ -0,0 +1,21 @@
[global]
error_log = /dev/stderr
[www]
listen = /run/php-fpm.sock
listen.backlog = 8192
listen.allowed_clients = 127.0.0.1
listen.owner = www
listen.group = www
listen.mode = 0666
user = www
group = www
pm.status_path = /fpm-status
pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 60s;
pm.max_requests = 1000
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
ping.path = /fpm-ping

94
.github/docker/config/nginx.conf vendored Normal file
View File

@@ -0,0 +1,94 @@
user www;
worker_processes auto;
error_log stderr warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
# Threat files with a unknown filetype as binary
default_type application/octet-stream;
# Define custom log format to include reponse times
log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time $pipe $upstream_cache_status';
access_log /dev/stdout main_timed;
error_log /dev/stderr crit;
keepalive_timeout 65;
server_tokens off;
# Enable gzip compression by default
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
# Include server configs
server {
listen [::]:80 default_server;
listen 80 default_server;
server_name _;
sendfile on;
tcp_nodelay on;
absolute_redirect off;
root /app/www/public;
index index.php;
# Pass the PHP scripts to PHP-FPM listening on php-fpm.sock
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
}
#rewrite rule for pretty urls
location / {
if (!-e $request_filename){
rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}
# Set the cache-control headers on assets to cache for 5 days
location ~* \.(jpg|jpeg|gif|png|ico|bmp)$ {
access_log off;
expires 30d;
}
location ~* \.(css|js)$ {
access_log off;
expires 12h;
}
# Deny access to . files, for security
location ~ /\. {
log_not_found off;
deny all;
}
# Allow fpm ping and status from localhost
location ~ ^/(fpm-status|fpm-ping)$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm.sock;
}
}
}

15
.github/docker/config/php.ini vendored Normal file
View File

@@ -0,0 +1,15 @@
[PHP]
short_open_tag = On
expose_php = Off
max_execution_time = 300
post_max_size = 50M
upload_max_filesize = 50M
[Date]
date.timezone = PRC
[Opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=10000
opcache.revalidate_freq=30

7
.github/docker/config/run_tasks.sh vendored Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
if [ -f "/app/www/.env" ]; then
php /app/www/think dmtask
else
exit 0
fi

37
.github/docker/config/supervisord.conf vendored Normal file
View File

@@ -0,0 +1,37 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:php-fpm]
command=php-fpm82 -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=false
startretries=0
[program:nginx]
command=nginx -g 'daemon off;'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=false
startretries=0
[program:dmtask]
command=php think dmtask
user=www
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startsecs=5
startretries=99999

18
.github/docker/entrypoint.sh vendored Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
if [ ! -f /app/www/public/index.php ] || [ ! -f /app/firstrun ]; then
echo 'Copying new files'
\cp -a /usr/src/www /app/
if [ -d /app/www/runtime/cache ]; then
rm -rf /app/www/runtime/*
fi
chown -R www.www /app/www
touch /app/firstrun
fi
exec "$@"

53
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
# 手动触发构建多架构镜像amd64 / arm64仅推送 latest 至 Docker Hub 与华为云 SWR。
# Dockerfile 与构建上下文位于 .github/docker/ 目录。
#
# 需在仓库 Settings → Secrets 中配置:
# DOCKERHUB_USERNAME / DOCKERHUB_TOKENDocker Hub 访问令牌)
# HUAWEI_SWR_USERNAME / HUAWEI_SWR_PASSWORD华为云 SWR 登录凭证,与本地 docker login swr.cn-east-3.myhuaweicloud.com 一致)
name: Docker Build
on:
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to Huawei SWR
uses: docker/login-action@v3
with:
registry: swr.cn-east-3.myhuaweicloud.com
username: ${{ secrets.HUAWEI_SWR_USERNAME }}
password: ${{ secrets.HUAWEI_SWR_PASSWORD }}
- name: Build and push (Docker Hub + Huawei SWR, latest only)
uses: docker/build-push-action@v6
with:
context: .github/docker
file: .github/docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
netcccyun/dnsmgr:latest
swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -257,7 +257,7 @@ location / {
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/instance/TEST/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/instance/test/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买测试证书,并在联系人管理添加联系人。',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
@@ -271,24 +271,6 @@ location / {
'placeholder' => '',
'required' => true,
],
'username' => [
'name' => '姓名',
'type' => 'input',
'placeholder' => '申请联系人的姓名',
'required' => true,
],
'phone' => [
'name' => '手机号码',
'type' => 'input',
'placeholder' => '申请联系人的手机号码',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请联系人的邮箱地址',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',

View File

@@ -27,18 +27,18 @@ class aliyun implements CertInterface
public function register()
{
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空');
$param = ['Action' => 'ListUserCertificateOrder'];
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) throw new Exception('必填参数不能为空');
$param = ['Action' => 'ListInstances'];
$this->request($param, true);
return true;
}
public function buyCert($domainList, &$order)
{
$param = ['Action' => 'DescribePackageState', 'ProductCode' => 'digicert-free-1-free'];
$param = ['Action' => 'GetInstanceSummary', 'InstanceType' => 'TEST'];
$data = $this->request($param, true);
if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('没有可用的免费证书资源包');
$this->log('证书资源包总数量:' . $data['TotalCount'] . ',已使用数量:' . $data['UsedCount']);
if (!isset($data['InactiveCount']) || $data['InactiveCount'] == 0) throw new Exception('没有待使用的测试证书实例,请先购买测试证书');
$this->log('实例总个数:' . $data['TotalCount'] . ',实例待使用总数:' . $data['InactiveCount']);
}
public function createOrder($domainList, &$order, $keytype, $keysize)
@@ -46,31 +46,92 @@ class aliyun implements CertInterface
if (empty($domainList)) throw new Exception('域名列表不能为空');
$domain = $domainList[0];
$param = [
'Action' => 'CreateCertificateRequest',
'ProductCode' => 'digicert-free-1-free',
'Username' => $this->config['username'],
'Phone' => $this->config['phone'],
'Email' => $this->config['email'],
'Domain' => $domain,
'ValidateType' => 'DNS'
'Action' => 'ListInstances',
'Status' => 'inactive',
'InstanceType' => 'TEST',
];
$data = $this->request($param, true);
if (empty($data['OrderId'])) throw new Exception('证书申请失败OrderId为空');
$order['OrderId'] = $data['OrderId'];
sleep(3);
if (empty($data['InstanceList'])) throw new Exception('待使用的测试证书实例列表为空');
$instanceId = $data['InstanceList'][0]['InstanceId'];
$param = [
'Action' => 'DescribeCertificateState',
'OrderId' => $order['OrderId'],
'Action' => 'ListContact',
];
$data = $this->request($param, true);
if (empty($data['ContactList'])) throw new Exception('联系人列表为空,请先添加联系人');
$contactId = $data['ContactList'][0]['ContactId'];
if ($keytype == 'ECC') $KeyAlgorithm = 'ECC_256';
else if ($keysize == '3072') $KeyAlgorithm = 'RSA_3072';
else $KeyAlgorithm = 'RSA_2048';
$param = [
'Action' => 'UpdateInstance',
'InstanceId' => $instanceId,
'Domain' => $domain,
'KeyAlgorithm' => $KeyAlgorithm,
'AutoReissue' => 'disable',
'ContactIdList.1' => $contactId,
'ValidateType' => 'DNS'
];
try {
$this->request($param);
} catch (Exception $e) {
throw new Exception('更新证书实例失败:' . $e->getMessage());
}
$param = [
'Action' => 'ApplyCertificate',
'InstanceId' => $instanceId
];
try {
$this->request($param);
} catch (Exception $e) {
throw new Exception('申请证书失败:' . $e->getMessage());
}
sleep(1);
$status = '';
do {
$param = [
'Action' => 'GetTaskAttribute',
'TaskId' => $instanceId
];
try {
$data = $this->request($param, true);
$status = $data['TaskStatus'];
} catch (Exception $e) {
throw new Exception('申请证书提交结果查询失败:' . $e->getMessage());
}
if ($status == 'processing') {
sleep(1);
} elseif ($status == 'failed') {
throw new Exception('申请证书失败:' . $data['TaskMessage']);
} else {
break;
}
} while ($status == 'processing');
$param = [
'Action' => 'GetInstanceDetail',
'InstanceId' => $instanceId
];
try {
$data = $this->request($param, true);
} catch (Exception $e) {
throw new Exception('获取实例详情失败:' . $e->getMessage());
}
$order['InstanceId'] = $instanceId;
$dnsList = [];
if ($data['Type'] == 'domain_verify') {
$mainDomain = getMainDomain($domain);
$name = substr($data['RecordDomain'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']];
if (!empty($data['DomainValidationList'])) {
foreach ($data['DomainValidationList'] as $opts) {
$mainDomain = getMainDomain($opts['Domain']);
$name = substr($opts['ValidationKey'] . '.' . $opts['RootDomain'], 0, - (strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $opts['ValidationType'], 'value' => $opts['ValidationValue']];
}
}
return $dnsList;
@@ -81,13 +142,13 @@ class aliyun implements CertInterface
public function getAuthStatus($domainList, $order)
{
$param = [
'Action' => 'DescribeCertificateState',
'OrderId' => $order['OrderId'],
'Action' => 'GetInstanceDetail',
'InstanceId' => $order['InstanceId'],
];
$data = $this->request($param, true);
if ($data['Type'] == 'certificate') {
if ($data['Status'] == 'normal') {
return true;
} elseif ($data['Type'] == 'verify_fail') {
} elseif ($data['Status'] == 'closed') {
throw new Exception('证书审核失败');
} else {
return false;
@@ -97,12 +158,19 @@ class aliyun implements CertInterface
public function finalizeOrder($domainList, $order, $keytype, $keysize)
{
$param = [
'Action' => 'DescribeCertificateState',
'OrderId' => $order['OrderId'],
'Action' => 'GetInstanceDetail',
'InstanceId' => $order['InstanceId'],
];
$data = $this->request($param, true);
$fullchain = $data['Certificate'];
$private_key = $data['PrivateKey'];
if (empty($data['CertificateId'])) throw new Exception('证书ID不存在');
$param = [
'Action' => 'GetUserCertificateDetail',
'CertId' => $data['CertificateId'],
];
$data = $this->request($param, true);
$fullchain = $data['Cert'];
$private_key = $data['Key'];
if (empty($fullchain) || empty($private_key)) throw new Exception('证书内容获取失败');
$certInfo = openssl_x509_parse($fullchain, true);
@@ -113,8 +181,8 @@ class aliyun implements CertInterface
public function revoke($order, $pem)
{
$param = [
'Action' => 'CancelCertificateForPackageRequest',
'OrderId' => $order['OrderId'],
'Action' => 'RevokeCertificate',
'InstanceId' => $order['InstanceId'],
];
$this->request($param);
}
@@ -122,22 +190,14 @@ class aliyun implements CertInterface
public function cancel($order)
{
$param = [
'Action' => 'DescribeCertificateState',
'OrderId' => $order['OrderId'],
'Action' => 'GetInstanceDetail',
'InstanceId' => $order['InstanceId'],
];
$data = $this->request($param, true);
if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process') {
if ($data['Status'] == 'pending') {
$param = [
'Action' => 'CancelOrderRequest',
'OrderId' => $order['OrderId'],
];
$this->request($param);
usleep(500000);
}
if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process' || $data['Type'] == 'payed' || $data['Type'] == 'verify_fail') {
$param = [
'Action' => 'DeleteCertificateRequest',
'OrderId' => $order['OrderId'],
'Action' => 'CancelPendingCertificate',
'InstanceId' => $order['InstanceId'],
];
$this->request($param);
}