mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-09 23:16:27 +02:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4825f1312 | ||
|
|
2dd4978fb3 | ||
|
|
349c1d70e2 | ||
|
|
a112cf0ea2 | ||
|
|
c1600c7b17 | ||
|
|
b8f64db33c | ||
|
|
96e1c8a972 | ||
|
|
bebd655bcc | ||
|
|
36d42da491 | ||
|
|
b0d831da56 | ||
|
|
c420c81877 | ||
|
|
72492e9fd9 | ||
|
|
3075b8d8a5 | ||
|
|
92d8f49778 | ||
|
|
1084fea43b | ||
|
|
7670d5a387 | ||
|
|
a9b773868d | ||
|
|
141b2ead43 | ||
|
|
b1b0655cc0 | ||
|
|
c64a385ffc | ||
|
|
7d02f15fde | ||
|
|
918bd872d9 | ||
|
|
0bc745e164 | ||
|
|
efed00afa3 | ||
|
|
1b1605400d | ||
|
|
879e667d9a | ||
|
|
0813f2cdca | ||
|
|
780e01ce4f | ||
|
|
3ea41c1c8b | ||
|
|
e25d5d76e9 | ||
|
|
c0e72908ab | ||
|
|
867785b774 | ||
|
|
d579ca07af | ||
|
|
7161caf0a5 | ||
|
|
c91b116466 | ||
|
|
b2d27b18a3 | ||
|
|
ee45ddd7ec | ||
|
|
9b66b020c9 | ||
|
|
ec16c3fc8b |
82
.github/docker/Dockerfile
vendored
Normal file
82
.github/docker/Dockerfile
vendored
Normal 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
21
.github/docker/config/fpm-pool.conf
vendored
Normal 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
94
.github/docker/config/nginx.conf
vendored
Normal 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
15
.github/docker/config/php.ini
vendored
Normal 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
7
.github/docker/config/run_tasks.sh
vendored
Normal 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
37
.github/docker/config/supervisord.conf
vendored
Normal 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
18
.github/docker/entrypoint.sh
vendored
Normal 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 "$@"
|
||||
56
.github/workflows/docker-build.yml
vendored
Normal file
56
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# 手动触发:构建多架构镜像(amd64 / arm64),仅推送 latest 至 Docker Hub 与华为云 SWR。
|
||||
# Dockerfile 与构建上下文位于 .github/docker/ 目录。
|
||||
#
|
||||
# 需在仓库 Settings → Secrets 中配置:
|
||||
# DOCKERHUB_USERNAME / DOCKERHUB_TOKEN(Docker 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
|
||||
# 避免向仓库推送 attestations;部分镜像仓库(含部分 SWR 场景)无法解析导致 “fail to parse manifest.json”
|
||||
provenance: false
|
||||
sbom: false
|
||||
tags: |
|
||||
netcccyun/dnsmgr:latest
|
||||
swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
/vendor
|
||||
*.log
|
||||
.env
|
||||
.ace-tool/
|
||||
/.codex-tmp/dns-panel-ref/
|
||||
|
||||
@@ -302,10 +302,12 @@ function getMainDomain($host)
|
||||
$domains = config('temp.domains');
|
||||
if (!$domains) {
|
||||
$domains = Db::name('domain')->column('name');
|
||||
$domains_alias = Db::name('domain_alias')->column('name');
|
||||
$domains = array_merge($domains, $domains_alias);
|
||||
config(['domains'=>$domains], 'temp');
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
if (str_ends_with($host, $domain)) {
|
||||
if ($host === $domain || str_ends_with($host, '.' . $domain)) {
|
||||
return $domain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,9 +505,12 @@ class Cert extends BaseController
|
||||
$mainDomain = getMainDomain($domain);
|
||||
$drow = Db::name('domain')->where('name', $mainDomain)->find();
|
||||
if (!$drow) {
|
||||
if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2);
|
||||
if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) {
|
||||
return ['code' => -1, 'msg' => '域名' . $domain . '未在本系统添加'];
|
||||
$drow = Db::name('domain_alias')->alias('A')->join('domain B', 'A.did = B.id')->where('A.name', $mainDomain)->find();
|
||||
if (!$drow) {
|
||||
if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2);
|
||||
if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) {
|
||||
return ['code' => -1, 'msg' => '域名' . $domain . '未在本系统添加'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1070
app/controller/Cloudflare.php
Normal file
1070
app/controller/Cloudflare.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -198,7 +198,7 @@ class Domain extends BaseController
|
||||
if (!empty($id)) {
|
||||
$select->where('A.id', $id);
|
||||
} elseif (!empty($kw)) {
|
||||
$select->whereLike('name|A.remark', '%' . $kw . '%');
|
||||
$select->whereLike('A.name|A.remark', '%' . $kw . '%');
|
||||
}
|
||||
if (!empty($aid)) {
|
||||
$select->where('A.aid', $aid);
|
||||
@@ -305,6 +305,7 @@ class Domain extends BaseController
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$id = input('post.id/d');
|
||||
Db::name('domain')->where('id', $id)->delete();
|
||||
Db::name('domain_alias')->where('did', $id)->delete();
|
||||
Db::name('dmtask')->where('did', $id)->delete();
|
||||
Db::name('optimizeip')->where('did', $id)->delete();
|
||||
Db::name('sctask')->where('did', $id)->delete();
|
||||
@@ -1106,8 +1107,88 @@ class Domain extends BaseController
|
||||
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
$domainRecords = $dns->getWeightSubDomains($page, $limit, $keyword);
|
||||
if (!$domainRecords) return json(['total' => 0, 'rows' => []]);
|
||||
return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]);
|
||||
}
|
||||
|
||||
public function alias()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$drow = Db::name('domain')->where('id', $id)->find();
|
||||
if (!$drow) {
|
||||
return $this->alert('error', '域名不存在');
|
||||
}
|
||||
if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限');
|
||||
if (request()->isAjax()) {
|
||||
$act = input('param.act');
|
||||
if ($act == 'add') {
|
||||
$alias = input('post.alias', null, 'trim');
|
||||
if (empty($alias)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
if ($dns->addDomainAlias($alias)) {
|
||||
return json(['code' => 0, 'msg' => '添加域名别名成功']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '添加域名别名失败,' . $dns->getError()]);
|
||||
}
|
||||
} elseif ($act == 'delete') {
|
||||
$alias_id = input('post.alias_id/d');
|
||||
if (empty($alias_id)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
if ($dns->deleteDomainAlias($alias_id)) {
|
||||
return json(['code' => 0, 'msg' => '删除域名别名成功']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '删除域名别名失败,' . $dns->getError()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
$domainAliasList = $dns->domainAliasList();
|
||||
if ($domainAliasList === false) $domainAliasList = [];
|
||||
|
||||
$this->updateAliasList($id, $domainAliasList);
|
||||
|
||||
View::assign('domainId', $id);
|
||||
View::assign('domainName', $drow['name']);
|
||||
View::assign('domainAliasList', $domainAliasList);
|
||||
return view();
|
||||
}
|
||||
|
||||
private function updateAliasList($id, $domainAliasList)
|
||||
{
|
||||
$domainAliases = array_column($domainAliasList, 'DomainAlias');
|
||||
$addList = [];
|
||||
$deleteList = [];
|
||||
$existList = Db::name('domain_alias')->where('did', $id)->select()->toArray();
|
||||
$existAliases = array_column($existList, 'name');
|
||||
foreach ($existList as $item) {
|
||||
if (!in_array($item['name'], $domainAliases)) {
|
||||
$deleteList[] = $item['id'];
|
||||
}
|
||||
}
|
||||
foreach ($domainAliases as $item) {
|
||||
if (!in_array($item, $existAliases)) {
|
||||
$addList[] = $item;
|
||||
}
|
||||
}
|
||||
if (!empty($deleteList)) {
|
||||
Db::name('domain_alias')->where('id', 'in', $deleteList)->delete();
|
||||
}
|
||||
if (!empty($addList)) {
|
||||
$dataList = [];
|
||||
foreach ($addList as $item) {
|
||||
$dataList[] = [
|
||||
'did' => $id,
|
||||
'name' => $item,
|
||||
];
|
||||
}
|
||||
Db::name('domain_alias')->insertAll($dataList);
|
||||
}
|
||||
}
|
||||
|
||||
public function expire_notice()
|
||||
{
|
||||
|
||||
@@ -85,8 +85,11 @@ class Optimizeip extends BaseController
|
||||
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
|
||||
return json(['code' => -1, 'msg' => '必填项不能为空']);
|
||||
}
|
||||
if ($task['recordnum'] > 5) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
|
||||
if ($task['recordnum'] < 1) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
|
||||
}
|
||||
if ($task['recordnum'] > 50) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
|
||||
}
|
||||
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()) {
|
||||
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);
|
||||
@@ -109,8 +112,11 @@ class Optimizeip extends BaseController
|
||||
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
|
||||
return json(['code' => -1, 'msg' => '必填项不能为空']);
|
||||
}
|
||||
if ($task['recordnum'] > 5) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
|
||||
if ($task['recordnum'] < 1) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
|
||||
}
|
||||
if ($task['recordnum'] > 50) {
|
||||
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
|
||||
}
|
||||
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()) {
|
||||
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);
|
||||
|
||||
@@ -95,11 +95,12 @@ class System extends BaseController
|
||||
public function proxytest()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$proxy_server = trim($_POST['proxy_server']);
|
||||
$proxy_port = $_POST['proxy_port'];
|
||||
$proxy_user = trim($_POST['proxy_user']);
|
||||
$proxy_pwd = trim($_POST['proxy_pwd']);
|
||||
$proxy_type = $_POST['proxy_type'];
|
||||
$proxy_server = input('post.proxy_server', '', 'trim');
|
||||
$proxy_port = input('post.proxy_port/d', 0);
|
||||
$proxy_user = input('post.proxy_user', '', 'trim');
|
||||
$proxy_pwd = input('post.proxy_pwd', '', 'trim');
|
||||
$proxy_type = input('post.proxy_type', 'http', 'trim');
|
||||
|
||||
try {
|
||||
check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -257,7 +257,7 @@ location / {
|
||||
'wildcard' => false,
|
||||
'max_domains' => 1,
|
||||
'cname' => false,
|
||||
'note' => '每个自然年有20张免费证书额度,证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/certExtend/free/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',
|
||||
|
||||
@@ -11,7 +11,7 @@ class DeployHelper
|
||||
'name' => '宝塔面板',
|
||||
'class' => 1,
|
||||
'icon' => 'bt.png',
|
||||
'desc' => '支持部署到宝塔面板&aaPanel搭建的站点、Docker、邮局与面板本身',
|
||||
'desc' => '支持部署到宝塔Linux面板&aaPanel搭建的站点',
|
||||
'note' => null,
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
@@ -27,15 +27,6 @@ class DeployHelper
|
||||
'placeholder' => '宝塔面板设置->面板设置->API接口',
|
||||
'required' => true,
|
||||
],
|
||||
'version' => [
|
||||
'name' => '面板版本',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => 'Linux面板+Win经典版',
|
||||
'1' => 'Win极速版',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
@@ -55,6 +46,7 @@ class DeployHelper
|
||||
'3' => 'Docker网站的证书',
|
||||
'2' => '邮局域名的证书',
|
||||
'1' => '面板本身的证书',
|
||||
'4' => '反向代理的证书',
|
||||
],
|
||||
'value' => '0',
|
||||
'required' => true,
|
||||
@@ -64,7 +56,58 @@ class DeployHelper
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站名称,每行一个',
|
||||
'note' => 'PHP项目和反代项目填写创建时绑定的第一个域名,Java/Node/Go等其他项目填写项目名称,邮局和IIS站点填写绑定的域名',
|
||||
'show' => 'type==0||type==2||type==3',
|
||||
'show' => 'type==0||type==2||type==3||type==4',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'btwin' => [
|
||||
'name' => '宝塔Win极速版',
|
||||
'class' => 1,
|
||||
'icon' => 'bt.png',
|
||||
'desc' => '支持部署到宝塔Windows面板极速版',
|
||||
'note' => null,
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '宝塔面板地址',
|
||||
'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'key' => [
|
||||
'name' => '接口密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '宝塔面板设置->面板设置->API接口',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'type' => [
|
||||
'name' => '部署类型',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '网站的证书',
|
||||
'1' => '面板本身的证书',
|
||||
],
|
||||
'value' => '0',
|
||||
'required' => true,
|
||||
],
|
||||
'sites' => [
|
||||
'name' => '网站名称列表',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站名称,每行一个',
|
||||
'note' => '',
|
||||
'show' => 'type==0',
|
||||
'required' => true,
|
||||
],
|
||||
'is_iis' => [
|
||||
@@ -677,6 +720,65 @@ class DeployHelper
|
||||
],
|
||||
],
|
||||
],
|
||||
'acepanel' => [
|
||||
'name' => 'AcePanel',
|
||||
'class' => 1,
|
||||
'icon' => 'acepanel.svg',
|
||||
'desc' => '支持 AcePanel 3.0+ 版本使用',
|
||||
'note' => '支持 AcePanel 3.0+ 版本使用',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'AcePanel 地址',
|
||||
'note' => '填写规则如:https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'id' => [
|
||||
'name' => '访问令牌ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '1',
|
||||
'note' => 'AcePanel 设置->用户->访问令牌',
|
||||
'required' => true,
|
||||
],
|
||||
'token' => [
|
||||
'name' => '访问令牌',
|
||||
'type' => 'input',
|
||||
'note' => 'AcePanel 设置->用户->访问令牌',
|
||||
'placeholder' => '32位字符串',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'type' => [
|
||||
'name' => '部署类型',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => 'AcePanel 网站的证书',
|
||||
'1' => 'AcePanel 本身的证书',
|
||||
],
|
||||
'value' => '0',
|
||||
'required' => true,
|
||||
],
|
||||
'sites' => [
|
||||
'name' => '网站名称列表',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站名称,每行一个',
|
||||
'note' => '填写创建网站时设置的网站唯一名称',
|
||||
'show' => 'type==0',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'ratpanel' => [
|
||||
'name' => '耗子面板',
|
||||
'class' => 1,
|
||||
@@ -777,6 +879,53 @@ class DeployHelper
|
||||
],
|
||||
],
|
||||
],
|
||||
'amh' => [
|
||||
'name' => 'AMH面板',
|
||||
'class' => 1,
|
||||
'icon' => 'amh.ico',
|
||||
'desc' => '',
|
||||
'note' => null,
|
||||
'tasknote' => '',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'AMH面板地址',
|
||||
'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API接口密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '安装amapi软件后查看,是密钥不是私钥',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'env_name' => [
|
||||
'name' => '环境名称',
|
||||
'type' => 'input',
|
||||
'placeholder' => '如:lnmp01',
|
||||
'required' => true,
|
||||
],
|
||||
'vhost_name' => [
|
||||
'name' => '网站名称列表',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站标识域名,每行一个',
|
||||
'note' => '网站标识域名一列的值,并非绑定域名',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'synology' => [
|
||||
'name' => '群晖面板',
|
||||
'class' => 1,
|
||||
@@ -1046,6 +1195,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'cdn', 'label'=>'内容分发CDN'],
|
||||
['value'=>'dcdn', 'label'=>'全站加速DCDN'],
|
||||
['value'=>'esa', 'label'=>'边缘安全加速ESA'],
|
||||
['value'=>'esa_saas', 'label'=>'边缘安全加速ESA SaaS'],
|
||||
['value'=>'oss', 'label'=>'对象存储OSS'],
|
||||
['value'=>'waf', 'label'=>'Web应用防火墙3.0'],
|
||||
['value'=>'waf2', 'label'=>'Web应用防火墙2.0'],
|
||||
@@ -1058,6 +1208,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'vod', 'label'=>'视频点播'],
|
||||
['value'=>'fc', 'label'=>'函数计算3.0'],
|
||||
['value'=>'fc2', 'label'=>'函数计算2.0'],
|
||||
['value'=>'ga', 'label'=>'全球加速'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
@@ -1067,7 +1218,14 @@ ctrl+x 保存退出<br/>',
|
||||
'name' => 'ESA站点域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'ESA添加的站点主域名',
|
||||
'show' => 'product==\'esa\'',
|
||||
'show' => 'product==\'esa\' || product == \'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'esa_saas_sitename' => [
|
||||
'name' => 'ESA SAAS站点域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'ESA SAAS站点域名',
|
||||
'show' => 'product == \'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'oss_endpoint' => [
|
||||
@@ -1092,7 +1250,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'ap-southeast-1', 'label'=>'非中国内地'],
|
||||
],
|
||||
'value' => 'cn-hangzhou',
|
||||
'show' => 'product==\'waf\'||product==\'waf2\'||product==\'ddoscoo\'||product==\'esa\'',
|
||||
'show' => 'product==\'waf\'||product==\'waf2\'||product==\'ddoscoo\'||product==\'esa\'||product==\'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'regionid' => [
|
||||
@@ -1148,6 +1306,21 @@ ctrl+x 保存退出<br/>',
|
||||
'note' => '进入NLB实例详情->监听列表,复制监听ID(只支持TCPSSL监听协议)',
|
||||
'required' => true,
|
||||
],
|
||||
'ga_id' => [
|
||||
'name' => '全球加速实例ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product==\'ga\'',
|
||||
'required' => true,
|
||||
],
|
||||
'ga_listener_id' => [
|
||||
'name' => '监听ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product==\'ga\'',
|
||||
'note' => '进入实例详情->监听列表,复制监听ID(只支持HTTPS监听协议)',
|
||||
'required' => true,
|
||||
],
|
||||
'deploy_type' => [
|
||||
'name' => '部署证书类型',
|
||||
'type' => 'select',
|
||||
@@ -1156,21 +1329,21 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'1', 'label'=>'扩展证书'],
|
||||
],
|
||||
'value' => '0',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'',
|
||||
'required' => true,
|
||||
],
|
||||
'clb_domain' => [
|
||||
'name' => '扩展域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product==\'clb\'&&deploy_type==1',
|
||||
'show' => 'product==\'clb\'&&deploy_type==1||product==\'ga\'&&deploy_type==1',
|
||||
'required' => true,
|
||||
],
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'upload\'',
|
||||
'placeholder' => '多个域名可用,隔开',
|
||||
'show' => 'product!=\'esa\'&&product!=\'esa_saas\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1755,6 +1928,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'alb', 'label'=>'应用型负载均衡ALB'],
|
||||
['value'=>'tos', 'label'=>'对象存储TOS'],
|
||||
['value'=>'live', 'label'=>'视频直播'],
|
||||
['value'=>'vod', 'label'=>'视频点播'],
|
||||
['value'=>'imagex', 'label'=>'veImageX'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
@@ -1768,6 +1942,23 @@ ctrl+x 保存退出<br/>',
|
||||
'show' => 'product==\'tos\'',
|
||||
'required' => true,
|
||||
],
|
||||
'vod_space_name' => [
|
||||
'name' => '点播空间名称',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product==\'vod\'',
|
||||
'required' => true,
|
||||
],
|
||||
'vod_domain_type' => [
|
||||
'name' => '点播域名类型',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value'=>'play', 'label'=>'点播加速域名和自定义源站加速域名'],
|
||||
['value'=>'image', 'label'=>'封面加速域名'],
|
||||
],
|
||||
'show' => 'product==\'vod\'',
|
||||
'required' => true,
|
||||
],
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
@@ -1987,7 +2178,7 @@ ctrl+x 保存退出<br/>',
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -2484,6 +2675,73 @@ ctrl+x 保存退出<br/>',
|
||||
],
|
||||
],
|
||||
],
|
||||
's3storage' => [
|
||||
'name' => 'S3存储',
|
||||
'class' => 3,
|
||||
'icon' => 'cloud.png',
|
||||
'desc' => '支持将证书上传到S3兼容存储(AWS S3、MinIO等)',
|
||||
'note' => '支持AWS S3、MinIO、阿里云OSS(S3兼容模式)等S3协议兼容的对象存储服务',
|
||||
'tasknote' => '证书和私钥将以PEM格式上传到指定的存储桶路径',
|
||||
'inputs' => [
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretAccessKey' => [
|
||||
'name' => 'SecretAccessKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'endpoint' => [
|
||||
'name' => 'S3 Endpoint',
|
||||
'type' => 'input',
|
||||
'placeholder' => '如:s3.amazonaws.com 或 minio.example.com:9000',
|
||||
'note' => 'AWS S3填写s3.区域.amazonaws.com,其他S3兼容服务填写对应地址',
|
||||
'required' => true,
|
||||
],
|
||||
'region' => [
|
||||
'name' => '区域',
|
||||
'type' => 'input',
|
||||
'placeholder' => '如:us-east-1',
|
||||
'value' => 'us-east-1',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'bucket' => [
|
||||
'name' => '存储桶名称',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'cert_path' => [
|
||||
'name' => '证书保存路径',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'ssl/cert.pem',
|
||||
'note' => '在存储桶内的文件路径,如 ssl/domain.com/cert.pem',
|
||||
'required' => true,
|
||||
],
|
||||
'key_path' => [
|
||||
'name' => '私钥保存路径',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'ssl/key.pem',
|
||||
'note' => '在存储桶内的文件路径,如 ssl/domain.com/key.pem',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'local' => [
|
||||
'name' => '复制到本机',
|
||||
'class' => 3,
|
||||
|
||||
@@ -374,6 +374,12 @@ class DnsHelper
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API密钥/令牌',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'auth' => [
|
||||
'name' => '认证方式',
|
||||
'type' => 'radio',
|
||||
@@ -383,12 +389,6 @@ class DnsHelper
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API密钥/令牌',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
@@ -651,7 +651,7 @@ class DnsHelper
|
||||
$dnstype = $account['type'];
|
||||
$class = "\\app\\lib\\dns\\{$dnstype}";
|
||||
if (class_exists($class)) {
|
||||
$config = json_decode($account['config'], true);
|
||||
$config = json_decode($account['config'] ?? '', true);
|
||||
$config['domain'] = $domain;
|
||||
$config['domainid'] = $domainid;
|
||||
$model = new $class($config);
|
||||
@@ -668,7 +668,7 @@ class DnsHelper
|
||||
$dnstype = $account['type'];
|
||||
$class = "\\app\\lib\\dns\\{$dnstype}";
|
||||
if (class_exists($class)) {
|
||||
$config = json_decode($account['config'], true);
|
||||
$config = json_decode($account['config'] ?? '', true);
|
||||
$config['domain'] = $account['name'];
|
||||
$config['domainid'] = $account['thirdid'];
|
||||
$model = new $class($config);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -62,10 +62,14 @@ class customacme implements CertInterface
|
||||
|
||||
$dnsList = [];
|
||||
if (!empty($order['challenges'])) {
|
||||
$keys = [];
|
||||
foreach ($order['challenges'] as $opts) {
|
||||
$key = $opts['key'] . '|' .$opts['value'];
|
||||
if (in_array($key, $keys)) continue;
|
||||
$mainDomain = getMainDomain($opts['domain']);
|
||||
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,13 +59,17 @@ class litessl implements CertInterface
|
||||
|
||||
$dnsList = [];
|
||||
if (!empty($order['challenges'])) {
|
||||
$keys = [];
|
||||
foreach ($order['challenges'] as $opts) {
|
||||
$key = $opts['key'] . '|' .$opts['value'];
|
||||
if (in_array($key, $keys)) continue;
|
||||
$mainDomain = getMainDomain($opts['domain']);
|
||||
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
|
||||
/*if (!array_key_exists($mainDomain, $dnsList)) {
|
||||
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "litessl.cn"'];
|
||||
}*/
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
163
app/lib/deploy/acepanel.php
Normal file
163
app/lib/deploy/acepanel.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class acepanel implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $id;
|
||||
private $token;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/');
|
||||
$this->id = $config['id'];
|
||||
$this->token = $config['token'];
|
||||
$this->proxy = $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->id) || empty($this->token)) throw new Exception('请填写完整面板地址和访问令牌');
|
||||
|
||||
$response = $this->request('/user/info', null, 'GET');
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['msg']) && $result['msg'] == "success") {
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception($result['msg'] ?? '面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if ($config['type'] == '1') {
|
||||
$this->deployPanel($fullchain, $privatekey);
|
||||
$this->log("面板证书部署成功");
|
||||
return;
|
||||
}
|
||||
$sites = explode("\n", $config['sites']);
|
||||
$success = 0;
|
||||
$errmsg = null;
|
||||
foreach ($sites as $site) {
|
||||
$site = trim($site);
|
||||
if (empty($site)) continue;
|
||||
try {
|
||||
$this->deploySite($site, $fullchain, $privatekey);
|
||||
$this->log("网站 {$site} 证书部署成功");
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("网站 {$site} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
}
|
||||
if ($success == 0) {
|
||||
throw new Exception($errmsg ?: '要部署的网站不存在');
|
||||
}
|
||||
}
|
||||
|
||||
private function deployPanel($fullchain, $privatekey)
|
||||
{
|
||||
$data = [
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
$response = $this->request('/setting/cert', $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['msg']) && $result['msg'] == "success") {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ?: '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploySite($name, $fullchain, $privatekey)
|
||||
{
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
$response = $this->request('/website/cert', $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['msg']) && $result['msg'] == "success") {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ?: '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function request($path, $params, $method = 'POST')
|
||||
{
|
||||
$url = $this->url . '/api' . $path;
|
||||
$body = $method == 'GET' ? null : json_encode($params);
|
||||
$sign = $this->signRequest($method, $url, $body, $this->id, $this->token);
|
||||
$response = http_request($url, $body, null, null, [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Timestamp' => $sign['timestamp'],
|
||||
'Authorization' => 'HMAC-SHA256 Credential=' . $sign['id'] . ', Signature=' . $sign['signature']
|
||||
], $this->proxy, $method);
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
private function signRequest($method, $url, $body, $id, $token)
|
||||
{
|
||||
// 解析URL并获取路径
|
||||
$parsedUrl = parse_url($url);
|
||||
$path = $parsedUrl['path'];
|
||||
$query = $parsedUrl['query'] ?? '';
|
||||
|
||||
// 规范化路径
|
||||
$canonicalPath = $path;
|
||||
if (!str_starts_with($path, '/api')) {
|
||||
$apiPos = strpos($path, '/api');
|
||||
if ($apiPos !== false) {
|
||||
$canonicalPath = substr($path, $apiPos);
|
||||
}
|
||||
}
|
||||
|
||||
// 构造规范化请求
|
||||
$canonicalRequest = implode("\n", [
|
||||
$method,
|
||||
$canonicalPath,
|
||||
$query,
|
||||
hash('sha256', $body ?: '')
|
||||
]);
|
||||
|
||||
// 计算签名
|
||||
$timestamp = time();
|
||||
$stringToSign = implode("\n", [
|
||||
'HMAC-SHA256',
|
||||
$timestamp,
|
||||
hash('sha256', $canonicalRequest)
|
||||
]);
|
||||
$signature = hash_hmac('sha256', $stringToSign, $token);
|
||||
|
||||
return [
|
||||
'timestamp' => $timestamp,
|
||||
'signature' => $signature,
|
||||
'id' => $id
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ class aliyun implements DeployInterface
|
||||
$this->deploy_alb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'nlb') {
|
||||
$this->deploy_nlb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'esa_saas') {
|
||||
$this->deploy_esa_saas($cert_id, $config);
|
||||
} elseif ($config['product'] == 'ga') {
|
||||
$this->deploy_ga($cert_id, $config);
|
||||
} elseif ($config['product'] == 'upload') {
|
||||
} else {
|
||||
throw new Exception('未知的产品类型');
|
||||
@@ -132,36 +136,98 @@ class aliyun implements DeployInterface
|
||||
|
||||
private function deploy_cdn($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('CDN绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('CDN绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cdn.aliyuncs.com', '2018-05-10', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetCdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetCdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_dcdn($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('DCDN绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('DCDN绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15', $this->proxy);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetDcdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('DCDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_esa_saas($cas_id, $config)
|
||||
{
|
||||
$sitename = $config['esa_sitename'];
|
||||
$saas_sitename = $config['esa_saas_sitename'];
|
||||
if (empty($sitename)) throw new Exception('ESA站点名称不能为空');
|
||||
if (empty($saas_sitename)) throw new Exception('ESA SAAS域名不能为空');
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$endpoint = 'esa.ap-southeast-1.aliyuncs.com';
|
||||
} else {
|
||||
$endpoint = 'esa.cn-hangzhou.aliyuncs.com';
|
||||
}
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2024-09-10');
|
||||
$param = [
|
||||
'Action' => 'ListSites',
|
||||
'SiteName' => $sitename,
|
||||
'SiteSearchType' => 'exact',
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('DCDN域名 ' . $domain . ' 部署证书成功!');
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA站点列表失败:' . $e->getMessage());
|
||||
}
|
||||
if ($data['TotalCount'] == 0) throw new Exception('ESA站点 ' . $sitename . ' 不存在');
|
||||
$this->log('成功查询到' . $data['TotalCount'] . '个ESA站点');
|
||||
$site_id = $data['Sites'][0]['SiteId'];
|
||||
// 查询对应的saas域名
|
||||
$param = [
|
||||
'Action' => 'ListCustomHostnames',
|
||||
'SiteName' => $saas_sitename,
|
||||
'SiteId' => $site_id,
|
||||
'SiteSearchType' => 'exact',
|
||||
];
|
||||
try {
|
||||
$saas_data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA saas域名失败:' . $e->getMessage());
|
||||
}
|
||||
if ($saas_data['TotalCount'] == 0) throw new Exception('ESA saas站点 ' . $saas_sitename . ' 不存在');
|
||||
$saas_hostname_id = $saas_data['Hostnames'][0]['HostnameId'];
|
||||
|
||||
$param = [
|
||||
'Action' => 'UpdateCustomHostname',
|
||||
'HostnameId' => $saas_hostname_id,
|
||||
'SslFlag' => 'on',
|
||||
'CertType' => 'cas',
|
||||
'CasId' => $cas_id,
|
||||
'CasRegion' => $config['region'],
|
||||
];
|
||||
$this->log('ESA SAAS站点部署参数 ' . json_encode($param));
|
||||
try {
|
||||
$saas_deploy_result = $client->request($param);
|
||||
$this->log('ESA SAAS站点部署结果 ' . json_encode($saas_deploy_result));
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('部署失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('ESA SAAS站点 ' . $saas_sitename . ' 证书添加成功!');
|
||||
}
|
||||
|
||||
private function deploy_esa($cas_id, $cert_name, $config)
|
||||
@@ -201,11 +267,11 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
$this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书');
|
||||
|
||||
$exist_cert_id = null;
|
||||
$exist_cert_name = null;
|
||||
$exist_cert_casid = null;
|
||||
$exist_cert = null;
|
||||
$oldest_cert = null;
|
||||
if ($data['TotalCount'] > 0) {
|
||||
foreach ($data['Result'] as $cert) {
|
||||
if ($cert['Type'] == 'free') continue;
|
||||
$domains = explode(',', $cert['SAN']);
|
||||
$flag = true;
|
||||
foreach ($domains as $domain) {
|
||||
@@ -215,11 +281,40 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
if ($flag) {
|
||||
$exist_cert_id = $cert['Id'];
|
||||
$exist_cert_name = $cert['Name'];
|
||||
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
|
||||
$exist_cert = $cert;
|
||||
break;
|
||||
}
|
||||
if (!$oldest_cert) {
|
||||
$oldest_cert = $cert;
|
||||
} elseif (strtotime($cert['CreateTime']) < strtotime($oldest_cert['CreateTime'])) {
|
||||
$oldest_cert = $cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$exist_cert) { //新增证书时,若配额已满,则删除最旧的证书
|
||||
$param = [
|
||||
'Action' => 'ListInstanceQuotasWithUsage',
|
||||
'SiteId' => $site_id,
|
||||
'QuotaNames' => 'customHttpCert',
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA站点证书配额失败:' . $e->getMessage());
|
||||
}
|
||||
if (!empty($data['Quotas']) && intval($data['Quotas'][0]['Usage']) >= intval($data['Quotas'][0]['QuotaValue']) && $oldest_cert) {
|
||||
$param = [
|
||||
'Action' => 'DeleteCertificate',
|
||||
'SiteId' => $site_id,
|
||||
'Id' => $oldest_cert['Id'],
|
||||
];
|
||||
try {
|
||||
$client->request($param, 'GET');
|
||||
$this->log('ESA站点 ' . $sitename . ' 删除证书 ' . $oldest_cert['Name'] . ' 成功');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('ESA站点 ' . $sitename . ' 删除证书' . $oldest_cert['Name'] . '失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +327,10 @@ class aliyun implements DeployInterface
|
||||
'Region' => $config['region'],
|
||||
];
|
||||
|
||||
if ($exist_cert_id) {
|
||||
$param['Id'] = $exist_cert_id;
|
||||
if ($exist_cert) {
|
||||
$param['Id'] = $exist_cert['Id'];
|
||||
|
||||
if ($exist_cert_casid == $cas_id) {
|
||||
if (isset($exist_cert['CasId']) && $exist_cert['CasId'] == $cas_id) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
@@ -243,8 +338,8 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client->request($param);
|
||||
|
||||
if ($exist_cert_name) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功');
|
||||
if ($exist_cert) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert['Name'] . ' 更新成功');
|
||||
} else {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书添加成功!');
|
||||
}
|
||||
@@ -256,14 +351,16 @@ class aliyun implements DeployInterface
|
||||
if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空');
|
||||
if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空');
|
||||
$client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']);
|
||||
$client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou');
|
||||
$this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$client->addBucketCnameCert($config['oss_bucket'], $domain, $cert_id . '-cn-hangzhou');
|
||||
$this->log('OSS域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_waf($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('WAF绑定域名不能为空');
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$cert_id .= '-ap-southeast-1';
|
||||
@@ -288,62 +385,63 @@ class aliyun implements DeployInterface
|
||||
$instance_id = $data['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
$param = [
|
||||
'Action' => 'DescribeDomainDetail',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询CNAME接入详情失败:' . $e->getMessage());
|
||||
}
|
||||
if (!isset($data['Listen'])) {
|
||||
throw new Exception('没有找到' . $domain . '监听器');
|
||||
}
|
||||
|
||||
if (isset($data['Listen']['CertId'])) {
|
||||
$old_cert_id = $data['Listen']['CertId'];
|
||||
if (!empty($old_cert_id) && $old_cert_id == $cert_id) {
|
||||
$this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$data['Listen']['CertId'] = $cert_id;
|
||||
if (empty($data['Listen']['HttpsPorts'])) {
|
||||
$data['Listen']['HttpsPorts'] = [443];
|
||||
$data['Listen']['TLSVersion'] = 'tlsv1.1';
|
||||
$data['Listen']['EnableTLSv3'] = true;
|
||||
$data['Listen']['CipherSuite'] = 1;
|
||||
}
|
||||
if (count($data['Redirect']['BackendPorts']) == 1 && $data['Redirect']['BackendPorts'][0]['Protocol'] == 'http') {
|
||||
$data['Redirect']['BackendPorts'][] = [
|
||||
'ListenPort' => 443,
|
||||
'Protocol' => 'https',
|
||||
'BackendPort' => $data['Redirect']['BackendPorts'][0]['BackendPort'],
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'DescribeDomainDetail',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$data['Redirect']['FocusHttpBackend'] = true;
|
||||
}
|
||||
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
|
||||
$param = [
|
||||
'Action' => 'ModifyDomain',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'Listen' => json_encode($data['Listen']),
|
||||
'Redirect' => json_encode($data['Redirect']),
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$data = $client->request($param);
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询CNAME接入详情失败:' . $e->getMessage());
|
||||
}
|
||||
if (!isset($data['Listen'])) {
|
||||
throw new Exception('没有找到' . $domain . '监听器');
|
||||
}
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
if (isset($data['Listen']['CertId'])) {
|
||||
$old_cert_id = $data['Listen']['CertId'];
|
||||
if (!empty($old_cert_id) && $old_cert_id == $cert_id) {
|
||||
$this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$data['Listen']['CertId'] = $cert_id;
|
||||
if (empty($data['Listen']['HttpsPorts'])) {
|
||||
$data['Listen']['HttpsPorts'] = [443];
|
||||
$data['Listen']['TLSVersion'] = 'tlsv1.1';
|
||||
$data['Listen']['EnableTLSv3'] = true;
|
||||
$data['Listen']['CipherSuite'] = 1;
|
||||
}
|
||||
if (count($data['Redirect']['BackendPorts']) == 1 && $data['Redirect']['BackendPorts'][0]['Protocol'] == 'http') {
|
||||
$data['Redirect']['BackendPorts'][] = [
|
||||
'ListenPort' => 443,
|
||||
'Protocol' => 'https',
|
||||
'BackendPort' => $data['Redirect']['BackendPorts'][0]['BackendPort'],
|
||||
];
|
||||
$data['Redirect']['FocusHttpBackend'] = true;
|
||||
}
|
||||
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
|
||||
$param = [
|
||||
'Action' => 'ModifyDomain',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'Listen' => json_encode($data['Listen']),
|
||||
'Redirect' => json_encode($data['Redirect']),
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$data = $client->request($param);
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_waf2($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('WAF绑定域名不能为空');
|
||||
|
||||
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
@@ -362,23 +460,24 @@ class aliyun implements DeployInterface
|
||||
$instance_id = $data['InstanceInfo']['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
$param = [
|
||||
'Action' => 'CreateCertificateByCertificateId',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'CreateCertificateByCertificateId',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_api($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$groupid = $config['api_groupid'];
|
||||
if (empty($groupid)) throw new Exception('API分组ID不能为空');
|
||||
if (empty($domain)) throw new Exception('API分组绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('API分组绑定域名不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
@@ -388,76 +487,80 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2016-07-14', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'SetDomainCertificate',
|
||||
'GroupId' => $groupid,
|
||||
'DomainName' => $domain,
|
||||
'CertificateName' => $cert_name,
|
||||
'CertificateBody' => $fullchain,
|
||||
'CertificatePrivateKey' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetDomainCertificate',
|
||||
'GroupId' => $groupid,
|
||||
'DomainName' => $domain,
|
||||
'CertificateName' => $cert_name,
|
||||
'CertificateBody' => $fullchain,
|
||||
'CertificatePrivateKey' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('API网关域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('API网关域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_ddoscoo($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('绑定域名不能为空');
|
||||
|
||||
$endpoint = 'ddoscoo.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-01-01', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'AssociateWebCert',
|
||||
'Domain' => $domain,
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'AssociateWebCert',
|
||||
'Domain' => $domain,
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_live($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('视频直播绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('视频直播绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'live.aliyuncs.com', '2016-11-01', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetLiveDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('设置视频直播域名 ' . $domain . ' 证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetLiveDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('设置视频直播域名 ' . $domain . ' 证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_vod($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('视频点播绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('视频点播绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'vod.cn-shanghai.aliyuncs.com', '2017-03-21', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetVodDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'SSLProtocol' => 'on',
|
||||
'SSLPub' => $fullchain,
|
||||
'SSLPri' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('视频点播域名 ' . $domain . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetVodDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'SSLProtocol' => 'on',
|
||||
'SSLPub' => $fullchain,
|
||||
'SSLPri' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('视频点播域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_fc($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$fc_cname = $config['fc_cname'];
|
||||
if (empty($domain)) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
@@ -466,41 +569,42 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2023-03-30', $this->proxy);
|
||||
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'authConfig' => $data['authConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'authConfig' => $data['authConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_fc2($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$fc_cname = $config['fc_cname'];
|
||||
if (empty($domain)) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
@@ -509,33 +613,35 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2021-04-06', $this->proxy);
|
||||
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_clb($cert_id, $cert_name, $config)
|
||||
@@ -735,6 +841,84 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_ga($cert_id, $config)
|
||||
{
|
||||
if (empty($config['ga_id'])) throw new Exception('全球加速实例ID不能为空');
|
||||
if (empty($config['ga_listener_id'])) throw new Exception('全球加速监听ID不能为空');
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'ga.cn-hangzhou.aliyuncs.com', '2019-11-20', $this->proxy);
|
||||
$cert_id = $cert_id . '-cn-hangzhou';
|
||||
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
|
||||
|
||||
if ($deploy_type == 1) {
|
||||
if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空');
|
||||
$param = [
|
||||
'Action' => 'ListListenerCertificates',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('扩展域名列表查询失败:' . $e->getMessage());
|
||||
}
|
||||
$need_add = [];
|
||||
foreach (explode(',', $config['clb_domain']) as $domain) {
|
||||
$domainExists = false;
|
||||
$exist_cert_id = null;
|
||||
foreach ($data['Certificates'] as $cert) {
|
||||
if (isset($cert['Domain']) && $domain == $cert['Domain']) {
|
||||
$domainExists = true;
|
||||
$exist_cert_id = $cert['CertificateId'];
|
||||
}
|
||||
}
|
||||
if ($domainExists) {
|
||||
if ($exist_cert_id == $cert_id) {
|
||||
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 证书已配置');
|
||||
continue;
|
||||
}
|
||||
$param = [
|
||||
'Action' => 'UpdateAdditionalCertificateWithListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 替换证书成功!');
|
||||
} else {
|
||||
$need_add[] = $domain;
|
||||
}
|
||||
}
|
||||
if (count($need_add) > 0) {
|
||||
$param = [
|
||||
'Action' => 'AssociateAdditionalCertificatesWithListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
];
|
||||
foreach ($need_add as $index => $domain) {
|
||||
$param['Certificates.' . ($index + 1) . '.Id'] = $cert_id;
|
||||
$param['Certificates.' . ($index + 1) . '.Domain'] = $domain;
|
||||
}
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听扩展域名 ' . implode(',', $need_add) . ' 绑定证书成功!');
|
||||
}
|
||||
} else {
|
||||
$param = [
|
||||
'Action' => 'UpdateListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
'Certificates.1.Id' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听默认证书更新成功!');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
|
||||
108
app/lib/deploy/amh.php
Normal file
108
app/lib/deploy/amh.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class amh implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $apikey;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/');
|
||||
$this->apikey = $config['apikey'];
|
||||
$this->proxy = $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥');
|
||||
$this->login();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function login()
|
||||
{
|
||||
$path = '/?c=amapi&a=login';
|
||||
$post_data = 'amapi_expires=' . time() + 120;
|
||||
$post_data .= '&amapi_sign=' . hash_hmac('sha256', $post_data, $this->apikey);
|
||||
$response = $this->request($path, $post_data);
|
||||
if ($response['code'] == 302 && strpos($response['redirect_url'], 'amh_token=') !== false) {
|
||||
if(preg_match('/amh_token=([A-Za-z0-9]+)/', $response['redirect_url'], $matches)) {
|
||||
return $matches[1];
|
||||
}else{
|
||||
throw new Exception('面板返回数据异常');
|
||||
}
|
||||
} elseif ($response['code'] == 200 && preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
|
||||
throw new Exception(strip_tags($matches[1]));
|
||||
} else {
|
||||
throw new Exception('面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if (empty($config['env_name'])) throw new Exception('环境名称不能为空');
|
||||
if (empty($config['vhost_name'])) throw new Exception('网站标识域名不能为空');
|
||||
|
||||
$amh_token = $this->login();
|
||||
|
||||
foreach (explode("\n", $config['vhost_name']) as $vhost_name) {
|
||||
$vhost_name = trim($vhost_name);
|
||||
if (empty($vhost_name)) continue;
|
||||
|
||||
$path = '/?c=amssl&a=admin_amssl&envs_name=' . $config['env_name'] . '&vhost_name=' . $vhost_name . '&ModuleSort=app';
|
||||
$params = [
|
||||
'submit_key_crt' => 'y',
|
||||
'key_input1' => 'key_input1',
|
||||
'key_content1' => $privatekey,
|
||||
'crt_input1' => 'crt_input1',
|
||||
'crt_content1' => $fullchain,
|
||||
'amh_token' => $amh_token,
|
||||
];
|
||||
$response = $this->request($path, $params);
|
||||
if (strpos($response['body'], '<p id="success"') !== false) {
|
||||
$this->log("网站 {$vhost_name} 证书部署成功");
|
||||
} elseif (preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
|
||||
$errmsg = strip_tags($matches[1]);
|
||||
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
|
||||
throw new Exception($errmsg);
|
||||
} elseif (preg_match('/<p id="error".*?>(.*?)<br \/>/s', $response['body'], $matches)) {
|
||||
$errmsg = $matches[1];
|
||||
if (strpos($errmsg, '<br />') !== false) {
|
||||
$errmsg = explode('<br />', $errmsg)[0];
|
||||
}
|
||||
$errmsg = strip_tags($errmsg);
|
||||
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
|
||||
throw new Exception($errmsg);
|
||||
} else {
|
||||
throw new Exception("网站 {$vhost_name} 证书部署失败:未知错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function request($path, $post_data = null)
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
$cookie = 'PHPSESSID=' . hash_hmac('md5', 'php_sessid=' . $this->apikey, $this->apikey);
|
||||
$response = http_request($url, $post_data, null, $cookie, null, $this->proxy);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,12 @@ class btpanel implements DeployInterface
|
||||
private $logger;
|
||||
private $url;
|
||||
private $key;
|
||||
private $version;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/');
|
||||
$this->key = $config['key'];
|
||||
$this->version = isset($config['version']) ? intval($config['version']) : 0;
|
||||
$this->proxy = $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
@@ -26,24 +24,13 @@ class btpanel implements DeployInterface
|
||||
{
|
||||
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
|
||||
|
||||
if ($this->version == 1) {
|
||||
$path = '/config/get_config';
|
||||
$response = $this->request($path, []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['panel']['status']) && $result['panel']['status']) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
$path = '/config?action=get_config';
|
||||
$response = $this->request($path, []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && ($result['status'] == 1 || isset($result['sites_path']))) {
|
||||
return true;
|
||||
} else {
|
||||
$path = '/config?action=get_config';
|
||||
$response = $this->request($path, []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && ($result['status'] == 1 || isset($result['sites_path']))) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,46 +42,22 @@ class btpanel implements DeployInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$isIIS = $config['type'] == '0' && $this->version == 1 && isset($config['is_iis']) && $config['is_iis'] == '1';
|
||||
if ($isIIS) {
|
||||
$response = $this->request('/panel/get_config', []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['paths']['soft'])) {
|
||||
if ($result['config']['webserver'] != 'iis') {
|
||||
throw new Exception('当前安装的Web服务器不是IIS');
|
||||
}
|
||||
$panel_path = $result['paths']['soft'];
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
|
||||
$pfx_dir = $panel_path . '/temp/ssl/' . getMillisecond();
|
||||
$pfx_path = $pfx_dir . '/cert.pfx';
|
||||
$pfx_password = '123456';
|
||||
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_password);
|
||||
$data = [
|
||||
['name' => 'path', 'contents' => $pfx_dir],
|
||||
['name' => 'filename', 'contents' => 'cert.pfx'],
|
||||
['name' => 'size', 'contents' => strlen($pfx)],
|
||||
['name' => 'start', 'contents' => '0'],
|
||||
['name' => 'blob', 'filename' => 'cert.pfx', 'contents' => $pfx],
|
||||
['name' => 'force', 'contents' => 'true'],
|
||||
];
|
||||
$response = $this->request('/files/upload', $data, true);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
$sites = explode("\n", $config['sites']);
|
||||
$success = 0;
|
||||
$errmsg = null;
|
||||
foreach ($sites as $site) {
|
||||
$siteName = trim($site);
|
||||
if (empty($siteName)) continue;
|
||||
if ($config['type'] == '3') {
|
||||
if ($config['type'] == '4') {
|
||||
try {
|
||||
$this->deployProxy($siteName, $fullchain, $privatekey);
|
||||
$this->log("反向代理站点 {$siteName} 证书部署成功");
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("反向代理站点 {$siteName} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
} elseif ($config['type'] == '3') {
|
||||
try {
|
||||
$this->deployDocker($siteName, $fullchain, $privatekey);
|
||||
$this->log("Docker域名 {$siteName} 证书部署成功");
|
||||
@@ -112,15 +75,6 @@ class btpanel implements DeployInterface
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("邮局域名 {$siteName} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
} elseif ($isIIS) {
|
||||
try {
|
||||
$this->deployIISSite($siteName, $pfx_path, $pfx_password);
|
||||
$this->log("域名 {$siteName} 证书部署成功");
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("域名 {$siteName} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->deploySite($siteName, $fullchain, $privatekey);
|
||||
@@ -139,113 +93,30 @@ class btpanel implements DeployInterface
|
||||
|
||||
private function deployPanel($fullchain, $privatekey)
|
||||
{
|
||||
if ($this->version == 1) {
|
||||
$path = '/config/set_panel_ssl';
|
||||
$data = [
|
||||
'ssl_key' => $privatekey,
|
||||
'ssl_pem' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
$path = '/config?action=SavePanelSSL';
|
||||
$data = [
|
||||
'privateKey' => $privatekey,
|
||||
'certPem' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
$path = '/config?action=SavePanelSSL';
|
||||
$data = [
|
||||
'privateKey' => $privatekey,
|
||||
'certPem' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploySite($siteName, $fullchain, $privatekey)
|
||||
{
|
||||
if ($this->version == 1) {
|
||||
$path = '/datalist/get_data_list';
|
||||
$data = [
|
||||
'table' => 'sites',
|
||||
'search_type' => 'PHP',
|
||||
'search' => $siteName,
|
||||
'p' => 1,
|
||||
'limit' => 10,
|
||||
'type' => -1,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['data'])) {
|
||||
if (empty($result['data'])) throw new Exception("网站 {$siteName} 不存在");
|
||||
$siteId = null;
|
||||
foreach ($result['data'] as $item) {
|
||||
if ($item['name'] == $siteName) {
|
||||
$siteId = $item['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($siteId)) throw new Exception("网站 {$siteName} 不存在");
|
||||
$path = '/site/set_site_ssl';
|
||||
$data = [
|
||||
'siteid' => $siteId,
|
||||
'status' => 'true',
|
||||
'sslType' => '',
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
} else {
|
||||
$path = '/site?action=SetSSL';
|
||||
$data = [
|
||||
'type' => '0',
|
||||
'siteName' => $siteName,
|
||||
'key' => $privatekey,
|
||||
'csr' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deployIISSite($domain, $pfx_path, $password = '123456')
|
||||
{
|
||||
$path = '/site/set_site_domain_ssl';
|
||||
$path = '/site?action=SetSSL';
|
||||
$data = [
|
||||
'domain' => $domain,
|
||||
'path' => $pfx_path,
|
||||
'password' => $password,
|
||||
'type' => '0',
|
||||
'siteName' => $siteName,
|
||||
'key' => $privatekey,
|
||||
'csr' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
@@ -297,6 +168,25 @@ class btpanel implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function deployProxy($domain, $fullchain, $privatekey)
|
||||
{
|
||||
$path = '/mod/proxy/com/set_ssl';
|
||||
$data = [
|
||||
'site_name' => $domain,
|
||||
'key' => $privatekey,
|
||||
'csr' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
|
||||
229
app/lib/deploy/btwin.php
Normal file
229
app/lib/deploy/btwin.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use app\lib\CertHelper;
|
||||
use Exception;
|
||||
|
||||
class btwin implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $key;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/');
|
||||
$this->key = $config['key'];
|
||||
$this->proxy = $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
|
||||
|
||||
$path = '/config/get_config';
|
||||
$response = $this->request($path, []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['panel']['status']) && $result['panel']['status']) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if ($config['type'] == '1') {
|
||||
$this->deployPanel($fullchain, $privatekey);
|
||||
$this->log("面板证书部署成功");
|
||||
return;
|
||||
}
|
||||
|
||||
$isIIS = $config['type'] == '0' && isset($config['is_iis']) && $config['is_iis'] == '1';
|
||||
if ($isIIS) {
|
||||
$response = $this->request('/panel/get_config', []);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['paths']['soft'])) {
|
||||
if ($result['config']['webserver'] != 'iis') {
|
||||
throw new Exception('当前安装的Web服务器不是IIS');
|
||||
}
|
||||
$panel_path = $result['paths']['soft'];
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
|
||||
$pfx_dir = $panel_path . '/temp/ssl/' . getMillisecond();
|
||||
$pfx_path = $pfx_dir . '/cert.pfx';
|
||||
$pfx_password = '123456';
|
||||
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_password);
|
||||
$data = [
|
||||
['name' => 'path', 'contents' => $pfx_dir],
|
||||
['name' => 'filename', 'contents' => 'cert.pfx'],
|
||||
['name' => 'size', 'contents' => strlen($pfx)],
|
||||
['name' => 'start', 'contents' => '0'],
|
||||
['name' => 'blob', 'filename' => 'cert.pfx', 'contents' => $pfx],
|
||||
['name' => 'force', 'contents' => 'true'],
|
||||
];
|
||||
$response = $this->request('/files/upload', $data, true);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
} else {
|
||||
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
$sites = explode("\n", $config['sites']);
|
||||
$success = 0;
|
||||
$errmsg = null;
|
||||
foreach ($sites as $site) {
|
||||
$siteName = trim($site);
|
||||
if (empty($siteName)) continue;
|
||||
if ($isIIS) {
|
||||
try {
|
||||
$this->deployIISSite($siteName, $pfx_path, $pfx_password);
|
||||
$this->log("域名 {$siteName} 证书部署成功");
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("域名 {$siteName} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->deploySite($siteName, $fullchain, $privatekey);
|
||||
$this->log("网站 {$siteName} 证书部署成功");
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("网站 {$siteName} 证书部署失败:" . $errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($success == 0) {
|
||||
throw new Exception($errmsg ? $errmsg : '要部署的网站不存在');
|
||||
}
|
||||
}
|
||||
|
||||
private function deployPanel($fullchain, $privatekey)
|
||||
{
|
||||
$path = '/config/set_panel_ssl';
|
||||
$data = [
|
||||
'ssl_key' => $privatekey,
|
||||
'ssl_pem' => $fullchain,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploySite($siteName, $fullchain, $privatekey)
|
||||
{
|
||||
$path = '/datalist/get_data_list';
|
||||
$data = [
|
||||
'table' => 'sites',
|
||||
'search_type' => 'PHP',
|
||||
'search' => $siteName,
|
||||
'p' => 1,
|
||||
'limit' => 10,
|
||||
'type' => -1,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['data'])) {
|
||||
if (empty($result['data'])) throw new Exception("网站 {$siteName} 不存在");
|
||||
$siteId = null;
|
||||
foreach ($result['data'] as $item) {
|
||||
if ($item['name'] == $siteName) {
|
||||
$siteId = $item['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($siteId)) throw new Exception("网站 {$siteName} 不存在");
|
||||
$path = '/site/set_site_ssl';
|
||||
$data = [
|
||||
'siteid' => $siteId,
|
||||
'status' => 'true',
|
||||
'sslType' => '',
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
private function deployIISSite($domain, $pfx_path, $password = '123456')
|
||||
{
|
||||
$path = '/site/set_site_domain_ssl';
|
||||
$data = [
|
||||
'domain' => $domain,
|
||||
'path' => $pfx_path,
|
||||
'password' => $password,
|
||||
];
|
||||
$response = $this->request($path, $data);
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['status']) && $result['status']) {
|
||||
return true;
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception($result['msg']);
|
||||
} else {
|
||||
throw new Exception($response ? $response : '返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function request($path, $params, $file = false)
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
|
||||
$now_time = time();
|
||||
$headers = [];
|
||||
if ($file) {
|
||||
$post_data = [
|
||||
['name' => 'request_token', 'contents' => md5($now_time . md5($this->key))],
|
||||
['name' => 'request_time', 'contents' => $now_time],
|
||||
];
|
||||
$post_data = array_merge($post_data, $params);
|
||||
$headers['Content-Type'] = 'multipart/form-data';
|
||||
} else {
|
||||
$post_data = [
|
||||
'request_token' => md5($now_time . md5($this->key)),
|
||||
'request_time' => $now_time
|
||||
];
|
||||
$post_data = array_merge($post_data, $params);
|
||||
}
|
||||
$response = http_request($url, $post_data, null, null, $headers, $this->proxy);
|
||||
return $response['body'];
|
||||
}
|
||||
}
|
||||
@@ -63,20 +63,23 @@ class ctyun implements DeployInterface
|
||||
}
|
||||
$this->log('上传证书成功 cert_name=' . $config['cert_name']);
|
||||
|
||||
$param = [
|
||||
'domain' => $config['domain'],
|
||||
'https_status' => 'on',
|
||||
'cert_name' => $config['cert_name'],
|
||||
];
|
||||
try {
|
||||
$client->request('POST', '/v1/domain/update-domain', null, $param);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$param = [
|
||||
'domain' => $domain,
|
||||
'https_status' => 'on',
|
||||
'cert_name' => $config['cert_name'],
|
||||
];
|
||||
try {
|
||||
$client->request('POST', '/v1/domain/update-domain', null, $param);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_icdn($fullchain, $privatekey, $config)
|
||||
@@ -98,20 +101,23 @@ class ctyun implements DeployInterface
|
||||
}
|
||||
$this->log('上传证书成功 cert_name=' . $config['cert_name']);
|
||||
|
||||
$param = [
|
||||
'domain' => $config['domain'],
|
||||
'https_status' => 'on',
|
||||
'cert_name' => $config['cert_name'],
|
||||
];
|
||||
try {
|
||||
$client->request('POST', '/v1/domain/update-domain', null, $param);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$param = [
|
||||
'domain' => $domain,
|
||||
'https_status' => 'on',
|
||||
'cert_name' => $config['cert_name'],
|
||||
];
|
||||
try {
|
||||
$client->request('POST', '/v1/domain/update-domain', null, $param);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_accessone($fullchain, $privatekey, $config)
|
||||
@@ -133,81 +139,87 @@ class ctyun implements DeployInterface
|
||||
}
|
||||
$this->log('上传证书成功 cert_name=' . $config['cert_name']);
|
||||
|
||||
$param = [
|
||||
'domain' => $config['domain'],
|
||||
'product_code' => '020',
|
||||
];
|
||||
try {
|
||||
$result = $client->request('POST', '/ctapi/v1/accessone/domain/config', null, $param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询域名配置失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($result['https_status'] == 'on' && $result['cert_name'] == $config['cert_name']) {
|
||||
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!');
|
||||
return;
|
||||
}
|
||||
|
||||
$result['https_status'] = 'on';
|
||||
$result['cert_name'] = $config['cert_name'];
|
||||
$exclude_keys = ['status', 'area_scope', 'cname', 'insert_date', 'status_date', 'record_status', 'record_num', 'customer_name', 'outlink_replace_filter', 'website_ipv6_access_mark', 'websocket_speed', 'dynamic_config', 'dynamic_ability'];
|
||||
foreach ($result as $key => $value) {
|
||||
if (in_array($key, $exclude_keys) || is_array($value) && empty($value)) {
|
||||
unset($result[$key]);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$param = [
|
||||
'domain' => $domain,
|
||||
'product_code' => '020',
|
||||
];
|
||||
try {
|
||||
$result = $client->request('POST', '/ctapi/v1/accessone/domain/config', null, $param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询域名配置失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
if (isset($result['origin'])) {
|
||||
foreach ($result['origin'] as &$origin) {
|
||||
$origin['weight'] = strval($origin['weight']);
|
||||
}
|
||||
}
|
||||
try {
|
||||
$client->request('POST', '/ctapi/v1/scdn/domain/modify_config', null, $result);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
if ($result['https_status'] == 'on' && $result['cert_name'] == $config['cert_name']) {
|
||||
$this->log('边缘安全加速域名 ' . $domain . ' 证书已部署,无需重复操作!');
|
||||
return;
|
||||
}
|
||||
|
||||
$result['https_status'] = 'on';
|
||||
$result['cert_name'] = $config['cert_name'];
|
||||
$exclude_keys = ['status', 'area_scope', 'cname', 'insert_date', 'status_date', 'record_status', 'record_num', 'customer_name', 'outlink_replace_filter', 'website_ipv6_access_mark', 'websocket_speed', 'dynamic_config', 'dynamic_ability'];
|
||||
foreach ($result as $key => $value) {
|
||||
if (in_array($key, $exclude_keys) || is_array($value) && empty($value)) {
|
||||
unset($result[$key]);
|
||||
}
|
||||
}
|
||||
if (isset($result['origin'])) {
|
||||
foreach ($result['origin'] as &$origin) {
|
||||
$origin['weight'] = strval($origin['weight']);
|
||||
}
|
||||
}
|
||||
try {
|
||||
$client->request('POST', '/ctapi/v1/scdn/domain/modify_config', null, $result);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('边缘安全加速域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_cf($fullchain, $privatekey, $config)
|
||||
{
|
||||
$client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cf-global.ctapi.ctyun.cn', $this->proxy);
|
||||
try {
|
||||
$data = $client->request('GET', '/openapi/v1/domains/customdomains/' . $config['domain'], null, null, ['regionId' => $config['region_id']]);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取自定义域名配置失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && trim($data['certConfig']['certificate']) == trim($fullchain)) {
|
||||
$this->log('函数计算域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$param = [
|
||||
'domainName' => $config['domain'],
|
||||
'description' => $data['description'],
|
||||
'protocol' => $data['protocol'],
|
||||
'certConfig' => [
|
||||
'certName' => 'cert' . substr($config['cert_name'], strpos($config['cert_name'], '-') + 1),
|
||||
'certificate' => $fullchain,
|
||||
'privateKey' => $privatekey,
|
||||
],
|
||||
'authConfig' => $data['authConfig'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
];
|
||||
try {
|
||||
$client->request('PUT', '/openapi/v1/domains/customdomains/' . $config['domain'], null, $param, ['regionId' => $config['region_id']]);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
try {
|
||||
$data = $client->request('GET', '/openapi/v1/domains/customdomains/' . $domain, null, null, ['regionId' => $config['region_id']]);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取自定义域名配置失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('函数计算域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
if (isset($data['certConfig']['certificate']) && trim($data['certConfig']['certificate']) == trim($fullchain)) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已部署,无需重复操作!');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$param = [
|
||||
'domainName' => $domain,
|
||||
'description' => $data['description'],
|
||||
'protocol' => $data['protocol'],
|
||||
'certConfig' => [
|
||||
'certName' => 'cert' . substr($config['cert_name'], strpos($config['cert_name'], '-') + 1),
|
||||
'certificate' => $fullchain,
|
||||
'privateKey' => $privatekey,
|
||||
],
|
||||
'authConfig' => $data['authConfig'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
];
|
||||
try {
|
||||
$client->request('PUT', '/openapi/v1/domains/customdomains/' . $domain, null, $param, ['regionId' => $config['region_id']]);
|
||||
} catch (Exception $e) {
|
||||
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
|
||||
@@ -48,6 +48,8 @@ class huoshan implements DeployInterface
|
||||
$this->deploy_clb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'alb') {
|
||||
$this->deploy_alb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'vod') {
|
||||
$this->deploy_vod($cert_id, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +137,33 @@ class huoshan implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_vod($cert_id, $config)
|
||||
{
|
||||
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
|
||||
if (empty($config['vod_space_name'])) throw new Exception('点播空间名称不能为空');
|
||||
if (empty($config['vod_domain_type'])) throw new Exception('点播域名类型不能为空');
|
||||
|
||||
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'vod.volcengineapi.com', 'vod', '2023-07-01', 'cn-north-1', $this->proxy);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$param = [
|
||||
'SpaceName' => $config['vod_space_name'],
|
||||
'DomainType' => $config['vod_domain_type'],
|
||||
'Domain' => $domain,
|
||||
'Config' => [
|
||||
'HTTPS' => [
|
||||
'Switch' => true,
|
||||
'CertInfo' => [
|
||||
'CertId' => $cert_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$client->request('POST', 'UpdateDomainConfig', $param);
|
||||
$this->log('视频点播域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_imagex($cert_id, $config)
|
||||
{
|
||||
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
|
||||
|
||||
@@ -144,7 +144,7 @@ class opanel implements DeployInterface
|
||||
$domains = $config['domainList'];
|
||||
if (empty($domains)) throw new Exception('没有设置要部署的域名');
|
||||
|
||||
$params = ['page' => 1, 'pageSize' => 500];
|
||||
$params = ['page' => 1, 'pageSize' => 500, 'orderBy' => 'expire_date', 'order' => 'null'];
|
||||
try {
|
||||
$data = $this->request("/websites/ssl/search", $params, $nodeName);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] " : "";
|
||||
|
||||
221
app/lib/deploy/s3storage.php
Normal file
221
app/lib/deploy/s3storage.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class s3storage implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $AccessKeyId;
|
||||
private $SecretAccessKey;
|
||||
private $endpoint;
|
||||
private $region;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->SecretAccessKey = $config['SecretAccessKey'];
|
||||
$this->endpoint = rtrim($config['endpoint'], '/');
|
||||
$this->region = !empty($config['region']) ? $config['region'] : 'us-east-1';
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey) || empty($this->endpoint)) {
|
||||
throw new Exception('必填参数不能为空');
|
||||
}
|
||||
|
||||
$this->s3Request('GET', '/', '', null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
$bucket = $config['bucket'];
|
||||
if (empty($bucket)) throw new Exception('存储桶名称不能为空');
|
||||
|
||||
$certPath = trim($config['cert_path'], '/');
|
||||
$keyPath = trim($config['key_path'], '/');
|
||||
if (empty($certPath) || empty($keyPath)) throw new Exception('证书和私钥保存路径不能为空');
|
||||
|
||||
$this->putObject($bucket, $certPath, $fullchain);
|
||||
$this->log("证书已上传到:s3://{$bucket}/{$certPath}");
|
||||
|
||||
$this->putObject($bucket, $keyPath, $privatekey);
|
||||
$this->log("私钥已上传到:s3://{$bucket}/{$keyPath}");
|
||||
}
|
||||
|
||||
private function putObject($bucket, $key, $content)
|
||||
{
|
||||
$path = '/' . $bucket . '/' . $key;
|
||||
$this->s3Request('PUT', $path, $content, 'application/x-pem-file');
|
||||
}
|
||||
|
||||
private function s3Request($method, $path, $body, $contentType)
|
||||
{
|
||||
$time = time();
|
||||
$date = gmdate("Ymd\THis\Z", $time);
|
||||
$shortDate = gmdate("Ymd", $time);
|
||||
|
||||
$host = preg_replace('#^https?://#', '', $this->endpoint);
|
||||
$scheme = (strpos($this->endpoint, 'https://') === 0) ? 'https' : 'http';
|
||||
if (strpos($this->endpoint, '://') === false) {
|
||||
$scheme = 'https';
|
||||
}
|
||||
|
||||
$payloadHash = hash('sha256', $body ?? '');
|
||||
|
||||
$headers = [
|
||||
'Host' => $host,
|
||||
'X-Amz-Date' => $date,
|
||||
'X-Amz-Content-Sha256' => $payloadHash,
|
||||
];
|
||||
if ($contentType) {
|
||||
$headers['Content-Type'] = $contentType;
|
||||
}
|
||||
|
||||
$authorization = $this->generateSign($method, $path, [], $headers, $body ?? '', $date, $shortDate);
|
||||
$headers['Authorization'] = $authorization;
|
||||
|
||||
$url = $scheme . '://' . $host . $path;
|
||||
|
||||
$headerArr = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
$headerArr[] = $k . ': ' . $v;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
if ($this->proxy) {
|
||||
curl_set_proxy($ch);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArr);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
if ($body !== null && $body !== '') {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
if ($errno) {
|
||||
$errmsg = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception('Curl error: ' . $errmsg);
|
||||
}
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 300) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$errmsg = 'HTTP Code: ' . $httpCode;
|
||||
if ($response) {
|
||||
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
|
||||
$xml = @simplexml_load_string($response);
|
||||
if ($xml && isset($xml->Message)) {
|
||||
$errmsg = (string)$xml->Message;
|
||||
} elseif ($xml && isset($xml->Error->Message)) {
|
||||
$errmsg = (string)$xml->Error->Message;
|
||||
}
|
||||
}
|
||||
throw new Exception($errmsg);
|
||||
}
|
||||
|
||||
private function generateSign($method, $path, $query, $headers, $body, $date, $shortDate)
|
||||
{
|
||||
$algorithm = 'AWS4-HMAC-SHA256';
|
||||
|
||||
$canonicalUri = $this->getCanonicalURI($path);
|
||||
$canonicalQueryString = $this->getCanonicalQueryString($query);
|
||||
[$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers);
|
||||
$hashedPayload = hash('sha256', $body);
|
||||
|
||||
$canonicalRequest = $method . "\n"
|
||||
. $canonicalUri . "\n"
|
||||
. $canonicalQueryString . "\n"
|
||||
. $canonicalHeaders . "\n"
|
||||
. $signedHeaders . "\n"
|
||||
. $hashedPayload;
|
||||
|
||||
$credentialScope = $shortDate . '/' . $this->region . '/s3/aws4_request';
|
||||
$stringToSign = $algorithm . "\n"
|
||||
. $date . "\n"
|
||||
. $credentialScope . "\n"
|
||||
. hash('sha256', $canonicalRequest);
|
||||
|
||||
$kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $this->SecretAccessKey, true);
|
||||
$kRegion = hash_hmac('sha256', $this->region, $kDate, true);
|
||||
$kService = hash_hmac('sha256', 's3', $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
||||
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
||||
|
||||
return $algorithm . ' Credential=' . $this->AccessKeyId . '/' . $credentialScope
|
||||
. ', SignedHeaders=' . $signedHeaders
|
||||
. ', Signature=' . $signature;
|
||||
}
|
||||
|
||||
private function escape($str)
|
||||
{
|
||||
$search = ['+', '*', '%7E'];
|
||||
$replace = ['%20', '%2A', '~'];
|
||||
return str_replace($search, $replace, urlencode($str));
|
||||
}
|
||||
|
||||
private function getCanonicalURI($path)
|
||||
{
|
||||
if (empty($path)) return '/';
|
||||
$parts = explode('/', $path);
|
||||
$parts = array_map(function ($item) {
|
||||
return $this->escape($item);
|
||||
}, $parts);
|
||||
return implode('/', $parts);
|
||||
}
|
||||
|
||||
private function getCanonicalQueryString($parameters)
|
||||
{
|
||||
if (empty($parameters)) return '';
|
||||
ksort($parameters);
|
||||
$pairs = [];
|
||||
foreach ($parameters as $key => $value) {
|
||||
$pairs[] = $this->escape($key) . '=' . $this->escape($value);
|
||||
}
|
||||
return implode('&', $pairs);
|
||||
}
|
||||
|
||||
private function getCanonicalHeaders($oldHeaders)
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($oldHeaders as $key => $value) {
|
||||
$headers[strtolower($key)] = trim($value);
|
||||
}
|
||||
ksort($headers);
|
||||
|
||||
$canonicalHeaders = '';
|
||||
$signedHeaders = '';
|
||||
foreach ($headers as $key => $value) {
|
||||
$canonicalHeaders .= $key . ':' . $value . "\n";
|
||||
$signedHeaders .= $key . ';';
|
||||
}
|
||||
$signedHeaders = substr($signedHeaders, 0, -1);
|
||||
return [$canonicalHeaders, $signedHeaders];
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,14 +159,20 @@ class ssh implements DeployInterface
|
||||
file_put_contents($privateKeyPath, $this->config['privatekey']);
|
||||
file_put_contents($publicKeyPath, $publicKey);
|
||||
umask($umask);
|
||||
if (!empty($this->config['passphrase'])) {
|
||||
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
|
||||
throw new Exception('私钥认证失败');
|
||||
}
|
||||
} else {
|
||||
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
|
||||
throw new Exception('私钥认证失败');
|
||||
|
||||
try {
|
||||
if (!empty($this->config['passphrase'])) {
|
||||
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
|
||||
throw new Exception('私钥认证失败');
|
||||
}
|
||||
} else {
|
||||
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
|
||||
throw new Exception('私钥认证失败');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unlink($publicKeyPath);
|
||||
unlink($privateKeyPath);
|
||||
}
|
||||
} else {
|
||||
if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {
|
||||
|
||||
@@ -25,6 +25,27 @@ class cloudflare implements DnsInterface
|
||||
$this->auth = isset($config['auth']) ? intval($config['auth']) : (preg_match('/^[0-9a-f]+$/i', $this->ApiKey) ? 0 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Cloudflare API 返回的完整域名中提取子域名(主机记录)
|
||||
* 兼容 Emoji/IDN 域名:Cloudflare API 返回 Punycode 格式,数据库存储 UTF-8
|
||||
*/
|
||||
private function extractName($fullName)
|
||||
{
|
||||
$domainAscii = idn_to_ascii($this->domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
|
||||
if ($domainAscii === false) $domainAscii = $this->domain;
|
||||
|
||||
if ($fullName === $domainAscii || $fullName === $this->domain) {
|
||||
return '@';
|
||||
}
|
||||
if (str_ends_with($fullName, '.' . $domainAscii)) {
|
||||
return substr($fullName, 0, -(strlen($domainAscii) + 1));
|
||||
}
|
||||
if (str_ends_with($fullName, '.' . $this->domain)) {
|
||||
return substr($fullName, 0, -(strlen($this->domain) + 1));
|
||||
}
|
||||
return $fullName;
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
@@ -66,8 +87,9 @@ class cloudflare implements DnsInterface
|
||||
if (!isNullOrEmpty($Value)) $KeyWord = $Value;
|
||||
$param = ['type' => $Type, 'search' => $KeyWord, 'page' => $PageNumber, 'per_page' => $PageSize];
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
if ($SubDomain == '@') $SubDomain = $this->domain;
|
||||
else $SubDomain .= '.' . $this->domain;
|
||||
$domainAscii = idn_to_ascii($this->domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) ?: $this->domain;
|
||||
if ($SubDomain == '@') $SubDomain = $domainAscii;
|
||||
else $SubDomain .= '.' . $domainAscii;
|
||||
$param['name'] = $SubDomain;
|
||||
}
|
||||
if (!isNullOrEmpty($Line)) {
|
||||
@@ -77,8 +99,9 @@ class cloudflare implements DnsInterface
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['result'] as $row) {
|
||||
$name = $this->domain == $row['name'] ? '@' : substr($row['name'], 0, -(strlen($this->domain) + 1));
|
||||
$name = $this->extractName($row['name']);
|
||||
$status = str_ends_with($name, '_pause') ? '0' : '1';
|
||||
$name = $name == '__root__' ? '@' : $name;
|
||||
$name = $status == '0' ? substr($name, 0, -6) : $name;
|
||||
if ($row['type'] == 'SRV' && isset($row['priority'])) {
|
||||
$row['content'] = $row['priority'] . ' ' . $row['content'];
|
||||
@@ -114,9 +137,10 @@ class cloudflare implements DnsInterface
|
||||
{
|
||||
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
|
||||
if ($data) {
|
||||
$name = $this->domain == $data['result']['name'] ? '@' : substr($data['result']['name'], 0, -(strlen($this->domain) + 1));
|
||||
$name = $this->extractName($data['result']['name']);
|
||||
$status = str_ends_with($name, '_pause') ? '0' : '1';
|
||||
$name = $status == '0' ? substr($name, 0, -6) : $name;
|
||||
$name = $name == '__root__' ? '@' : $name;
|
||||
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
|
||||
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
|
||||
}
|
||||
@@ -182,6 +206,12 @@ class cloudflare implements DnsInterface
|
||||
{
|
||||
$info = $this->getDomainRecordInfo($RecordId);
|
||||
$Name = $Status == '1' ? str_replace('_pause', '', $info['Name']) : $info['Name'] . '_pause';
|
||||
// @ 作为特殊字符不能设置为解析, 故设置暂停解析的时候, 替换为 __root__
|
||||
if ($Name == '__root__') {
|
||||
$Name = '@';
|
||||
} elseif ($Name == '@_pause') {
|
||||
$Name = '__root___pause';
|
||||
}
|
||||
return $this->updateDomainRecord($RecordId, $Name, $info['Type'], $info['Value'], $info['Line'], $info['TTL'], $info['MX'], $info['Weight'], $info['Remark']);
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,44 @@ class dnspod implements DnsInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
//域名别名列表
|
||||
public function domainAliasList()
|
||||
{
|
||||
$action = 'DescribeDomainAliasList';
|
||||
$param = [
|
||||
'Domain' => $this->domain,
|
||||
];
|
||||
$data = $this->send_request($action, $param);
|
||||
if ($data) {
|
||||
return $data['DomainAliasList'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//添加域名别名
|
||||
public function addDomainAlias($alias)
|
||||
{
|
||||
$action = 'CreateDomainAlias';
|
||||
$param = [
|
||||
'Domain' => $this->domain,
|
||||
'DomainAlias' => $alias,
|
||||
];
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data);
|
||||
}
|
||||
|
||||
//删除域名别名
|
||||
public function deleteDomainAlias($id)
|
||||
{
|
||||
$action = 'DeleteDomainAlias';
|
||||
$param = [
|
||||
'Domain' => $this->domain,
|
||||
'DomainAliasId' => $id,
|
||||
];
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data);
|
||||
}
|
||||
|
||||
private function convertLineCode($line)
|
||||
{
|
||||
$convert_dict = ['default' => '0', 'unicom' => '10=1', 'telecom' => '10=0', 'mobile' => '10=3', 'edu' => '10=2', 'oversea' => '3=0', 'btvn' => '10=22', 'search' => '80=0', 'internal' => '7=0'];
|
||||
|
||||
@@ -79,7 +79,6 @@ class huawei implements DnsInterface
|
||||
foreach ($data['recordsets'] as $row) {
|
||||
$name = substr($row['name'], 0, -(strlen($row['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
if ($row['type'] == 'MX') list($row['mx'], $row['records']) = explode(' ', $row['records'][0]);
|
||||
$list[] = [
|
||||
'RecordId' => $row['id'],
|
||||
'Domain' => rtrim($row['zone_name'], '.'),
|
||||
@@ -113,7 +112,6 @@ class huawei implements DnsInterface
|
||||
if ($data) {
|
||||
$name = substr($data['name'], 0, -(strlen($data['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]);
|
||||
return [
|
||||
'RecordId' => $data['id'],
|
||||
'Domain' => rtrim($data['zone_name'], '.'),
|
||||
@@ -139,7 +137,6 @@ class huawei implements DnsInterface
|
||||
if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"';
|
||||
$records = array_reverse(explode(',', $Value));
|
||||
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark];
|
||||
if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value;
|
||||
if ($Weight > 0) $params['weight'] = intval($Weight);
|
||||
$data = $this->send_request('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params);
|
||||
return is_array($data) ? $data['id'] : false;
|
||||
@@ -152,7 +149,6 @@ class huawei implements DnsInterface
|
||||
if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"';
|
||||
$records = array_reverse(explode(',', $Value));
|
||||
$params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark];
|
||||
if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value;
|
||||
if ($Weight > 0) $params['weight'] = intval($Weight);
|
||||
$data = $this->send_request('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params);
|
||||
return is_array($data);
|
||||
|
||||
@@ -99,7 +99,7 @@ class CertDeployService
|
||||
if (!empty($error) && strlen($error) > 300) {
|
||||
$error = mb_strcut($error, 0, 300);
|
||||
}
|
||||
$update = ['status' => $status, 'error' => $error, 'retrytime' => $retrytime];
|
||||
$update = ['status' => $status, 'error' => $error ? str_replace(["\r", "\n"], '', $error) : null, 'retrytime' => $retrytime];
|
||||
if ($status == 1){
|
||||
$update['retry'] = 0;
|
||||
$update['lasttime'] = date('Y-m-d H:i:s');
|
||||
|
||||
@@ -22,6 +22,7 @@ class CertOrderService
|
||||
private $dnsList;
|
||||
private $domainList;
|
||||
private $cnameDomainList = [];
|
||||
private $domainsAliasList = [];
|
||||
|
||||
// 订单状态:0:待提交 1:待验证 2:正在验证 3:已签发 4:已吊销 -1:购买证书失败 -2:创建订单失败 -3:添加DNS失败 -4:验证DNS失败 -5:验证订单失败 -6:订单验证未通过 -7:签发证书失败
|
||||
public function __construct($oid)
|
||||
@@ -72,6 +73,12 @@ class CertOrderService
|
||||
if (!$drow && preg_match('/^xn--/', $mainDomain)) {
|
||||
$drow = Db::name('domain')->where('name', idn_to_utf8($mainDomain))->find();
|
||||
}
|
||||
if (!$drow) {
|
||||
$drow = Db::name('domain_alias')->alias('A')->join('domain B', 'A.did = B.id')->where('A.name', $mainDomain)->field('A.name as alias,B.name as maindomain')->find();
|
||||
if ($drow) {
|
||||
$this->domainsAliasList[$drow['alias']] = $drow['maindomain'];
|
||||
}
|
||||
}
|
||||
if (!$drow) {
|
||||
if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2);
|
||||
$cname_row = Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find();
|
||||
@@ -181,7 +188,7 @@ class CertOrderService
|
||||
if (!empty($error) && strlen($error) > 300) {
|
||||
$error = mb_strcut($error, 0, 300);
|
||||
}
|
||||
$update = ['status' => $status, 'error' => $error, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime];
|
||||
$update = ['status' => $status, 'error' => $error ? str_replace(["\r", "\n"], '', $error) : null, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime];
|
||||
$res = Db::name('cert_order')->where('id', $this->order['id'])->data($update);
|
||||
if ($status < 0 || $retrytime) {
|
||||
$this->order['retry']++;
|
||||
@@ -261,6 +268,18 @@ class CertOrderService
|
||||
$this->saveResult(-2, $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
foreach ($this->domainsAliasList as $alias => $mainDomain) {
|
||||
if (isset($this->dnsList[$alias])) {
|
||||
if (!isset($this->dnsList[$mainDomain])) {
|
||||
$this->dnsList[$mainDomain] = $this->dnsList[$alias];
|
||||
} else {
|
||||
$this->dnsList[$mainDomain] = array_merge($this->dnsList[$mainDomain], $this->dnsList[$alias]);
|
||||
}
|
||||
unset($this->dnsList[$alias]);
|
||||
}
|
||||
}
|
||||
|
||||
Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info), 'dns' => json_encode($this->dnsList)]);
|
||||
|
||||
if (!empty($this->dnsList)) {
|
||||
|
||||
604
app/service/CloudflareEnhanceService.php
Normal file
604
app/service/CloudflareEnhanceService.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CloudflareEnhanceService
|
||||
{
|
||||
private string $email = '';
|
||||
private string $apiKey = '';
|
||||
private int $auth = 0;
|
||||
private bool $proxy = false;
|
||||
private string $accountId = '';
|
||||
private string $baseUrl = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->email = trim((string)($config['email'] ?? ''));
|
||||
$this->apiKey = preg_replace('/\s+/', '', trim((string)($config['apikey'] ?? '')));
|
||||
$this->auth = isset($config['auth']) ? intval($config['auth']) : (preg_match('/^[0-9a-f]+$/i', $this->apiKey) ? 0 : 1);
|
||||
$this->proxy = isset($config['proxy']) && strval($config['proxy']) === '1';
|
||||
$this->accountId = trim((string)($config['account_id'] ?? ''));
|
||||
}
|
||||
|
||||
public function isApiTokenAuth(): bool
|
||||
{
|
||||
return $this->auth === 1;
|
||||
}
|
||||
|
||||
public function getConfiguredAccountId(): string
|
||||
{
|
||||
return $this->accountId;
|
||||
}
|
||||
|
||||
public function getAccounts(): array
|
||||
{
|
||||
try {
|
||||
return $this->paginate('/accounts', [], 50);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取账户列表', $e, 'Account:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getDefaultAccountId(): string
|
||||
{
|
||||
try {
|
||||
$accounts = $this->getAccounts();
|
||||
if (!empty($accounts[0]['id'])) {
|
||||
return trim((string)$accounts[0]['id']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones', ['page' => 1, 'per_page' => 1]);
|
||||
$first = $payload['result'][0] ?? [];
|
||||
$accountId = trim((string)($first['account']['id'] ?? ''));
|
||||
if ($accountId !== '') {
|
||||
return $accountId;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getZone(string $zoneId): array
|
||||
{
|
||||
try {
|
||||
return $this->requestResult('GET', '/zones/' . $zoneId);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取域名详情', $e, 'Zone:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function listCustomHostnames(string $zoneId): array
|
||||
{
|
||||
try {
|
||||
return $this->paginate('/zones/' . $zoneId . '/custom_hostnames', [], 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取自定义主机名列表', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomHostname(string $zoneId, string $hostnameId): array
|
||||
{
|
||||
try {
|
||||
return $this->requestResult('GET', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取自定义主机名详情', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array
|
||||
{
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$payload = [
|
||||
'hostname' => $hostname,
|
||||
'ssl' => [
|
||||
'method' => 'http',
|
||||
'type' => 'dv',
|
||||
],
|
||||
];
|
||||
$origin = trim((string)$customOriginServer);
|
||||
if ($origin !== '') {
|
||||
$payload['custom_origin_server'] = $this->normalizeHostname($origin);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/zones/' . $zoneId . '/custom_hostnames', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCustomHostname(string $zoneId, string $hostnameId, array $payload): array
|
||||
{
|
||||
if (isset($payload['custom_origin_server']) && $payload['custom_origin_server'] !== null) {
|
||||
$payload['custom_origin_server'] = $this->normalizeHostname($payload['custom_origin_server']);
|
||||
}
|
||||
if (isset($payload['hostname']) && $payload['hostname'] !== null) {
|
||||
$payload['hostname'] = $this->normalizeHostname($payload['hostname']);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('PATCH', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId), [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCustomHostname(string $zoneId, string $hostnameId): bool
|
||||
{
|
||||
try {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/custom_hostnames/' . $hostnameId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function getFallbackOrigin(string $zoneId): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], null, true);
|
||||
if ($result === null) {
|
||||
return '';
|
||||
}
|
||||
return trim((string)($result['origin'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() === 404) {
|
||||
return '';
|
||||
}
|
||||
$this->throwActionError('获取 Fallback Origin', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateFallbackOrigin(string $zoneId, string $origin): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('PUT', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], [
|
||||
'origin' => $this->normalizeHostname($origin),
|
||||
]);
|
||||
return trim((string)($result['origin'] ?? $origin));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新 Fallback Origin', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFallbackOrigin(string $zoneId): bool
|
||||
{
|
||||
try {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], null, true);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() === 404) {
|
||||
return true;
|
||||
}
|
||||
$this->throwActionError('删除 Fallback Origin', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listTunnels(string $accountId): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/cfd_tunnel', ['is_deleted' => 'false'], 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel 列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createTunnel(string $accountId, string $name): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/cfd_tunnel', [], [
|
||||
'name' => trim($name),
|
||||
'tunnel_secret' => base64_encode(random_bytes(32)),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建 Tunnel', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTunnel(string $accountId, string $tunnelId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 Tunnel', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function getTunnelToken(string $accountId, string $tunnelId): string
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/token');
|
||||
if (is_string($result)) {
|
||||
return $result;
|
||||
}
|
||||
return trim((string)($result['token'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel Token', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getTunnelConfig(string $accountId, string $tunnelId): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/configurations', [], null, true);
|
||||
return is_array($result) ? $result : [];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel 配置', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateTunnelConfig(string $accountId, string $tunnelId, array $config): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->requestResult('PUT', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/configurations', [], [
|
||||
'config' => $config,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新 Tunnel 配置', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listCidrRoutes(string $accountId, ?string $tunnelId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$query = ['is_deleted' => 'false'];
|
||||
if (!empty($tunnelId)) {
|
||||
$query['tunnel_id'] = $tunnelId;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/teamnet/routes', $query, 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 CIDR 路由列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createCidrRoute(string $accountId, string $tunnelId, string $network, ?string $comment = null, ?string $virtualNetworkId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$payload = [
|
||||
'network' => trim($network),
|
||||
'tunnel_id' => trim($tunnelId),
|
||||
];
|
||||
if (!empty($comment)) {
|
||||
$payload['comment'] = trim($comment);
|
||||
}
|
||||
if (!empty($virtualNetworkId)) {
|
||||
$payload['virtual_network_id'] = trim($virtualNetworkId);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/teamnet/routes', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建 CIDR 路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCidrRoute(string $accountId, string $routeId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/teamnet/routes/' . $routeId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 CIDR 路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listHostnameRoutes(string $accountId, ?string $tunnelId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$query = ['is_deleted' => 'false'];
|
||||
if (!empty($tunnelId)) {
|
||||
$query['tunnel_id'] = $tunnelId;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/zerotrust/routes/hostname', $query, 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取主机名路由列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createHostnameRoute(string $accountId, string $tunnelId, string $hostname, ?string $comment = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$payload = [
|
||||
'hostname' => $this->normalizeHostname($hostname),
|
||||
'tunnel_id' => trim($tunnelId),
|
||||
];
|
||||
if (!empty($comment)) {
|
||||
$payload['comment'] = trim($comment);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/zerotrust/routes/hostname', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建主机名路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteHostnameRoute(string $accountId, string $routeId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/zerotrust/routes/hostname/' . $routeId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除主机名路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function upsertTunnelCnameRecord(string $zoneId, string $hostname, string $tunnelId): array
|
||||
{
|
||||
$zoneId = trim($zoneId);
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$target = trim($tunnelId) . '.cfargotunnel.com';
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'type' => 'CNAME',
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$records = $payload['result'] ?? [];
|
||||
|
||||
$allByNamePayload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$allByName = $allByNamePayload['result'] ?? [];
|
||||
$otherTypes = [];
|
||||
foreach ($allByName as $row) {
|
||||
$type = strtoupper((string)($row['type'] ?? ''));
|
||||
$name = $this->normalizeHostname($row['name'] ?? '');
|
||||
if ($name === $hostname && $type !== 'CNAME') {
|
||||
$otherTypes[] = $type;
|
||||
}
|
||||
}
|
||||
if (!empty($otherTypes)) {
|
||||
$otherTypes = array_unique(array_filter($otherTypes));
|
||||
throw new Exception('主机名已存在非 CNAME 记录(' . implode(', ', $otherTypes) . '),无法同步 Tunnel CNAME', 400);
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
$name = $this->normalizeHostname($record['name'] ?? '');
|
||||
if ($name !== $hostname) {
|
||||
continue;
|
||||
}
|
||||
$content = $this->normalizeHostname($record['content'] ?? '');
|
||||
$proxied = !empty($record['proxied']);
|
||||
if ($content === $this->normalizeHostname($target) && $proxied) {
|
||||
return ['action' => 'unchanged'];
|
||||
}
|
||||
|
||||
$this->requestResult('PUT', '/zones/' . $zoneId . '/dns_records/' . $record['id'], [], [
|
||||
'type' => 'CNAME',
|
||||
'name' => $hostname,
|
||||
'content' => $target,
|
||||
'proxied' => true,
|
||||
'ttl' => 1,
|
||||
]);
|
||||
return ['action' => 'updated'];
|
||||
}
|
||||
|
||||
$this->requestResult('POST', '/zones/' . $zoneId . '/dns_records', [], [
|
||||
'type' => 'CNAME',
|
||||
'name' => $hostname,
|
||||
'content' => $target,
|
||||
'proxied' => true,
|
||||
'ttl' => 1,
|
||||
]);
|
||||
return ['action' => 'created'];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('同步 Tunnel CNAME 记录', $e, 'Zone:DNS:Edit');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTunnelCnameRecordIfMatch(string $zoneId, string $hostname, string $tunnelId): array
|
||||
{
|
||||
$zoneId = trim($zoneId);
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$target = $this->normalizeHostname(trim($tunnelId) . '.cfargotunnel.com');
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'type' => 'CNAME',
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$records = $payload['result'] ?? [];
|
||||
foreach ($records as $record) {
|
||||
$name = $this->normalizeHostname($record['name'] ?? '');
|
||||
$content = $this->normalizeHostname($record['content'] ?? '');
|
||||
if ($name === $hostname && $content === $target) {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/dns_records/' . $record['id']);
|
||||
return ['deleted' => true];
|
||||
}
|
||||
}
|
||||
return ['deleted' => false];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 Tunnel CNAME 记录', $e, 'Zone:DNS:Edit');
|
||||
}
|
||||
}
|
||||
|
||||
private function paginate(string $path, array $query = [], int $perPage = 100): array
|
||||
{
|
||||
$all = [];
|
||||
$page = 1;
|
||||
$maxPage = 200;
|
||||
while ($page <= $maxPage) {
|
||||
$payload = $this->requestRaw('GET', $path, array_merge($query, [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
]));
|
||||
$batch = $payload['result'] ?? [];
|
||||
if (!is_array($batch)) {
|
||||
$batch = [];
|
||||
}
|
||||
foreach ($batch as $item) {
|
||||
$all[] = $item;
|
||||
}
|
||||
|
||||
$totalPages = intval($payload['result_info']['total_pages'] ?? 0);
|
||||
if ($totalPages > 0) {
|
||||
if ($page >= $totalPages) {
|
||||
break;
|
||||
}
|
||||
} elseif (count($batch) < $perPage || empty($batch)) {
|
||||
break;
|
||||
}
|
||||
$page++;
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function requestResult(string $method, string $path, array $query = [], ?array $body = null, bool $allowNotFound = false)
|
||||
{
|
||||
$payload = $this->requestRaw($method, $path, $query, $body, $allowNotFound);
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
return $payload['result'] ?? [];
|
||||
}
|
||||
|
||||
private function requestRaw(string $method, string $path, array $query = [], ?array $body = null, bool $allowNotFound = false): ?array
|
||||
{
|
||||
$headers = $this->buildHeaders($body !== null);
|
||||
$url = $this->baseUrl . $path;
|
||||
if (!empty($query)) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$response = http_request(
|
||||
$url,
|
||||
$body,
|
||||
null,
|
||||
null,
|
||||
$headers,
|
||||
$this->proxy,
|
||||
strtoupper($method),
|
||||
20
|
||||
);
|
||||
|
||||
$status = intval($response['code'] ?? 0);
|
||||
if ($allowNotFound && $status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = json_decode($response['body'] ?? '', true);
|
||||
if (!is_array($payload)) {
|
||||
throw new Exception('Cloudflare 返回数据解析失败', $status > 0 ? $status : 502);
|
||||
}
|
||||
|
||||
if (($payload['success'] ?? false) !== true) {
|
||||
if ($allowNotFound && $status === 404) {
|
||||
return null;
|
||||
}
|
||||
$message = $this->extractErrorMessage($payload);
|
||||
throw new Exception($message !== '' ? $message : 'Cloudflare API 请求失败', $status > 0 ? $status : 400);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function buildHeaders(bool $json = false): array
|
||||
{
|
||||
if ($this->apiKey === '') {
|
||||
throw new Exception('Cloudflare API 凭证为空', 400);
|
||||
}
|
||||
|
||||
if ($this->auth === 1) {
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $this->apiKey,
|
||||
];
|
||||
} else {
|
||||
if ($this->email === '') {
|
||||
throw new Exception('当前 Cloudflare 账户缺少邮箱地址,旧版 API Key 认证需要填写邮箱', 400);
|
||||
}
|
||||
$headers = [
|
||||
'X-Auth-Email' => $this->email,
|
||||
'X-Auth-Key' => $this->apiKey,
|
||||
];
|
||||
}
|
||||
|
||||
if ($json) {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function assertTunnelSupported(): void
|
||||
{
|
||||
if (!$this->isApiTokenAuth()) {
|
||||
throw new Exception('Cloudflare Tunnels 仅支持 API 令牌认证,请将当前账户的认证方式切换为 API令牌', 400);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeHostname($hostname): string
|
||||
{
|
||||
$hostname = trim((string)$hostname);
|
||||
if ($hostname === '') {
|
||||
return '';
|
||||
}
|
||||
$hostname = rtrim($hostname, '.');
|
||||
$hostname = convertDomainToAscii($hostname);
|
||||
return strtolower($hostname);
|
||||
}
|
||||
|
||||
private function extractErrorMessage(array $payload): string
|
||||
{
|
||||
if (!empty($payload['errors'][0]['message'])) {
|
||||
return trim((string)$payload['errors'][0]['message']);
|
||||
}
|
||||
if (!empty($payload['messages'][0]['message'])) {
|
||||
return trim((string)$payload['messages'][0]['message']);
|
||||
}
|
||||
if (!empty($payload['result']['message'])) {
|
||||
return trim((string)$payload['result']['message']);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function throwActionError(string $action, Exception $e, string $permissionHint = ''): void
|
||||
{
|
||||
$status = intval($e->getCode());
|
||||
$message = trim($e->getMessage());
|
||||
|
||||
if ($status === 401) {
|
||||
$message = 'Cloudflare 凭证无效或已过期,无法' . $action;
|
||||
} elseif ($status === 403) {
|
||||
$message = 'Cloudflare 权限不足,无法' . $action;
|
||||
if ($permissionHint !== '') {
|
||||
$message .= '。请确认 Token 具备 ' . $permissionHint . ' 权限';
|
||||
}
|
||||
} elseif ($status === 404 && $message === '') {
|
||||
$message = $action . '失败:资源不存在';
|
||||
} elseif ($status === 429) {
|
||||
$message = 'Cloudflare API 请求过于频繁,暂时无法' . $action . ',请稍后重试';
|
||||
} elseif ($status >= 500) {
|
||||
$message = 'Cloudflare 服务暂时不可用,无法' . $action . ',请稍后重试';
|
||||
} elseif ($message === '') {
|
||||
$message = $action . '失败';
|
||||
}
|
||||
|
||||
throw new Exception($message, $status > 0 ? $status : 400);
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,14 @@ class OptimizeService
|
||||
public static function get_license($api, $key)
|
||||
{
|
||||
if ($api == 2) {
|
||||
throw new Exception('当前接口暂不支持');
|
||||
throw new Exception('xingpingcn.top 接口免费使用,无需密钥,无积分限制');
|
||||
} elseif ($api == 1) {
|
||||
$url = 'https://api.hostmonit.com/get_license?license='.$key;
|
||||
} else {
|
||||
$url = 'https://www.wetest.vip/api/cf2dns/get_license?license='.$key;
|
||||
}
|
||||
$response = get_curl($url);
|
||||
if (!$response) throw new Exception('接口请求失败');
|
||||
$arr = json_decode($response, true);
|
||||
if (isset($arr['code']) && $arr['code'] == 200 && isset($arr['count'])) {
|
||||
return $arr['count'];
|
||||
@@ -39,7 +40,9 @@ class OptimizeService
|
||||
public function get_ip_address($cdn_type = 1, $ip_type = 'v4')
|
||||
{
|
||||
$api = config_get('optimize_ip_api', 0);
|
||||
if ($api == 1) {
|
||||
if ($api == 2) {
|
||||
return $this->get_ip_address_xingpingcn($ip_type);
|
||||
} elseif ($api == 1) {
|
||||
$url = 'https://api.hostmonit.com/get_optimization_ip';
|
||||
} else {
|
||||
$url = 'https://www.wetest.vip/api/cf2dns/';
|
||||
@@ -58,6 +61,7 @@ class OptimizeService
|
||||
'type' => $ip_type,
|
||||
];
|
||||
$response = get_curl($url, json_encode($params), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']);
|
||||
if (!$response) throw new Exception('接口请求失败');
|
||||
$arr = json_decode($response, true);
|
||||
if (isset($arr['code']) && $arr['code'] == 200) {
|
||||
return $arr['info'];
|
||||
@@ -70,6 +74,59 @@ class OptimizeService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 xingpingcn.top 获取优选IP数据
|
||||
* @param string $ip_type IP类型 v4/v6
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function get_ip_address_xingpingcn($ip_type = 'v4')
|
||||
{
|
||||
if ($ip_type == 'v6') {
|
||||
throw new Exception('xingpingcn.top 接口暂不支持IPv6');
|
||||
}
|
||||
$proxy = config_get('optimize_ip_proxy', '');
|
||||
if (!empty($proxy)) {
|
||||
$proxy = trim($proxy);
|
||||
if (filter_var($proxy, FILTER_VALIDATE_URL) === false) {
|
||||
throw new Exception('无效的代理地址配置:URL 格式错误');
|
||||
}
|
||||
$scheme = parse_url($proxy, PHP_URL_SCHEME);
|
||||
if (!in_array($scheme, ['http', 'https'], true)) {
|
||||
throw new Exception('无效的代理地址配置:仅支持 http 和 https 协议');
|
||||
}
|
||||
$url = rtrim($proxy, '/') . '/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
|
||||
} else {
|
||||
$url = 'https://raw.githubusercontent.com/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
|
||||
}
|
||||
$response = get_curl($url);
|
||||
if (!$response) {
|
||||
throw new Exception('获取优选IP数据失败,网络请求失败,请检查网络连接或代理地址');
|
||||
}
|
||||
$arr = json_decode($response, true);
|
||||
if (isset($arr['Cf']['result'])) {
|
||||
$result = $arr['Cf']['result'];
|
||||
$info = [];
|
||||
// 转换格式:dianxin->CT, liantong->CU, yidong->CM, default->DEF
|
||||
if (isset($result['dianxin']) && is_array($result['dianxin'])) {
|
||||
$info['CT'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['dianxin']);
|
||||
}
|
||||
if (isset($result['liantong']) && is_array($result['liantong'])) {
|
||||
$info['CU'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['liantong']);
|
||||
}
|
||||
if (isset($result['yidong']) && is_array($result['yidong'])) {
|
||||
$info['CM'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['yidong']);
|
||||
}
|
||||
// 不使用他的默认线路数据, 因为这真的是默认. 由后续逻辑自己决定是否把CT线路当DEF来用
|
||||
// if (isset($result['default']) && is_array($result['default'])) {
|
||||
// $info['DEF'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['default']);
|
||||
// }
|
||||
return $info;
|
||||
} else {
|
||||
throw new Exception('获取优选IP数据失败,接口返回数据格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
public function get_ip_address2($cdn_type = 1, $ip_type = 'v4')
|
||||
{
|
||||
$key = $cdn_type.'_'.$ip_type;
|
||||
|
||||
@@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `dnsmgr_config` VALUES ('version', '1045');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('version', '1048');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
|
||||
@@ -251,4 +251,14 @@ CREATE TABLE `dnsmgr_sctask` (
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_domain_alias`;
|
||||
CREATE TABLE `dnsmgr_domain_alias` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`did` int(11) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`),
|
||||
KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -189,4 +189,13 @@ CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` (
|
||||
|
||||
ALTER TABLE `dnsmgr_account`
|
||||
ADD COLUMN `config` text DEFAULT NULL,
|
||||
CHANGE COLUMN `ak` `name` varchar(255) NOT NULL;
|
||||
CHANGE COLUMN `ak` `name` varchar(255) NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dnsmgr_domain_alias` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`did` int(11) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`),
|
||||
KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -42,12 +42,14 @@ class DnsQueryUtils
|
||||
$id = array_rand(self::$doh_servers);
|
||||
$url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
|
||||
$data = get_curl($url);
|
||||
if (!$data) return false;
|
||||
$arr = json_decode($data, true);
|
||||
if (!$arr) {
|
||||
unset(self::$doh_servers[$id]);
|
||||
$id = array_rand(self::$doh_servers);
|
||||
$url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
|
||||
$data = get_curl($url);
|
||||
if (!$data) return false;
|
||||
$arr = json_decode($data, true);
|
||||
if (!$arr) return false;
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@ class MsgNotice
|
||||
$url = 'https://wxpusher.zjiecode.com/api/send/message';
|
||||
$post = ['appToken' => $wechat_apptoken, 'content' => $content, 'summary' => $title, 'contentType' => 3, 'uids' => [$wechat_appuid]];
|
||||
$result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']);
|
||||
if (!$result) return '请求失败';
|
||||
$arr = json_decode($result, true);
|
||||
if (isset($arr['success']) && $arr['success'] == true) {
|
||||
return true;
|
||||
@@ -246,6 +247,7 @@ class MsgNotice
|
||||
$url = $tgbot_url.'/bot'.$tgbot_token.'/sendMessage';
|
||||
$post = ['chat_id' => $tgbot_chatid, 'text' => $content, 'parse_mode' => 'HTML'];
|
||||
$result = self::telegram_curl($url, http_build_query($post));
|
||||
if (!$result) return '请求失败';
|
||||
$arr = json_decode($result, true);
|
||||
if (isset($arr['ok']) && $arr['ok'] == true) {
|
||||
return true;
|
||||
@@ -348,6 +350,7 @@ class MsgNotice
|
||||
return '不支持的Webhook地址';
|
||||
}
|
||||
$result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']);
|
||||
if (!$result) return '请求失败';
|
||||
$arr = json_decode($result, true);
|
||||
if (isset($arr['errcode']) && $arr['errcode'] == 0 || isset($arr['code']) && $arr['code'] == 0) {
|
||||
return true;
|
||||
|
||||
690
app/view/cloudflare/hostnames.html
Normal file
690
app/view/cloudflare/hostnames.html
Normal file
@@ -0,0 +1,690 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}Cloudflare增强 - {$domainName}{/block}
|
||||
{block name="main"}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float:none;">
|
||||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
<div class="clearfix">
|
||||
<div class="pull-right" style="margin-top:-6px;max-width:100%;">
|
||||
<a href="/record/{$domainId}" class="btn btn-sm btn-default" style="vertical-align:middle;"><i class="fa fa-reply fa-fw"></i> 返回解析</a>
|
||||
</div>
|
||||
<h3 class="panel-title" style="padding-top:4px;">Cloudflare增强 - {$domainName}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>说明:</strong> 这里管理 Cloudflare 自定义主机名、证书状态、证书校验与 Fallback Origin。
|
||||
</div>
|
||||
|
||||
<div class="well well-sm">
|
||||
<div class="form-inline">
|
||||
<div class="form-group" style="width:70%;max-width:720px;">
|
||||
<label>Fallback Origin</label>
|
||||
<input type="text" id="fallbackOrigin" class="form-control" style="width:80%;" placeholder="例如 origin.example.com">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveFallbackOrigin()">保存</button>
|
||||
<button type="button" class="btn btn-default" onclick="loadFallbackOrigin()">刷新</button>
|
||||
<button type="button" class="btn btn-danger" onclick="clearFallbackOrigin()">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom:5px;">
|
||||
<div class="pull-left">
|
||||
<a href="javascript:refreshHostnameList()" class="btn btn-default" title="刷新自定义主机名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openAddDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 添加自定义主机名</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-store" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="storeTitle">添加自定义主机名</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<input type="hidden" name="hostname_id" value="">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">主机名</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="例如 app.example.com 或 *.example.com" required>
|
||||
<p class="help-block" id="hostnameHint">创建后主机名不能直接改名,如需改名请删除后重建。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义源站</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||||
<p class="help-block">留空表示清空当前自定义源站,回退到 Fallback Origin 或默认源站逻辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitHostname()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-verification" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="verificationTitle">证书校验</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="verificationContent"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="refreshHostnameValidation()">刷新校验</button>
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js?v=1005"></script>
|
||||
<script>
|
||||
var currentVerificationHostnameId = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#form-store").bootstrapValidator();
|
||||
loadFallbackOrigin();
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/hostnames/data/{$domainId}',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: hostnameResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: '主机名'},
|
||||
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
||||
{field: 'ssl_validation_status', title: '证书校验', formatter: formatStatus},
|
||||
{field: 'verification_status', title: '所有权校验', formatter: formatStatus},
|
||||
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return ''
|
||||
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
|
||||
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">校验</a> '
|
||||
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function hostnameResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshHostnameList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function formatStatus(value){
|
||||
var v = String(value || '').toLowerCase();
|
||||
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
if(v && v !== '-'){
|
||||
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function getHostnameRow(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
if(!row){
|
||||
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
||||
return null;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function resetHostnameForm(){
|
||||
$("#form-store")[0].reset();
|
||||
$("#form-store input[name=hostname_id]").val('');
|
||||
$("#form-store input[name=hostname]").prop('readonly', false);
|
||||
$("#form-store").data("bootstrapValidator").resetForm(true);
|
||||
}
|
||||
|
||||
function openAddDialog(){
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('添加自定义主机名');
|
||||
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function openEditDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('编辑自定义主机名');
|
||||
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
||||
$("#form-store input[name=hostname_id]").val(row.id);
|
||||
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
||||
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function submitHostname(){
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
||||
var url = hostnameId ? '/cloudflare/hostnames/update/{$domainId}' : '/cloudflare/hostnames/add/{$domainId}';
|
||||
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: $("#form-store").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-store").modal('hide');
|
||||
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openVerificationDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
currentVerificationHostnameId = id;
|
||||
renderVerificationDialog(row);
|
||||
$("#modal-verification").modal('show');
|
||||
}
|
||||
|
||||
function refreshHostnameValidation(){
|
||||
if(!currentVerificationHostnameId){
|
||||
layer.msg('请先选择自定义主机名');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/refresh/{$domainId}',
|
||||
data: {hostname_id: currentVerificationHostnameId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
renderVerificationDialog(res.data);
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderVerificationDialog(row){
|
||||
$("#verificationTitle").text('证书校验 - ' + row.hostname);
|
||||
var html = '';
|
||||
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新校验”会重新向 Cloudflare 发起一次校验。</div>';
|
||||
html += '<div class="row">';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书校验', formatStatusText(row.ssl_validation_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('所有权校验', formatStatusText(row.verification_status))+'</div>';
|
||||
html += '</div>';
|
||||
|
||||
var ownership = row.ownership_verification || {};
|
||||
if(ownership.name || ownership.value){
|
||||
html += renderSection('所有权 TXT 校验',
|
||||
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
||||
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
||||
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
||||
+ renderQuickAddTxtButton(ownership.name || '', ownership.value || '', '快速添加所有权 TXT')
|
||||
);
|
||||
}
|
||||
|
||||
var ownershipHttp = row.ownership_verification_http || {};
|
||||
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
||||
html += renderSection('所有权 HTTP 校验',
|
||||
renderCopyTextarea('HTTP URL', ownershipHttp.http_url || '', true, 2)
|
||||
+ renderCopyTextarea('HTTP Body', ownershipHttp.http_body || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||||
if(records.length > 0){
|
||||
var recordsHtml = '';
|
||||
for(var i = 0; i < records.length; i++){
|
||||
var item = records[i] || {};
|
||||
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
||||
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
||||
recordsHtml += '<div class="panel-heading"><strong>证书校验记录 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
||||
recordsHtml += '<div class="panel-body">';
|
||||
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
||||
recordsHtml += renderQuickAddTxtButton(item.txt_name || '', item.txt_value || '', '快速添加 TXT');
|
||||
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP URL', item.http_url || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP Body', item.http_body || '', true, 3);
|
||||
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
||||
recordsHtml += '</div></div>';
|
||||
}
|
||||
html += renderSection('证书校验记录', recordsHtml);
|
||||
}else{
|
||||
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书校验记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新校验”或稍后刷新列表。</div>';
|
||||
}
|
||||
|
||||
if(row.validation_errors){
|
||||
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
||||
}
|
||||
|
||||
$("#verificationContent").html(html);
|
||||
}
|
||||
|
||||
function renderSummaryCard(title, value){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
||||
}
|
||||
|
||||
function renderSection(title, body){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function renderCopyInput(label, value, copyable){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
if(copyable){
|
||||
html += '<div class="input-group">';
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
||||
html += '</div>';
|
||||
}else{
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCopyTextarea(label, value, copyable, rows){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
||||
if(copyable){
|
||||
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderQuickAddTxtButton(name, value, label){
|
||||
var txtName = String(name || '').trim();
|
||||
var txtValue = String(value || '').trim();
|
||||
if(!txtName || !txtValue){
|
||||
return '';
|
||||
}
|
||||
return '<div class="text-right" style="margin-top:8px;margin-bottom:12px;"><button type="button" class="btn btn-success btn-xs" data-name="' + encodeURIComponent(txtName) + '" data-value="' + encodeURIComponent(txtValue) + '" onclick="quickAddTxtRecord(this)">' + htmlEscape(label || '快速添加 TXT') + '</button></div>';
|
||||
}
|
||||
|
||||
function formatStatusText(value){
|
||||
var text = value || '-';
|
||||
if(text === '-'){
|
||||
return '<span class="text-muted">-</span>';
|
||||
}
|
||||
return formatStatus(text);
|
||||
}
|
||||
|
||||
function copyEncodedValue(btn){
|
||||
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
||||
}
|
||||
|
||||
function copyText(text){
|
||||
var value = String(text || '');
|
||||
if(!value){
|
||||
layer.msg('没有可复制的内容');
|
||||
return;
|
||||
}
|
||||
if(navigator.clipboard && window.isSecureContext){
|
||||
navigator.clipboard.writeText(value).then(function(){
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}).catch(function(){
|
||||
fallbackCopyText(value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopyText(value);
|
||||
}
|
||||
|
||||
function fallbackCopyText(text){
|
||||
var $temp = $('<textarea readonly></textarea>');
|
||||
$('body').append($temp);
|
||||
$temp.val(text).select();
|
||||
try{
|
||||
document.execCommand('copy');
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}catch(e){
|
||||
layer.alert('复制失败,请手动复制', {icon: 2});
|
||||
}
|
||||
$temp.remove();
|
||||
}
|
||||
|
||||
function quickAddTxtRecord(btn){
|
||||
var fullName = decodeURIComponent($(btn).attr('data-name') || '');
|
||||
var value = decodeURIComponent($(btn).attr('data-value') || '');
|
||||
resolveTxtRecordTargets(fullName, function(targets){
|
||||
if(!targets.length){
|
||||
layer.alert('系统中未找到与该 TXT 主机名对应的托管域名,请手动到解析页添加', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if(targets.length === 1){
|
||||
confirmQuickAddTxtRecord(fullName, value, targets[0]);
|
||||
return;
|
||||
}
|
||||
openTxtTargetPicker(fullName, value, targets);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTxtRecordTargets(fullName, callback){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/txttargets/{$domainId}',
|
||||
data: {hostname: fullName},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
var targets = res.data && $.isArray(res.data.candidates) ? res.data.candidates : [];
|
||||
callback(targets);
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openTxtTargetPicker(fullName, value, targets){
|
||||
var html = '<div style="padding:16px 18px 6px;">';
|
||||
html += '<div class="alert alert-warning" style="margin-bottom:12px;">检测到多个可用解析域名,请确认要写入哪个服务商。</div>';
|
||||
html += '<div class="form-group"><label>TXT 主机名</label><div><code>' + htmlEscape(fullName) + '</code></div></div>';
|
||||
html += '<div class="form-group"><label>TXT 值</label><textarea class="form-control" rows="3" readonly>' + htmlEscape(value) + '</textarea></div>';
|
||||
html += '<form id="txtTargetPickerForm">';
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var target = targets[i] || {};
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
html += '<div class="radio" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:10px 12px;">';
|
||||
html += '<label style="display:block;padding-left:22px;">';
|
||||
html += '<input type="radio" name="txtTarget" value="' + htmlEscape(String(target.domain_id || '')) + '"' + (i === 0 ? ' checked' : '') + '>';
|
||||
html += '<strong>' + htmlEscape(target.domain_name || '-') + '</strong>';
|
||||
if(target.is_current_domain){
|
||||
html += ' <span class="label label-primary">当前页</span>';
|
||||
}
|
||||
html += '<div class="help-block" style="margin:8px 0 0;">';
|
||||
html += '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>';
|
||||
html += '服务商:' + htmlEscape(providerName) + '<br>';
|
||||
html += '账户:' + htmlEscape(accountName);
|
||||
html += '</div>';
|
||||
html += '</label></div>';
|
||||
}
|
||||
html += '</form></div>';
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择解析服务商',
|
||||
area: ['640px', 'auto'],
|
||||
shadeClose: false,
|
||||
content: html,
|
||||
btn: ['添加 TXT', '取消'],
|
||||
yes: function(index){
|
||||
var selectedId = $('#txtTargetPickerForm input[name=txtTarget]:checked').val();
|
||||
var target = findTxtTargetByDomainId(targets, selectedId);
|
||||
if(!target){
|
||||
layer.msg('请选择要写入的解析域名', {icon: 2});
|
||||
return;
|
||||
}
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmQuickAddTxtRecord(fullName, value, target){
|
||||
layer.confirm(buildQuickAddConfirmHtml(fullName, target), {title: '提示', icon: 0}, function(index){
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuickAddConfirmHtml(fullName, target){
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
return '确定要快速添加 TXT 记录吗?<br><br>'
|
||||
+ 'TXT 主机名:<code>' + htmlEscape(fullName) + '</code><br>'
|
||||
+ '解析域名:<code>' + htmlEscape(target.domain_name || '-') + '</code><br>'
|
||||
+ '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>'
|
||||
+ '服务商:' + htmlEscape(providerName) + '<br>'
|
||||
+ '账户:' + htmlEscape(accountName);
|
||||
}
|
||||
|
||||
function submitQuickAddTxtRecord(value, target){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/add/' + target.domain_id,
|
||||
data: {
|
||||
name: target.record_name,
|
||||
type: 'TXT',
|
||||
value: value,
|
||||
line: '0',
|
||||
ttl: 600,
|
||||
mx: 1,
|
||||
weight: 0,
|
||||
remark: 'Cloudflare证书校验'
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-verification").modal('show');
|
||||
layer.msg('TXT 记录已添加到 ' + (target.domain_name || '-'), {icon: 1, time: 1400});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findTxtTargetByDomainId(targets, domainId){
|
||||
var selected = String(domainId || '');
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var item = targets[i] || {};
|
||||
if(String(item.domain_id || '') === selected){
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function deleteHostname(id, hostname){
|
||||
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/delete/{$domainId}',
|
||||
data: {hostname_id: id, hostname: hostname},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
refreshHostnameList();
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFallbackOrigin(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/get/{$domainId}',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val((res.data && res.data.origin) ? res.data.origin : '');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveFallbackOrigin(){
|
||||
var origin = $.trim($("#fallbackOrigin").val());
|
||||
if(!origin){
|
||||
layer.msg('请输入 Fallback Origin');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/set/{$domainId}',
|
||||
data: {origin: origin},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val(res.data.origin || origin);
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFallbackOrigin(){
|
||||
layer.confirm('确定要清空 Fallback Origin 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/delete/{$domainId}',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#fallbackOrigin").val('');
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
607
app/view/cloudflare/tunnels.html
Normal file
607
app/view/cloudflare/tunnels.html
Normal file
@@ -0,0 +1,607 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}Cloudflare Tunnels - {$accountName}{/block}
|
||||
{block name="main"}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float:none;">
|
||||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a href="/account" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回账户</a>
|
||||
Cloudflare Tunnels - {$accountName}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>Account ID:</strong>{$cfAccountId}
|
||||
<br>
|
||||
这里管理 Tunnel 列表、公网主机名、CIDR 路由和主机名路由。公网主机名会自动同步为对应域名下的 CNAME。
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom:15px;">
|
||||
<div class="pull-left">
|
||||
<a href="javascript:refreshTunnelList()" class="btn btn-default" title="刷新 Tunnel 列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openTunnelDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 创建 Tunnel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-tunnel" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title">创建 Tunnel</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-tunnel">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">名称</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="name" placeholder="例如 edge-prod" required>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitTunnel()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-token" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title">Tunnel Token</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Tunnel</label>
|
||||
<input type="text" class="form-control" id="tokenTunnelName" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Token</label>
|
||||
<textarea id="tokenValue" class="form-control" rows="4" readonly></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>启动命令</label>
|
||||
<textarea id="tokenCommand" class="form-control" rows="3" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" onclick="copyTokenCommand()">复制启动命令</button>
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-public" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="publicTitle">公网主机名</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-public">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="hostname,例如 app.example.com" style="width:240px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="service" placeholder="service,例如 http://127.0.0.1:8080" style="width:260px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="path" placeholder="可留空,例如 /api/*" style="width:180px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="savePublicHostname()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="publicTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-cidr" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="cidrTitle">CIDR 路由</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-cidr">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="network" placeholder="例如 10.10.0.0/16" style="width:220px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="comment" placeholder="备注,可留空" style="width:240px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveCidrRoute()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="cidrTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-hostname-route" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="hostnameRouteTitle">主机名路由</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-hostname-route">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="例如 internal.example.com" style="width:260px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="comment" placeholder="备注,可留空" style="width:240px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveHostnameRoute()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="hostnameRouteTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var selectedTunnelId = '';
|
||||
var selectedTunnelName = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#form-tunnel").bootstrapValidator();
|
||||
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/tunnels/data/{$accountId}',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'name', title: '名称'},
|
||||
{field: 'id', title: 'Tunnel ID'},
|
||||
{field: 'status', title: '状态', formatter: tunnelStatusFormatter},
|
||||
{field: 'connection_count', title: '连接数'},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return ''
|
||||
+ '<a href="javascript:showToken(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-info btn-xs">Token</a> '
|
||||
+ '<a href="javascript:openPublicHostnames(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-primary btn-xs">公网主机名</a> '
|
||||
+ '<a href="javascript:openCidrRoutes(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-warning btn-xs">CIDR</a> '
|
||||
+ '<a href="javascript:openHostnameRoutes(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-success btn-xs">主机名路由</a> '
|
||||
+ '<a href="javascript:deleteTunnel(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#publicTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'hostname',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: 'Hostname'},
|
||||
{field: 'path', title: 'Path', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'service', title: 'Service'},
|
||||
{field: 'zone_name', title: '匹配域名', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deletePublicHostname(\''+escapeJs(row.hostname)+'\', \''+escapeJs(row.path || '')+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#cidrTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'network', title: 'CIDR'},
|
||||
{field: 'comment', title: '备注', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deleteCidrRoute(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#hostnameRouteTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: 'Hostname'},
|
||||
{field: 'comment', title: '备注', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deleteHostnameRoute(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function tableResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '请求失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshTunnelList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function tunnelStatusFormatter(value){
|
||||
var v = (value || '').toLowerCase();
|
||||
if(v === 'healthy' || v === 'active'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'inactive' || v === 'down' || v === 'degraded'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
return value ? '<span class="label label-default">'+htmlEscape(value)+'</span>' : '-';
|
||||
}
|
||||
|
||||
function openTunnelDialog(){
|
||||
$("#form-tunnel")[0].reset();
|
||||
$("#form-tunnel").data("bootstrapValidator").resetForm();
|
||||
$("#modal-tunnel").modal('show');
|
||||
}
|
||||
|
||||
function submitTunnel(){
|
||||
$("#form-tunnel").data("bootstrapValidator").validate();
|
||||
if(!$("#form-tunnel").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/add/{$accountId}',
|
||||
data: $("#form-tunnel").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-tunnel").modal('hide');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTunnel(tunnelId, tunnelName){
|
||||
layer.confirm('确定要删除 Tunnel '+tunnelName+' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/delete/{$accountId}',
|
||||
data: {tunnel_id: tunnelId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showToken(tunnelId, tunnelName){
|
||||
$("#tokenTunnelName").val(tunnelName + ' [' + tunnelId + ']');
|
||||
$("#tokenValue").val('');
|
||||
$("#tokenCommand").val('');
|
||||
$("#modal-token").modal('show');
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/token/{$accountId}',
|
||||
data: {tunnel_id: tunnelId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
var token = (res.data && res.data.token) ? res.data.token : '';
|
||||
$("#tokenValue").val(token);
|
||||
$("#tokenCommand").val('cloudflared tunnel run --token ' + token);
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyTokenCommand(){
|
||||
copyPlainText($("#tokenCommand").val());
|
||||
}
|
||||
|
||||
function openPublicHostnames(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#publicTitle").text('公网主机名 - ' + tunnelName);
|
||||
$("#form-public")[0].reset();
|
||||
$("#modal-public").modal('show');
|
||||
$("#publicTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/publichostnames/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function savePublicHostname(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-public").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/publichostnames/save/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#publicTable").bootstrapTable('refresh');
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletePublicHostname(hostname, path){
|
||||
layer.confirm('确定要删除公网主机名 '+hostname+' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/publichostnames/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, hostname: hostname, path: path},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-public").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#publicTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openCidrRoutes(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#cidrTitle").text('CIDR 路由 - ' + tunnelName);
|
||||
$("#form-cidr")[0].reset();
|
||||
$("#modal-cidr").modal('show');
|
||||
$("#cidrTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/cidr/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function saveCidrRoute(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-cidr").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/cidr/add/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#cidrTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCidrRoute(routeId){
|
||||
layer.confirm('确定要删除该 CIDR 路由吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/cidr/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, route_id: routeId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-cidr").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#cidrTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openHostnameRoutes(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#hostnameRouteTitle").text('主机名路由 - ' + tunnelName);
|
||||
$("#form-hostname-route")[0].reset();
|
||||
$("#modal-hostname-route").modal('show');
|
||||
$("#hostnameRouteTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/hostnameroutes/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function saveHostnameRoute(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-hostname-route").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/hostnameroutes/add/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#hostnameRouteTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteHostnameRoute(routeId){
|
||||
layer.confirm('确定要删除该主机名路由吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/hostnameroutes/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, route_id: routeId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-hostname-route").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#hostnameRouteTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPlainText(text){
|
||||
var temp = document.createElement('textarea');
|
||||
temp.style.position = 'absolute';
|
||||
temp.style.left = '-9999px';
|
||||
temp.value = text || '';
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(temp);
|
||||
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
|
||||
}
|
||||
|
||||
function escapeJs(str){
|
||||
return String(str || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
function htmlEscape(str){
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -9,7 +9,7 @@
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<div class="form-group">
|
||||
<label>搜索</label>
|
||||
<input type="text" class="form-control" name="kw" placeholder="AccessKey或备注">
|
||||
<input type="text" class="form-control" name="kw" placeholder="账户名称或备注">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新域名账户列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
@@ -29,6 +29,7 @@
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var userLevel = "{$user['level']|default=''}";
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
const defaultPageSize = 15;
|
||||
@@ -54,7 +55,7 @@ $(document).ready(function(){
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: 'AccessKey'
|
||||
title: '账户名称'
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
@@ -69,6 +70,10 @@ $(document).ready(function(){
|
||||
title: '操作',
|
||||
formatter: function(value, row, index) {
|
||||
var html = '<a href="/account/edit?id='+row.id+'" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a> <a href="/domain?aid='+row.id+'" class="btn btn-default btn-xs">域名</a>';
|
||||
var rowType = String(row.type || '').toLowerCase();
|
||||
if(userLevel == '2' && rowType === 'cloudflare'){
|
||||
html += ' <a href="/cloudflare/tunnels/'+row.id+'" class="btn btn-default btn-xs">Tunnels</a>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<label class="col-sm-3 control-label no-padding-right" is-required>账户类型</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="type" class="form-control" v-model="set.type">
|
||||
<option v-for="(item, key) in typeList" :value="key">{{item.name}}</option>
|
||||
<option v-for="(item, key) in typeList" :value="key" :data-icon="item.icon">{{item.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +95,8 @@
|
||||
{block name="script"}
|
||||
<script src="/static/js/vue-2.7.16.min.js"></script>
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script>
|
||||
var info = {$info|json_encode|raw};
|
||||
@@ -163,8 +165,28 @@ new Vue({
|
||||
this.set.type = Object.keys(typeList)[0]
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.$nextTick(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
function formatType(option) {
|
||||
if (!option.id) return option.text;
|
||||
var icon = $(option.element).data('icon');
|
||||
if (icon) {
|
||||
return $('<span><img src="/static/images/' + icon + '" class="type-logo" />' + option.text + '</span>');
|
||||
}
|
||||
return option.text;
|
||||
}
|
||||
$('select[name=type]').select2({
|
||||
templateResult: formatType,
|
||||
templateSelection: formatType,
|
||||
minimumResultsForSearch: Infinity,
|
||||
width: '100%'
|
||||
}).on('select2:select', function(e){
|
||||
that.set.type = e.params.data.id;
|
||||
});
|
||||
if(that.action == 'edit'){
|
||||
$('select[name=type]').val(that.set.type).trigger('change.select2');
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
132
app/view/domain/alias.html
Normal file
132
app/view/domain/alias.html
Normal file
@@ -0,0 +1,132 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}域名别名 - {$domainName}{/block}
|
||||
{block name="main"}
|
||||
<style>
|
||||
.table-bordered>tbody>tr>td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:200px;vertical-align:middle;}
|
||||
</style>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h3 class="panel-title"><a href="/record/{$domainId}" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>域名别名 - {$domainName}</h3></div>
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fa fa-info-circle"></i> 域名别名使用完全相同的解析记录,免除重复操作,仅支持专业版及以上套餐
|
||||
</div>
|
||||
|
||||
<form id="form-add" class="form-inline" onsubmit="return addAlias()">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="alias" id="aliasInput" placeholder="请输入想要绑定的别名" required style="min-width:280px;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-plus"></i> 添加别名</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
|
||||
<table id="listTable" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>域名别名</th>
|
||||
<th>域名状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{volist name="domainAliasList" id="item"}
|
||||
<tr data-id="{$item.Id}">
|
||||
<td>{$item.DomainAlias}</td>
|
||||
<td>
|
||||
{if isset($item.Status)}
|
||||
{if $item.Status == '2'}
|
||||
<font color="green"><i class="fa fa-check-circle"></i> 正常</font>
|
||||
{elseif $item.Status == '3'}
|
||||
<font color="red"><i class="fa fa-ban"></i> 封禁</font>
|
||||
{else/}
|
||||
<font color="#b5bbc8"><i class="fa fa-pause-circle"></i> DNS不正确</font>
|
||||
{/if}
|
||||
{else/}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a href="javascript:delAlias({$item.Id})" class="btn btn-danger btn-xs"><i class="fa fa-trash"></i> 删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
{empty name="domainAliasList"}
|
||||
<tr><td colspan="3" class="text-center text-muted">暂无域名别名</td></tr>
|
||||
{/empty}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var domainId = {$domainId};
|
||||
|
||||
function addAlias() {
|
||||
var alias = $("#aliasInput").val().trim();
|
||||
if (!alias) {
|
||||
layer.msg('请输入想要绑定的别名');
|
||||
return false;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/alias/' + domainId + '?act=add',
|
||||
data: { alias: alias },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
layer.close(ii);
|
||||
if (data.code == 0) {
|
||||
layer.alert(data.msg, { icon: 1 }, function() {
|
||||
layer.closeAll();
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.alert(data.msg || '添加失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.close(ii);
|
||||
layer.alert('网络请求失败', { icon: 2 });
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function delAlias(id) {
|
||||
layer.confirm('确定要删除该域名别名吗?', { icon: 3, title: '提示' }, function(idx) {
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/alias/' + domainId + '?act=delete',
|
||||
data: { alias_id: id },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
layer.close(ii);
|
||||
layer.close(idx);
|
||||
if (data.code == 0) {
|
||||
layer.alert(data.msg, { icon: 1 }, function() {
|
||||
layer.closeAll();
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
layer.alert(data.msg || '删除失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.close(ii);
|
||||
layer.close(idx);
|
||||
layer.alert('网络请求失败', { icon: 2 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -146,7 +146,7 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新域名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
{if request()->user['level'] eq 2}<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
|
||||
{if $user['level'] eq 2}<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('updateexpire')">刷新到期时间</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li></ul>
|
||||
@@ -172,7 +172,7 @@
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var userLevel = "{:request()->user['level']}";
|
||||
var userLevel = "{$user['level']|default=''}";
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
const defaultPageSize = getCookie('domain_pagesize') ? getCookie('domain_pagesize') : 15;
|
||||
|
||||
@@ -95,6 +95,7 @@ new Vue({
|
||||
},
|
||||
async getDomainList(){
|
||||
this.domainList = [];
|
||||
this.page = 1;
|
||||
while(true){
|
||||
try{
|
||||
layer.msg('正在获取第'+this.page+'页域名', {icon: 16, shade: 0.01});
|
||||
|
||||
@@ -55,12 +55,12 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
|
||||
<input type="text" class="form-control" name="value" placeholder="输入记录值" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display:none" id="mx_type">
|
||||
{if $dnsconfig.type!='huawei'}<div class="form-group" style="display:none" id="mx_type">
|
||||
<label class="col-sm-3 control-label no-padding-right">MX优先级</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="mx" value="10">
|
||||
</div>
|
||||
</div>
|
||||
</div>{/if}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">TTL</label>
|
||||
<div class="col-sm-9">
|
||||
@@ -183,7 +183,9 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新解析记录列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加记录</a>
|
||||
{if $dnsconfig.type=='cloudflare' && $user['level'] eq 2}<a href="/cloudflare/hostnames/{$domainId}" class="btn btn-default">Cloudflare增强</a>{/if}
|
||||
{if $dnsconfig.type=='aliyun'}<a href="/record/weight/{$domainId}" class="btn btn-default">权重配置</a>{/if}
|
||||
{if $dnsconfig.type=='dnspod'}<a href="/record/alias/{$domainId}" class="btn btn-default">域名别名</a>{/if}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu"><li><a href="/record/batchadd/{$domainId}">添加</a></li><li><a href="javascript:operation('open')">启用</a></li><li><a href="javascript:operation('pause')">暂停</a></li><li><a href="javascript:operation('edit')">修改记录</a></li><li><a href="javascript:operation('editline')">修改线路</a></li>{if $dnsconfig.remark == 1}<li><a href="javascript:operation('editremark')">修改备注</a></li>{/if}<li><a href="javascript:operation('delete')">删除</a></li></ul>
|
||||
@@ -291,7 +293,7 @@ $(document).ready(function(){
|
||||
title: '记录值',
|
||||
formatter: function(value, row, index) {
|
||||
var copyId = 'copy-value-' + row.RecordId;
|
||||
if(row.Type == 'MX') {
|
||||
if(row.Type == 'MX' && dnsconfig.type!='huawei') {
|
||||
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
|
||||
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>'
|
||||
+ '<span class="mx-priority"> | '+row.MX+'</span>';
|
||||
|
||||
@@ -54,14 +54,14 @@
|
||||
<label class="col-sm-3 control-label no-padding-right">解析IP类型<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="同时开启IPv6&IPv4将会请求2次接口消耗双倍积分"><i class="fa fa-question-circle"></i></span></label>
|
||||
<div class="col-sm-6">
|
||||
<label class="checkbox-inline" v-for="option in iptypeList">
|
||||
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" required> {{option.label}}
|
||||
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" :disabled="option.value=='v6' && isXingpingcn" required> {{option.label}}<span v-if="option.value=='v6' && isXingpingcn" class="text-muted">(xingpingcn.top不支持)</span>
|
||||
</label><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">每线路解析数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="数量不要超过当前域名套餐允许的最大数量,否则会添加解析失败"><i class="fa fa-question-circle"></i></span></label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="5" required>
|
||||
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="50" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -110,6 +110,7 @@ new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
action: '{$action}',
|
||||
optimize_ip_api: '{:config_get("optimize_ip_api", 0)}',
|
||||
set: {
|
||||
id: '',
|
||||
remark: '',
|
||||
@@ -132,8 +133,18 @@ new Vue({
|
||||
4:'EdgeOne'
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isXingpingcn: function() {
|
||||
return this.optimize_ip_api == '2';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'set.ip_type_select': function(val){
|
||||
// 如果使用xingpingcn.top接口,自动移除v6
|
||||
if(this.isXingpingcn && val.includes('v6')){
|
||||
this.set.ip_type_select = val.filter(v => v !== 'v6');
|
||||
return;
|
||||
}
|
||||
this.set.ip_type = val.join(',');
|
||||
}
|
||||
},
|
||||
@@ -185,4 +196,4 @@ new Vue({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{/block}
|
||||
{/block}
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
<div class="panel-heading"><h3 class="panel-title">使用说明</h3></div>
|
||||
<div class="panel-body">
|
||||
<p><li>不支持对CloudFlare里的域名添加优选,必须使用其他DNS服务商。需开通Cloudflare for SaaS,且域名使用CNAME的方式解析到CloudFlare。</li></p>
|
||||
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne;<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
|
||||
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne;<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare;<a href="https://github.com/xingpingcn/enhanced-FaaS-in-China" target="_blank" rel="noreferrer">xingpingcn.top</a> 只支持CloudFlare(免费、无需密钥)。</li></p>
|
||||
<p><li>接口密钥:默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
|
||||
<p><li>代理地址:如 https://ghfast.top/https://raw.githubusercontent.com/ ,留空则直接访问 https://raw.githubusercontent.com/。</li></p>
|
||||
<p><li>自动更新:可查看<a href="/system/cronset">计划任务设置</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,19 +25,23 @@
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">数据接口设置</h3></div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
|
||||
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form" id="apiSettingForm">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">数据接口</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option></select></div>
|
||||
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" id="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option><option value="2">xingpingcn.top</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="keyGroup">
|
||||
<label class="col-sm-3 control-label">接口密钥</label>
|
||||
<div class="col-sm-9"><input type="text" name="optimize_ip_key" value="{:config_get('optimize_ip_key', 'o1zrmHAF')}" class="form-control"/></div>
|
||||
</div>
|
||||
<div class="form-group" id="proxyGroup" style="display:none;">
|
||||
<label class="col-sm-3 control-label">代理地址</label>
|
||||
<div class="col-sm-9"><input type="text" name="optimize_ip_proxy" value="{:config_get('optimize_ip_proxy', '')}" class="form-control" placeholder="留空则直接访问GitHub"/></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-9">
|
||||
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
|
||||
<a href="javascript:queryapi()" class="btn btn-default btn-block">查询积分</a>
|
||||
<a href="javascript:queryapi()" class="btn btn-default btn-block" id="queryBtn">查询积分</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -69,6 +74,25 @@ var items = $("select[default]");
|
||||
for (i = 0; i < items.length; i++) {
|
||||
$(items[i]).val($(items[i]).attr("default")||0);
|
||||
}
|
||||
// 切换接口时显示/隐藏对应设置项
|
||||
function toggleApiSettings(){
|
||||
var api = $("#optimize_ip_api").val();
|
||||
if(api == '2'){
|
||||
$("#keyGroup").hide();
|
||||
$("#proxyGroup").show();
|
||||
$("#queryBtn").hide();
|
||||
}else{
|
||||
$("#keyGroup").show();
|
||||
$("#proxyGroup").hide();
|
||||
$("#queryBtn").show();
|
||||
}
|
||||
}
|
||||
$("#optimize_ip_api").change(function(){
|
||||
toggleApiSettings();
|
||||
});
|
||||
// 页面加载时初始化
|
||||
toggleApiSettings();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
function saveSetting(obj){
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
@@ -118,4 +142,4 @@ function queryapi(){
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
{/block}
|
||||
|
||||
111
composer.lock
generated
111
composer.lock
generated
@@ -8,16 +8,16 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "cccyun/php-whois",
|
||||
"version": "1.2",
|
||||
"version": "1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/netcccyun/php-whois.git",
|
||||
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1"
|
||||
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/c631f1c5e26e7150501a14cd25a2380f8a077ca1",
|
||||
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1",
|
||||
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/f02627ba0bef005aa9e336d63541f9fd288675b5",
|
||||
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -62,9 +62,9 @@
|
||||
"црщшы"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/netcccyun/php-whois/tree/1.2"
|
||||
"source": "https://github.com/netcccyun/php-whois/tree/1.3"
|
||||
},
|
||||
"time": "2025-06-25T06:54:23+00:00"
|
||||
"time": "2026-02-12T05:56:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cccyun/think-captcha",
|
||||
@@ -329,16 +329,16 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "2.8.0",
|
||||
"version": "2.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "21dc724a0583619cd1652f673303492272778051"
|
||||
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
|
||||
"reference": "21dc724a0583619cd1652f673303492272778051",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884",
|
||||
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -354,6 +354,7 @@
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"http-interop/http-factory-tests": "0.9.0",
|
||||
"jshttp/mime-db": "1.54.0.1",
|
||||
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -425,7 +426,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/psr7/issues",
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.8.0"
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.9.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -441,7 +442,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-23T21:21:41+00:00"
|
||||
"time": "2026-03-10T16:41:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
@@ -952,16 +953,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1011,7 +1012,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1031,11 +1032,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
@@ -1098,7 +1099,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1122,7 +1123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -1183,7 +1184,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1207,16 +1208,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1268,7 +1269,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1288,11 +1289,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-12-23T08:48:59+00:00"
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -1348,7 +1349,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1372,16 +1373,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692"
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1428,7 +1429,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1448,32 +1449,32 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.3.5",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc"
|
||||
"reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc",
|
||||
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883",
|
||||
"reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0"
|
||||
"symfony/console": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
@@ -1504,7 +1505,7 @@
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.5"
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1524,7 +1525,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-27T09:00:46+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/framework",
|
||||
@@ -1908,16 +1909,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.3.5",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
|
||||
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1929,10 +1930,10 @@
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/process": "^6.4|^7.0",
|
||||
"symfony/uid": "^6.4|^7.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/uid": "^6.4|^7.0|^8.0",
|
||||
"twig/twig": "^3.12"
|
||||
},
|
||||
"bin": [
|
||||
@@ -1971,7 +1972,7 @@
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1991,7 +1992,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-27T09:00:46+00:00"
|
||||
"time": "2026-03-30T13:44:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-trace",
|
||||
@@ -2062,5 +2063,5 @@
|
||||
"ext-ssh2": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ return [
|
||||
'show_error_msg' => true,
|
||||
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
|
||||
|
||||
'version' => '1046',
|
||||
'version' => '1049',
|
||||
|
||||
'dbversion' => '1045'
|
||||
'dbversion' => '1048'
|
||||
];
|
||||
|
||||
6
public/static/images/acepanel.svg
Normal file
6
public/static/images/acepanel.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="812" height="812">
|
||||
<rect x="0" y="0" width="812" height="812" rx="80" ry="80" fill="#333333"/>
|
||||
<path d="M0,0 L136,0 L138,23 L141,47 L146,91 L152,146 L155,169 L157,188 L161,222 L163,243 L167,275 L169,294 L173,329 L176,355 L178,372 L178,377 L360,377 L354,399 L342,439 L333,470 L324,501 L321,509 L302,509 L307,504 L313,497 L313,494 L296,481 L293,481 L286,489 L277,494 L270,496 L261,496 L253,493 L248,487 L246,481 L246,474 L247,473 L276,470 L296,464 L307,458 L314,451 L318,443 L320,430 L318,420 L313,412 L306,406 L294,401 L290,400 L267,400 L254,404 L243,410 L233,419 L223,434 L217,450 L215,464 L215,478 L217,490 L222,502 L226,507 L226,509 L187,509 L193,501 L199,492 L199,488 L179,479 L176,478 L169,488 L164,493 L157,496 L150,496 L142,493 L138,488 L136,480 L138,464 L142,445 L146,435 L151,429 L157,426 L162,425 L169,425 L176,428 L181,436 L182,439 L187,438 L208,430 L207,422 L201,412 L193,406 L182,401 L175,400 L155,400 L143,404 L132,410 L122,419 L115,430 L110,440 L107,450 L105,468 L106,488 L111,500 L116,509 L86,509 L84,496 L81,460 L78,432 L74,385 L-27,385 L-22,373 L-7,342 L1,324 L9,308 L14,297 L67,297 L66,293 L62,236 L60,213 L57,169 L52,104 L52,95 L45,95 L43,101 L27,136 L11,171 L-7,210 L-26,251 L-42,285 L-50,304 L-60,326 L-68,342 L-76,359 L-84,375 L-96,400 L-114,437 L-125,460 L-137,485 L-148,508 L-149,509 L-261,509 L-259,503 L-245,475 L-235,456 L-212,411 L-202,392 L-189,366 L-179,347 L-159,308 L-145,281 L-137,266 L-129,250 L-105,203 L-91,176 L-82,159 L-74,143 L-51,98 L-34,65 L-19,36 L-1,1 Z " fill="#FEFEFE" transform="translate(372,151)"/>
|
||||
<path d="M0,0 L11,0 L17,3 L19,6 L19,15 L14,21 L4,25 L-14,28 L-20,28 L-18,17 L-13,9 L-5,2 Z " fill="#FBFBFB" transform="translate(642,575)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/static/images/amh.ico
Normal file
BIN
public/static/images/amh.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -51,6 +51,31 @@ Route::group(function () {
|
||||
Route::get('/account/:action', 'domain/account_add');
|
||||
Route::get('/account', 'domain/account');
|
||||
|
||||
Route::get('/cloudflare/hostnames/:id', 'cloudflare/hostnames');
|
||||
Route::post('/cloudflare/hostnames/data/:id', 'cloudflare/hostnames_data');
|
||||
Route::post('/cloudflare/hostnames/add/:id', 'cloudflare/hostnames_add');
|
||||
Route::post('/cloudflare/hostnames/update/:id', 'cloudflare/hostnames_update');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/delete/:id', 'cloudflare/hostnames_delete');
|
||||
Route::post('/cloudflare/hostnames/txttargets/:id', 'cloudflare/hostnames_txt_targets');
|
||||
Route::post('/cloudflare/fallback/get/:id', 'cloudflare/fallback_get');
|
||||
Route::post('/cloudflare/fallback/set/:id', 'cloudflare/fallback_set');
|
||||
Route::post('/cloudflare/fallback/delete/:id', 'cloudflare/fallback_delete');
|
||||
Route::get('/cloudflare/tunnels/:id', 'cloudflare/tunnels');
|
||||
Route::post('/cloudflare/tunnels/data/:id', 'cloudflare/tunnels_data');
|
||||
Route::post('/cloudflare/tunnels/add/:id', 'cloudflare/tunnels_add');
|
||||
Route::post('/cloudflare/tunnels/delete/:id', 'cloudflare/tunnels_delete');
|
||||
Route::post('/cloudflare/tunnels/token/:id', 'cloudflare/tunnels_token');
|
||||
Route::post('/cloudflare/tunnels/publichostnames/data/:id', 'cloudflare/tunnels_public_hostnames_data');
|
||||
Route::post('/cloudflare/tunnels/publichostnames/save/:id', 'cloudflare/tunnels_public_hostnames_save');
|
||||
Route::post('/cloudflare/tunnels/publichostnames/delete/:id', 'cloudflare/tunnels_public_hostnames_delete');
|
||||
Route::post('/cloudflare/tunnels/cidr/data/:id', 'cloudflare/tunnels_cidr_data');
|
||||
Route::post('/cloudflare/tunnels/cidr/add/:id', 'cloudflare/tunnels_cidr_add');
|
||||
Route::post('/cloudflare/tunnels/cidr/delete/:id', 'cloudflare/tunnels_cidr_delete');
|
||||
Route::post('/cloudflare/tunnels/hostnameroutes/data/:id', 'cloudflare/tunnels_hostname_routes_data');
|
||||
Route::post('/cloudflare/tunnels/hostnameroutes/add/:id', 'cloudflare/tunnels_hostname_routes_add');
|
||||
Route::post('/cloudflare/tunnels/hostnameroutes/delete/:id', 'cloudflare/tunnels_hostname_routes_delete');
|
||||
|
||||
Route::any('/domain/expirenotice', 'domain/expire_notice');
|
||||
Route::post('/domain/updatedate', 'domain/update_date');
|
||||
Route::post('/domain/data', 'domain/domain_data');
|
||||
@@ -74,6 +99,7 @@ Route::group(function () {
|
||||
Route::post('/record/list', 'domain/record_list');
|
||||
Route::post('/record/weight/data/:id', 'domain/weight_data');
|
||||
Route::any('/record/weight/:id', 'domain/weight');
|
||||
Route::any('/record/alias/:id', 'domain/alias');
|
||||
Route::get('/record/:id', 'domain/record');
|
||||
|
||||
Route::get('/dmonitor/overview', 'dmonitor/overview');
|
||||
|
||||
Reference in New Issue
Block a user