mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-09 15:06:28 +02:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532cecc3bf | ||
|
|
91864aa6be | ||
|
|
9403875044 | ||
|
|
5d53d46659 | ||
|
|
c73f9cd536 | ||
|
|
97dfc1f12f | ||
|
|
a5ec8a3ff6 | ||
|
|
12bdb6cb67 | ||
|
|
a99e3b8642 | ||
|
|
a1cfd470d9 | ||
|
|
945d91386c | ||
|
|
668e2b4ceb | ||
|
|
75a8aa97b8 | ||
|
|
29bcd293ef | ||
|
|
b267d3df86 | ||
|
|
50edcd6dac | ||
|
|
04980fcdd3 | ||
|
|
07a0f54bc1 | ||
|
|
db418c7a11 | ||
|
|
8e4848c14c | ||
|
|
8cbc1f9a18 | ||
|
|
ccda489e81 | ||
|
|
45af1ad464 | ||
|
|
7e49a40057 | ||
|
|
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 |
85
.github/docker/Dockerfile
vendored
Normal file
85
.github/docker/Dockerfile
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
|
||||
# CACHE_BUST 须写进每条相关 RUN,否则 GHA/BuildKit 可能单独命中 composer 相关层缓存,vendor 仍来自旧构建
|
||||
ARG CACHE_BUST=local
|
||||
|
||||
# Add application
|
||||
RUN mkdir -p /usr/src && echo "$CACHE_BUST" >/dev/null && 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(与下面 install 一并随 CACHE_BUST 失效)
|
||||
RUN echo "$CACHE_BUST" >/dev/null && wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
|
||||
|
||||
RUN echo "$CACHE_BUST" >/dev/null && composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader --no-cache
|
||||
|
||||
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 "$@"
|
||||
59
.github/workflows/docker-build.yml
vendored
Normal file
59
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
# 手动触发:构建多架构镜像(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
|
||||
# 每次运行唯一,打破「下载源码 + composer」等层的缓存,否则会一直用首次构建时的层
|
||||
build-args: |
|
||||
CACHE_BUST=${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
# 避免向仓库推送 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 . '未在本系统添加'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,7 +751,7 @@ class Cert extends BaseController
|
||||
$ids = input('post.ids');
|
||||
$success = 0;
|
||||
$certid = 0;
|
||||
if (input('post.action') == 'cert') {
|
||||
if (input('post.act') == 'cert') {
|
||||
$certid = input('post.certid/d');
|
||||
$cert = Db::name('cert_order')->where('id', $certid)->find();
|
||||
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);
|
||||
|
||||
1343
app/controller/Cloudflare.php
Normal file
1343
app/controller/Cloudflare.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use think\facade\View;
|
||||
use think\facade\Cache;
|
||||
use app\lib\DnsHelper;
|
||||
use app\service\ExpireNoticeService;
|
||||
use app\utils\DnsQueryUtils;
|
||||
use Exception;
|
||||
|
||||
class Domain extends BaseController
|
||||
@@ -157,8 +158,10 @@ class Domain extends BaseController
|
||||
}
|
||||
$accounts[] = ['id' => $row['id'], 'name' => $name, 'type' => DnsHelper::$dns_config[$row['type']]['name'], 'add' => DnsHelper::$dns_config[$row['type']]['add']];
|
||||
}
|
||||
$categorys = Db::name('domain_category')->order('sort', 'asc')->order('id', 'desc')->select();
|
||||
View::assign('accounts', $accounts);
|
||||
View::assign('types', $types);
|
||||
View::assign('categorys', $categorys);
|
||||
return view();
|
||||
}
|
||||
|
||||
@@ -188,6 +191,7 @@ class Domain extends BaseController
|
||||
$kw = input('post.kw', null, 'trim');
|
||||
$type = input('post.type', null, 'trim');
|
||||
$status = input('post.status', null, 'trim');
|
||||
$cid = input('post.cid', null, 'trim');
|
||||
$order = input('post.order', null, 'trim');
|
||||
$offset = input('post.offset/d', 0);
|
||||
$limit = input('post.limit/d', 10);
|
||||
@@ -206,6 +210,9 @@ class Domain extends BaseController
|
||||
if (!empty($type)) {
|
||||
$select->whereLike('B.type', $type);
|
||||
}
|
||||
if (!isNullOrEmpty($cid)) {
|
||||
$select->where('A.cid', $cid);
|
||||
}
|
||||
if (request()->user['level'] == 1) {
|
||||
$select->where('is_hide', 0)->where('A.name', 'in', request()->user['permission']);
|
||||
}
|
||||
@@ -235,10 +242,12 @@ class Domain extends BaseController
|
||||
}
|
||||
$rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->limit($offset, $limit)->select();
|
||||
|
||||
$categorys = Db::name('domain_category')->column('name', 'id');
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
|
||||
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
|
||||
$row['category_name'] = isset($categorys[$row['cid']]) ? $categorys[$row['cid']] : '';
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
@@ -290,6 +299,7 @@ class Domain extends BaseController
|
||||
$is_hide = input('post.is_hide/d');
|
||||
$is_sso = input('post.is_sso/d');
|
||||
$is_notice = input('post.is_notice/d');
|
||||
$cid = input('post.cid/d', 0);
|
||||
$expiretime = input('post.expiretime', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
if (empty($remark)) $remark = null;
|
||||
@@ -297,6 +307,7 @@ class Domain extends BaseController
|
||||
'is_hide' => $is_hide,
|
||||
'is_sso' => $is_sso,
|
||||
'is_notice' => $is_notice,
|
||||
'cid' => $cid,
|
||||
'expiretime' => $expiretime ? $expiretime : null,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
@@ -305,6 +316,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();
|
||||
@@ -1004,6 +1016,68 @@ class Domain extends BaseController
|
||||
return view('log');
|
||||
}
|
||||
|
||||
public function smartparse()
|
||||
{
|
||||
if (request()->user['type'] == 'domain') {
|
||||
return redirect('/record/' . request()->user['id']);
|
||||
}
|
||||
|
||||
$list = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')
|
||||
->field('A.id, A.name, A.aid, B.type')
|
||||
->order('A.name', 'asc')
|
||||
->select();
|
||||
|
||||
$domainList = [];
|
||||
foreach ($list as $row) {
|
||||
if (request()->user['level'] == 1 && !in_array($row['name'], request()->user['permission'])) {
|
||||
continue;
|
||||
}
|
||||
$dnsTypeName = isset(DnsHelper::$dns_config[$row['type']]) ? DnsHelper::$dns_config[$row['type']]['name'] : $row['type'];
|
||||
$domainList[] = [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'dnsType' => $dnsTypeName
|
||||
];
|
||||
}
|
||||
|
||||
View::assign('domainList', $domainList);
|
||||
return view();
|
||||
}
|
||||
|
||||
public function quickinfo()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$drow = Db::name('domain')->where('id', $id)->find();
|
||||
if (!$drow) {
|
||||
return json(['code' => -1, 'msg' => '域名不存在']);
|
||||
}
|
||||
if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']);
|
||||
|
||||
try {
|
||||
list($recordLine, $minTTL) = $this->get_line_and_ttl($drow);
|
||||
|
||||
$recordLineArr = [];
|
||||
foreach ($recordLine as $key => $item) {
|
||||
$recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']];
|
||||
}
|
||||
|
||||
$dnstype = Db::name('account')->where('id', $drow['aid'])->value('type');
|
||||
$dnsconfig = DnsHelper::$dns_config[$dnstype];
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'recordLine' => $recordLineArr,
|
||||
'minTTL' => $minTTL ? $minTTL : 1,
|
||||
'weight' => $dnsconfig['weight'] ?? false,
|
||||
'remark' => $dnsconfig['remark'] ?? 0
|
||||
]
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function add_log($domain, $action, $data)
|
||||
{
|
||||
if (strlen($data) > 500) $data = substr($data, 0, 500);
|
||||
@@ -1106,8 +1180,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()
|
||||
{
|
||||
@@ -1137,4 +1291,138 @@ class Domain extends BaseController
|
||||
$result = (new ExpireNoticeService())->updateDomainDate($id, $drow['name']);
|
||||
return json($result);
|
||||
}
|
||||
|
||||
public function record_check()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$drow = Db::name('domain')->where('id', $id)->find();
|
||||
if (!$drow) {
|
||||
return json(['code' => -1, 'msg' => '域名不存在']);
|
||||
}
|
||||
if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']);
|
||||
|
||||
$recordid = input('post.recordid', null, 'trim');
|
||||
$name = input('post.name', null, 'trim');
|
||||
$type = input('post.type', null, 'trim');
|
||||
$value = input('post.value', null, 'trim');
|
||||
|
||||
if (empty($recordid) || empty($name) || empty($type)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
|
||||
$domain = $name === '@' ? $drow['name'] : $name . '.' . $drow['name'];
|
||||
$domain = strtolower($domain);
|
||||
|
||||
$supported_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'PTR', 'LOC', 'LUA'];
|
||||
if (!in_array($type, $supported_types)) {
|
||||
return json(['code' => -1, 'msg' => '该记录类型暂不支持检测']);
|
||||
}
|
||||
|
||||
$dns_records = DnsQueryUtils::get_dns_records($domain, $type);
|
||||
if ($dns_records === false || empty($dns_records)) {
|
||||
$dns_records = DnsQueryUtils::query_dns_doh($domain, $type);
|
||||
}
|
||||
|
||||
if ($dns_records === false || empty($dns_records)) {
|
||||
return json(['code' => 0, 'data' => ['status' => 'not_found', 'message' => '未查询到该解析记录', 'actual' => []]]);
|
||||
}
|
||||
|
||||
$dns_records = array_map('strtolower', $dns_records);
|
||||
$expected_value = strtolower(rtrim(trim($value), '.'));
|
||||
|
||||
if (in_array($expected_value, $dns_records)) {
|
||||
return json(['code' => 0, 'data' => ['status' => 'active', 'actual' => $dns_records]]);
|
||||
} else {
|
||||
return json(['code' => 0, 'data' => ['status' => 'mismatch', 'expected' => $expected_value, 'actual' => $dns_records]]);
|
||||
}
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
return view();
|
||||
}
|
||||
|
||||
public function category_data()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]);
|
||||
$offset = input('post.offset/d', 0);
|
||||
$limit = input('post.limit/d', 10);
|
||||
|
||||
$select = Db::name('domain_category');
|
||||
$total = $select->count();
|
||||
$rows = $select->order('sort', 'asc')->order('id', 'desc')->limit($offset, $limit)->select()->toArray();
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['domain_count'] = Db::name('domain')->where('cid', $row['id'])->count();
|
||||
}
|
||||
|
||||
return json(['total' => $total, 'rows' => $rows]);
|
||||
}
|
||||
|
||||
public function category_op()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$action = input('param.action');
|
||||
if ($action == 'add') {
|
||||
$name = input('post.name', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$sort = input('post.sort/d', 0);
|
||||
if (empty($name)) return json(['code' => -1, 'msg' => '分类名称不能为空']);
|
||||
if (Db::name('domain_category')->where('name', $name)->find()) {
|
||||
return json(['code' => -1, 'msg' => '分类名称已存在']);
|
||||
}
|
||||
Db::name('domain_category')->insert([
|
||||
'name' => $name,
|
||||
'remark' => $remark,
|
||||
'sort' => $sort,
|
||||
'addtime' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return json(['code' => 0, 'msg' => '添加分类成功!']);
|
||||
} elseif ($action == 'edit') {
|
||||
$id = input('post.id/d');
|
||||
$row = Db::name('domain_category')->where('id', $id)->find();
|
||||
if (!$row) return json(['code' => -1, 'msg' => '分类不存在']);
|
||||
$name = input('post.name', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$sort = input('post.sort/d', 0);
|
||||
if (empty($name)) return json(['code' => -1, 'msg' => '分类名称不能为空']);
|
||||
if (Db::name('domain_category')->where('name', $name)->where('id', '<>', $id)->find()) {
|
||||
return json(['code' => -1, 'msg' => '分类名称已存在']);
|
||||
}
|
||||
Db::name('domain_category')->where('id', $id)->update([
|
||||
'name' => $name,
|
||||
'remark' => $remark,
|
||||
'sort' => $sort,
|
||||
]);
|
||||
return json(['code' => 0, 'msg' => '修改分类成功!']);
|
||||
} elseif ($action == 'del') {
|
||||
$id = input('post.id/d');
|
||||
$count = Db::name('domain')->where('cid', $id)->count();
|
||||
if ($count > 0) return json(['code' => -1, 'msg' => '该分类下存在域名,无法删除']);
|
||||
Db::name('domain_category')->where('id', $id)->delete();
|
||||
return json(['code' => 0, 'msg' => '删除分类成功!']);
|
||||
}
|
||||
return json(['code' => -3]);
|
||||
}
|
||||
|
||||
public function category_list()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$list = Db::name('domain_category')->order('sort', 'asc')->order('id', 'desc')->select();
|
||||
foreach ($list as &$row) {
|
||||
$row['domain_count'] = Db::name('domain')->where('cid', $row['id'])->count();
|
||||
}
|
||||
return json(['code' => 0, 'data' => $list]);
|
||||
}
|
||||
|
||||
public function domain_set_category()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$ids = input('post.ids');
|
||||
$cid = input('post.cid/d', 0);
|
||||
if (empty($ids)) return json(['code' => -1, 'msg' => '请选择要操作的域名']);
|
||||
$count = Db::name('domain')->where('id', 'in', $ids)->update(['cid' => $cid]);
|
||||
return json(['code' => 0, 'msg' => '成功设置' . $count . '个域名的分类!']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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任务已存在']);
|
||||
|
||||
@@ -92,14 +92,29 @@ class System extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function customwebhooktest()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$custom_webhook_url = config_get('custom_webhook_url');
|
||||
if (empty($custom_webhook_url)) return json(['code' => -1, 'msg' => '请先保存设置']);
|
||||
$content = "这是一封测试消息!\n来自:" . $this->request->root(true);
|
||||
$result = \app\utils\MsgNotice::send_custom_webhook('消息发送测试', $content);
|
||||
if ($result === true) {
|
||||
return json(['code' => 0, 'msg' => '消息发送成功!']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '消息发送失败!' . $result]);
|
||||
}
|
||||
}
|
||||
|
||||
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' => [
|
||||
@@ -253,6 +296,59 @@ class DeployHelper
|
||||
],
|
||||
'taskinputs' => [],
|
||||
],
|
||||
'nginxproxymanager' => [
|
||||
'name' => 'Nginx Proxy Manager',
|
||||
'class' => 1,
|
||||
'icon' => 'npm.svg',
|
||||
'desc' => '更新 Nginx Proxy Manager 的自定义证书并自动绑定 Proxy Host',
|
||||
'note' => '填写 Nginx Proxy Manager 面板地址与登录账号密码,系统将通过官方 API 登录并执行证书更新。',
|
||||
'tasknote' => '如填写证书ID则优先更新该自定义证书;留空时系统会根据当前证书订单的域名在 NPM 中匹配 Proxy Host,并在首次成功后自动保存证书ID,后续续期优先走该ID,不再依赖域名匹配。',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'Nginx Proxy Manager 面板地址',
|
||||
'note' => '填写规则如:http://192.168.1.100:81 ,不要带 /api 等后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'email' => [
|
||||
'name' => '登录邮箱',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'NPM 登录邮箱',
|
||||
'validator' => 'email',
|
||||
'required' => true,
|
||||
],
|
||||
'password' => [
|
||||
'name' => '登录密码',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'NPM 登录密码',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '留空则按域名匹配 Proxy Host 并自动回填',
|
||||
'note' => '优先级最高。填写后将直接更新该自定义证书ID;仅支持 NPM 中 provider 为 other 的自定义证书。',
|
||||
],
|
||||
'host_id' => [
|
||||
'name' => 'Proxy Host ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '可留空,留空则按域名自动匹配',
|
||||
'note' => '可选。未填写证书ID时,若填写此项则仅处理指定 Proxy Host;若留空则按当前证书订单域名自动查找匹配的 Proxy Host。',
|
||||
],
|
||||
],
|
||||
],
|
||||
'btwaf' => [
|
||||
'name' => '堡塔云WAF',
|
||||
'class' => 1,
|
||||
@@ -677,6 +773,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 +932,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,8 +1248,10 @@ 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'=>'wafres', 'label'=>'Web应用防火墙3.0(云产品接入)'],
|
||||
['value'=>'waf2', 'label'=>'Web应用防火墙2.0'],
|
||||
['value'=>'clb', 'label'=>'传统型负载均衡CLB'],
|
||||
['value'=>'alb', 'label'=>'应用型负载均衡ALB'],
|
||||
@@ -1058,6 +1262,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 +1272,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 +1304,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==\'wafres\'||product==\'ddoscoo\'||product==\'esa\'||product==\'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'regionid' => [
|
||||
@@ -1148,6 +1360,29 @@ 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,
|
||||
],
|
||||
'waf_resource_id' => [
|
||||
'name' => '云产品防护对象ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个ID可用,隔开',
|
||||
'show' => 'product==\'wafres\'',
|
||||
'note' => '进入查看防护对象,对象名称一列即为云产品防护对象ID',
|
||||
'required' => true,
|
||||
],
|
||||
'deploy_type' => [
|
||||
'name' => '部署证书类型',
|
||||
'type' => 'select',
|
||||
@@ -1156,21 +1391,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\'||product==\'wafres\'',
|
||||
'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\'&&product!=\'wafres\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1755,6 +1990,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 +2004,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 +2240,7 @@ ctrl+x 保存退出<br/>',
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -2484,6 +2737,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',
|
||||
@@ -518,6 +518,41 @@ class DnsHelper
|
||||
'page' => true,
|
||||
'add' => true,
|
||||
],
|
||||
'technitium' => [
|
||||
'name' => 'Technitium',
|
||||
'icon' => 'technitium.png',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'url' => [
|
||||
'name' => 'Server URL',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'http://127.0.0.1:5380',
|
||||
'required' => true,
|
||||
],
|
||||
'token' => [
|
||||
'name' => 'API Token',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => false,
|
||||
'page' => true,
|
||||
'add' => true,
|
||||
],
|
||||
'aliyunesa' => [
|
||||
'name' => '阿里云ESA',
|
||||
'icon' => 'aliyun.png',
|
||||
@@ -608,6 +643,47 @@ class DnsHelper
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
'dnsmgr' => [
|
||||
'name' => '同系统对接',
|
||||
'icon' => 'logo.png',
|
||||
'note' => '对接其他聚合DNS管理系统站点',
|
||||
'config' => [
|
||||
'base_url' => [
|
||||
'name' => '站点地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '例如:https://dns.example.com',
|
||||
'required' => true,
|
||||
],
|
||||
'uid' => [
|
||||
'name' => '用户 ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'key' => [
|
||||
'name' => 'API 密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
'redirect' => true,
|
||||
'log' => false,
|
||||
'weight' => true,
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
];
|
||||
|
||||
public static $line_name = [
|
||||
@@ -627,6 +703,7 @@ class DnsHelper
|
||||
'spaceship' => ['DEF' => 'default'],
|
||||
'aliyunesa' => ['DEF' => '0'],
|
||||
'tencenteo' => ['DEF' => 'Default'],
|
||||
'cccyun' => ['DEF' => 'default'],
|
||||
];
|
||||
|
||||
public static function getList()
|
||||
@@ -651,7 +728,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 +745,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);
|
||||
}
|
||||
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ class aliyun implements DeployInterface
|
||||
$this->deploy_oss($cert_id, $config);
|
||||
} elseif ($config['product'] == 'waf') {
|
||||
$this->deploy_waf($cert_id, $config);
|
||||
} elseif ($config['product'] == 'wafres') {
|
||||
$this->deploy_waf_res($cert_id, $config);
|
||||
} elseif ($config['product'] == 'waf2') {
|
||||
$this->deploy_waf2($cert_id, $config);
|
||||
} elseif ($config['product'] == 'ddoscoo') {
|
||||
@@ -66,6 +68,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 +138,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,
|
||||
];
|
||||
$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' => 'SetDcdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
'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 +269,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 +283,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 +329,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 +340,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 +353,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 +387,176 @@ class aliyun implements DeployInterface
|
||||
$instance_id = $data['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$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'],
|
||||
];
|
||||
$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_waf_res($cert_id, $config)
|
||||
{
|
||||
if (empty($config['waf_resource_id'])) throw new Exception('云产品防护对象ID不能为空');
|
||||
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$cert_id .= '-ap-southeast-1';
|
||||
} else {
|
||||
$cert_id .= '-cn-hangzhou';
|
||||
}
|
||||
|
||||
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'DescribeDomainDetail',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'Action' => 'DescribeInstance',
|
||||
'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 . '监听器');
|
||||
throw new Exception('获取WAF实例详情失败:' . $e->getMessage());
|
||||
}
|
||||
if (empty($data['InstanceId'])) throw new Exception('当前账号未找到WAF实例');
|
||||
$instance_id = $data['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
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;
|
||||
foreach (explode(',', $config['waf_resource_id']) as $waf_resource_id) {
|
||||
$parts = explode('-', $waf_resource_id);
|
||||
$resource_instance_id = $parts[count($parts) - 3] ?? '';
|
||||
if (empty($resource_instance_id)) {
|
||||
throw new Exception('ResourceInstanceId解析失败:' . $waf_resource_id);
|
||||
}
|
||||
$param = [
|
||||
'Action' => 'DescribeCloudResourceList',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询云产品接入WAF配置失败:' . $e->getMessage());
|
||||
}
|
||||
if (empty($data['CloudResourceList'])) {
|
||||
throw new Exception('WAF云产品接入实例不存在:' . $waf_resource_id);
|
||||
}
|
||||
|
||||
if ($deploy_type == 0) {
|
||||
$param = [
|
||||
'Action' => 'ModifyCloudResourceDefaultCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('WAF云产品防护对象 ' . $waf_resource_id . ' 部署默认证书成功!');
|
||||
} else {
|
||||
$param = [
|
||||
'Action' => 'CreateCloudResourceExtensionCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('WAF云产品防护对象 ' . $waf_resource_id . ' 部署扩展证书成功!');
|
||||
|
||||
$this->clean_waf_res_expired_certs($client, $instance_id, $resource_instance_id, $waf_resource_id, $config['region']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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'];
|
||||
private function clean_waf_res_expired_certs($client, $instance_id, $resource_instance_id, $waf_resource_id, $region)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'ModifyDomain',
|
||||
'Action' => 'DescribeResourceInstanceCerts',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'Listen' => json_encode($data['Listen']),
|
||||
'Redirect' => json_encode($data['Redirect']),
|
||||
'RegionId' => $config['region'],
|
||||
'ResourceInstanceId' => $resource_instance_id,
|
||||
'RegionId' => $region,
|
||||
];
|
||||
$data = $client->request($param);
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
$this->log('查询扩展证书列表失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
if (empty($data['Certs'])) return;
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
$now = time();
|
||||
foreach ($data['Certs'] as $cert) {
|
||||
if (empty($cert['CertIdentifier']) || empty($cert['AfterDate'])) continue;
|
||||
$expire_time = strtotime($cert['AfterDate']);
|
||||
if ($expire_time !== false && $expire_time < $now) {
|
||||
$param = [
|
||||
'Action' => 'DeleteCloudResourceExtensionCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert['CertIdentifier'],
|
||||
'RegionId' => $region,
|
||||
];
|
||||
try {
|
||||
$client->request($param);
|
||||
$this->log('已删除过期扩展证书:' . $cert['CertIdentifier']);
|
||||
} catch (Exception $e) {
|
||||
$this->log('删除过期扩展证书失败:' . $cert['CertIdentifier'] . ' ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +575,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 +602,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 +684,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 +728,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 +956,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('绑定的域名不能为空');
|
||||
|
||||
375
app/lib/deploy/nginxproxymanager.php
Normal file
375
app/lib/deploy/nginxproxymanager.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class nginxproxymanager implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $email;
|
||||
private $password;
|
||||
private $proxy;
|
||||
private $token;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'] ?? '', '/');
|
||||
$this->email = trim($config['email'] ?? '');
|
||||
$this->password = $config['password'] ?? '';
|
||||
$this->proxy = isset($config['proxy']) && $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->email) || empty($this->password)) {
|
||||
throw new Exception('请填写面板地址、登录邮箱和登录密码');
|
||||
}
|
||||
|
||||
$this->login();
|
||||
$this->request('GET', '/nginx/certificates');
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
$domains = $config['domainList'] ?? [];
|
||||
$domains = array_values(array_filter(array_map('trim', $domains)));
|
||||
if (empty($domains)) {
|
||||
throw new Exception('没有设置要部署的域名');
|
||||
}
|
||||
|
||||
$this->login();
|
||||
|
||||
$certificateId = intval($config['id'] ?? 0);
|
||||
if ($certificateId > 0) {
|
||||
$this->log('使用配置中的证书ID:' . $certificateId . ' 直接更新 NPM 自定义证书');
|
||||
$certificate = $this->getCertificate($certificateId);
|
||||
$this->assertCustomCertificate($certificate, $certificateId);
|
||||
$this->uploadCertificate($certificateId, $fullchain, $privatekey);
|
||||
$this->log('证书ID:' . $certificateId . ' 更新成功!');
|
||||
return;
|
||||
}
|
||||
|
||||
$hostId = intval($config['host_id'] ?? 0);
|
||||
$hosts = $this->resolveTargetHosts($domains, $hostId);
|
||||
if (empty($hosts)) {
|
||||
throw new Exception('未找到匹配的 Proxy Host,请填写证书ID或 Proxy Host ID');
|
||||
}
|
||||
|
||||
$this->log('匹配到 Proxy Host ' . count($hosts) . ' 个');
|
||||
|
||||
$resolvedCertificateId = 0;
|
||||
$conflictMessage = null;
|
||||
foreach ($hosts as $host) {
|
||||
$hostCertificateId = intval($host['certificate_id'] ?? 0);
|
||||
if ($hostCertificateId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$certificate = $this->getCertificate($hostCertificateId);
|
||||
$this->assertCustomCertificate($certificate, $hostCertificateId);
|
||||
|
||||
if ($resolvedCertificateId === 0) {
|
||||
$resolvedCertificateId = $hostCertificateId;
|
||||
} elseif ($resolvedCertificateId !== $hostCertificateId) {
|
||||
$conflictMessage = '匹配到多个 Proxy Host,但它们绑定了不同的自定义证书ID,无法自动决定更新哪个证书,请手动填写证书ID';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 当前证书不可直接更新:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($conflictMessage !== null) {
|
||||
throw new Exception($conflictMessage);
|
||||
}
|
||||
|
||||
if ($resolvedCertificateId === 0) {
|
||||
$resolvedCertificateId = $this->createCustomCertificate($domains);
|
||||
$this->log('创建自定义证书成功,证书ID:' . $resolvedCertificateId);
|
||||
}
|
||||
|
||||
$this->uploadCertificate($resolvedCertificateId, $fullchain, $privatekey);
|
||||
$this->log('证书ID:' . $resolvedCertificateId . ' 更新成功!');
|
||||
|
||||
foreach ($hosts as $host) {
|
||||
$currentCertificateId = intval($host['certificate_id'] ?? 0);
|
||||
if ($currentCertificateId !== $resolvedCertificateId) {
|
||||
$this->updateProxyHostCertificate($host, $resolvedCertificateId);
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定到证书ID:' . $resolvedCertificateId);
|
||||
} else {
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定目标证书,无需重复更新绑定');
|
||||
}
|
||||
}
|
||||
|
||||
$info['config']['id'] = (string)$resolvedCertificateId;
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function login()
|
||||
{
|
||||
$data = $this->request('POST', '/tokens', [
|
||||
'identity' => $this->email,
|
||||
'secret' => $this->password,
|
||||
], false, false);
|
||||
|
||||
if (empty($data['token'])) {
|
||||
if (!empty($data['requires_2fa'])) {
|
||||
throw new Exception('当前 NPM 账户启用了双因素认证,暂不支持');
|
||||
}
|
||||
throw new Exception('登录 NPM 失败,未返回访问令牌');
|
||||
}
|
||||
|
||||
$this->token = $data['token'];
|
||||
}
|
||||
|
||||
private function resolveTargetHosts(array $domains, int $hostId): array
|
||||
{
|
||||
if ($hostId > 0) {
|
||||
return [$this->getProxyHost($hostId)];
|
||||
}
|
||||
|
||||
$hosts = $this->request('GET', '/nginx/proxy-hosts');
|
||||
if (!is_array($hosts)) {
|
||||
throw new Exception('获取 Proxy Host 列表失败');
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
foreach ($hosts as $host) {
|
||||
$hostDomains = $host['domain_names'] ?? [];
|
||||
if ($this->hasIntersectDomain($domains, $hostDomains)) {
|
||||
$matched[] = $this->getProxyHost(intval($host['id']));
|
||||
}
|
||||
}
|
||||
|
||||
return $matched;
|
||||
}
|
||||
|
||||
private function hasIntersectDomain(array $domains, array $hostDomains): bool
|
||||
{
|
||||
foreach ($hostDomains as $hostDomain) {
|
||||
$hostDomain = trim((string)$hostDomain);
|
||||
if ($hostDomain === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
if ($this->domainMatches($domain, $hostDomain) || $this->domainMatches($hostDomain, $domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function domainMatches(string $pattern, string $domain): bool
|
||||
{
|
||||
$pattern = strtolower(trim($pattern));
|
||||
$domain = strtolower(trim($domain));
|
||||
if ($pattern === '' || $domain === '') {
|
||||
return false;
|
||||
}
|
||||
if ($pattern === $domain) {
|
||||
return true;
|
||||
}
|
||||
if (str_starts_with($pattern, '*.')) {
|
||||
$suffix = substr($pattern, 1);
|
||||
return str_ends_with($domain, $suffix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createCustomCertificate(array $domains): int
|
||||
{
|
||||
$result = $this->request('POST', '/nginx/certificates', [
|
||||
'provider' => 'other',
|
||||
'nice_name' => $this->buildCertificateName($domains),
|
||||
]);
|
||||
|
||||
if (isset($result['owner_user_id'])) {
|
||||
$this->log('NPM 新建证书归属用户ID:' . intval($result['owner_user_id']) . '(由当前登录账号决定)');
|
||||
}
|
||||
|
||||
$certificateId = intval($result['id'] ?? 0);
|
||||
if ($certificateId <= 0) {
|
||||
throw new Exception('创建 NPM 自定义证书失败');
|
||||
}
|
||||
return $certificateId;
|
||||
}
|
||||
|
||||
private function buildCertificateName(array $domains): string
|
||||
{
|
||||
return trim($domains[0]);
|
||||
}
|
||||
|
||||
private function uploadCertificate(int $certificateId, string $fullchain, string $privatekey): void
|
||||
{
|
||||
[$certificate, $intermediateCertificate] = $this->splitFullchain($fullchain);
|
||||
|
||||
$multipart = [
|
||||
[
|
||||
'name' => 'certificate',
|
||||
'filename' => 'certificate.pem',
|
||||
'contents' => $certificate,
|
||||
],
|
||||
[
|
||||
'name' => 'certificate_key',
|
||||
'filename' => 'certificate.key',
|
||||
'contents' => $privatekey,
|
||||
],
|
||||
];
|
||||
|
||||
if ($intermediateCertificate !== '') {
|
||||
$multipart[] = [
|
||||
'name' => 'intermediate_certificate',
|
||||
'filename' => 'intermediate.pem',
|
||||
'contents' => $intermediateCertificate,
|
||||
];
|
||||
}
|
||||
|
||||
$this->request(
|
||||
'POST',
|
||||
'/nginx/certificates/' . $certificateId . '/upload',
|
||||
$multipart,
|
||||
true,
|
||||
true,
|
||||
['Content-Type' => 'multipart/form-data']
|
||||
);
|
||||
}
|
||||
|
||||
private function splitFullchain(string $fullchain): array
|
||||
{
|
||||
preg_match_all('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s', $fullchain, $matches);
|
||||
$certificates = array_values(array_filter(array_map('trim', $matches[0] ?? [])));
|
||||
if (empty($certificates)) {
|
||||
throw new Exception('证书内容格式错误,未找到 PEM 证书块');
|
||||
}
|
||||
|
||||
$certificate = $certificates[0] . "\n";
|
||||
$intermediateCertificate = '';
|
||||
if (count($certificates) > 1) {
|
||||
$intermediateCertificate = implode("\n", array_slice($certificates, 1)) . "\n";
|
||||
}
|
||||
|
||||
return [$certificate, $intermediateCertificate];
|
||||
}
|
||||
|
||||
private function updateProxyHostCertificate(array $host, int $certificateId): void
|
||||
{
|
||||
$payload = [
|
||||
'certificate_id' => $certificateId,
|
||||
];
|
||||
|
||||
$this->request('PUT', '/nginx/proxy-hosts/' . intval($host['id']), $payload);
|
||||
}
|
||||
|
||||
private function assertCustomCertificate(array $certificate, int $certificateId): void
|
||||
{
|
||||
if (($certificate['provider'] ?? '') !== 'other') {
|
||||
throw new Exception('证书ID:' . $certificateId . ' 不是自定义证书(provider=other),无法通过上传接口更新');
|
||||
}
|
||||
}
|
||||
|
||||
private function getCertificate(int $certificateId): array
|
||||
{
|
||||
$certificate = $this->request('GET', '/nginx/certificates/' . $certificateId);
|
||||
if (!is_array($certificate) || empty($certificate['id'])) {
|
||||
throw new Exception('证书ID:' . $certificateId . ' 不存在');
|
||||
}
|
||||
return $certificate;
|
||||
}
|
||||
|
||||
private function getProxyHost(int $hostId): array
|
||||
{
|
||||
$host = $this->request('GET', '/nginx/proxy-hosts/' . $hostId);
|
||||
if (!is_array($host) || empty($host['id'])) {
|
||||
throw new Exception('Proxy Host ID:' . $hostId . ' 不存在');
|
||||
}
|
||||
|
||||
$this->log('读取 Proxy Host ID:' . intval($host['id']) . ' owner_user_id:' . intval($host['owner_user_id'] ?? 0) . ' certificate_id:' . intval($host['certificate_id'] ?? 0));
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, $params = null, bool $auth = true, bool $logBodyOnError = true, array $extraHeaders = [])
|
||||
{
|
||||
$headers = $extraHeaders;
|
||||
if (!isset($headers['Content-Type']) && $params !== null && strtoupper($method) !== 'GET') {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if ($auth) {
|
||||
if (empty($this->token)) {
|
||||
throw new Exception('NPM 访问令牌不存在,请先登录');
|
||||
}
|
||||
$headers['Authorization'] = 'Bearer ' . $this->token;
|
||||
}
|
||||
|
||||
$requestData = $params;
|
||||
if ($params !== null && isset($headers['Content-Type']) && strtolower($headers['Content-Type']) !== 'multipart/form-data') {
|
||||
$requestData = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
$response = http_request(
|
||||
$this->url . '/api' . $path,
|
||||
$requestData,
|
||||
null,
|
||||
null,
|
||||
$headers,
|
||||
$this->proxy,
|
||||
$method,
|
||||
30
|
||||
);
|
||||
|
||||
$body = $response['body'] ?? '';
|
||||
$result = json_decode($body, true);
|
||||
if ($response['code'] >= 200 && $response['code'] < 300) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($logBodyOnError && $body !== '') {
|
||||
$this->log('Response:' . $body);
|
||||
}
|
||||
|
||||
if (isset($result['error']['message'])) {
|
||||
throw new Exception($result['error']['message']);
|
||||
}
|
||||
if (isset($result['message'])) {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
if (isset($result['error']) && is_string($result['error']) && $result['error'] !== '') {
|
||||
throw new Exception($result['error']);
|
||||
}
|
||||
if ($body !== '') {
|
||||
throw new Exception('请求失败(httpCode=' . $response['code'] . '): ' . $this->truncateResponseBody($body));
|
||||
}
|
||||
|
||||
throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
|
||||
}
|
||||
|
||||
private function truncateResponseBody(string $body): string
|
||||
{
|
||||
$body = trim($body);
|
||||
if ($body === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (mb_strlen($body) > 300) {
|
||||
return mb_substr($body, 0, 300) . '...';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
282
app/lib/dns/dnsmgr.php
Normal file
282
app/lib/dns/dnsmgr.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use Exception;
|
||||
|
||||
class dnsmgr implements DnsInterface
|
||||
{
|
||||
private $uid;
|
||||
private $key;
|
||||
private $baseUrl;
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $proxy;
|
||||
private $domainInfo;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->uid = $config['uid'];
|
||||
$this->key = $config['key'];
|
||||
$this->baseUrl = rtrim($config['base_url'], '/');
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->proxy = $proxy;
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if ($this->getDomainList() != false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$param = [
|
||||
'offset' => $offset,
|
||||
'limit' => $PageSize,
|
||||
];
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$param['kw'] = $KeyWord;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/domain', $param);
|
||||
if ($data && isset($data['rows'])) {
|
||||
$list = [];
|
||||
foreach ($data['rows'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['id'],
|
||||
'Domain' => $row['name'],
|
||||
'RecordCount' => $row['recordcount'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$param = [
|
||||
'offset' => $offset,
|
||||
'limit' => $PageSize,
|
||||
];
|
||||
if (!isNullOrEmpty($KeyWord)) $param['keyword'] = $KeyWord;
|
||||
if (!isNullOrEmpty($SubDomain)) $param['subdomain'] = $SubDomain;
|
||||
if (!isNullOrEmpty($Value)) $param['value'] = $Value;
|
||||
if (!isNullOrEmpty($Type)) $param['type'] = $Type;
|
||||
if (!isNullOrEmpty($Line)) $param['line'] = $Line;
|
||||
if (!isNullOrEmpty($Status)) $param['status'] = $Status;
|
||||
|
||||
$data = $this->send_request('/api/record/data/' . $this->domainid, $param);
|
||||
if ($data && isset($data['rows'])) {
|
||||
$list = [];
|
||||
foreach ($data['rows'] as $row) {
|
||||
$list[] = [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $row['Domain'],
|
||||
'Name' => $row['Name'],
|
||||
'Type' => $row['Type'],
|
||||
'Value' => $row['Value'],
|
||||
'Line' => $row['Line'],
|
||||
'LineName' => $row['LineName'],
|
||||
'TTL' => $row['TTL'],
|
||||
'MX' => $row['MX'],
|
||||
'Status' => $row['Status'],
|
||||
'Weight' => $row['Weight'],
|
||||
'Remark' => $row['Remark'],
|
||||
'UpdateTime' => $row['UpdateTime'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
|
||||
{
|
||||
if ($SubDomain == '') $SubDomain = '@';
|
||||
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
|
||||
}
|
||||
|
||||
public function getDomainRecordInfo($RecordId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = [
|
||||
'name' => $Name,
|
||||
'type' => $Type,
|
||||
'value' => $Value,
|
||||
'line' => $Line,
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
|
||||
$param['mx'] = intval($MX);
|
||||
}
|
||||
if (!isNullOrEmpty($Weight)) {
|
||||
$param['weight'] = intval($Weight);
|
||||
}
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$param['remark'] = $Remark;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/record/add/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'name' => $Name,
|
||||
'type' => $Type,
|
||||
'value' => $Value,
|
||||
'line' => $Line,
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
|
||||
$param['mx'] = intval($MX);
|
||||
}
|
||||
if (!isNullOrEmpty($Weight)) {
|
||||
$param['weight'] = intval($Weight);
|
||||
}
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$param['remark'] = $Remark;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/record/update/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'remark' => $Remark,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/remark/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/delete/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'status' => $Status,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/status/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRecordLine()
|
||||
{
|
||||
$data = $this->getDomainInfo();
|
||||
if ($data && isset($data['recordLine'])) {
|
||||
$list = [];
|
||||
foreach ($data['recordLine'] as $row) {
|
||||
$list[$row['id']] = [
|
||||
'name' => $row['name'],
|
||||
'parent' => isset($row['parent']) ? $row['parent'] : null,
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMinTTL()
|
||||
{
|
||||
$data = $this->getDomainInfo();
|
||||
if ($data && isset($data['minTTL'])) {
|
||||
return $data['minTTL'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainInfo()
|
||||
{
|
||||
if (!empty($this->domainInfo)) return $this->domainInfo;
|
||||
$data = $this->send_request('/api/domain/' . $this->domainid, ['loginurl' => 0]);
|
||||
if ($data) {
|
||||
$this->domainInfo = $data;
|
||||
return $data;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_request($path, $param = [])
|
||||
{
|
||||
try {
|
||||
$timestamp = (string)time();
|
||||
$signStr = $this->uid . $timestamp . $this->key;
|
||||
$sign = md5($signStr);
|
||||
|
||||
$url = $this->baseUrl . $path;
|
||||
|
||||
$param['uid'] = $this->uid;
|
||||
$param['timestamp'] = $timestamp;
|
||||
$param['sign'] = $sign;
|
||||
$postData = http_build_query($param);
|
||||
|
||||
$response = http_request($url, $postData, null, null, null, $this->proxy);
|
||||
|
||||
$result = json_decode($response['body'], true);
|
||||
if (isset($result['code']) && $result['code'] == 0) {
|
||||
return isset($result['data']) ? $result['data'] : null;
|
||||
} elseif (isset($result['rows']) && isset($result['total'])) {
|
||||
return $result;
|
||||
} elseif (isset($result['msg'])) {
|
||||
$this->setError($result['msg']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError($response['body']);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -63,7 +63,7 @@ class huawei implements DnsInterface
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize];
|
||||
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize, 'records' => $Value];
|
||||
if (!isNullOrEmpty($Status)) {
|
||||
$Status = $Status == '1' ? 'ACTIVE' : 'DISABLE';
|
||||
$query['status'] = $Status;
|
||||
@@ -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);
|
||||
|
||||
499
app/lib/dns/technitium.php
Normal file
499
app/lib/dns/technitium.php
Normal file
@@ -0,0 +1,499 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use Exception;
|
||||
|
||||
class technitium implements DnsInterface
|
||||
{
|
||||
private $url;
|
||||
private $token;
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $proxy;
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/') . '/api';
|
||||
$this->token = $config['token'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if ($this->getDomainList() !== false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
|
||||
{
|
||||
$data = $this->send_request('GET', '/zones/list');
|
||||
if ($data && isset($data['response']['zones'])) {
|
||||
$list = [];
|
||||
foreach ($data['response']['zones'] as $zone) {
|
||||
$list[] = [
|
||||
'DomainId' => $zone['name'],
|
||||
'Domain' => $zone['name'],
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
|
||||
return strpos($v['Domain'], $KeyWord) !== false;
|
||||
}));
|
||||
}
|
||||
return ['total' => count($list), 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$params = ['domain' => $this->domain, 'listZone' => 'true'];
|
||||
$data = $this->send_request('GET', '/zones/records/get', $params);
|
||||
if ($data && isset($data['response']['records'])) {
|
||||
$list = [];
|
||||
$records = $data['response']['records'];
|
||||
foreach ($records as $i => &$row) {
|
||||
$row['id'] = $i;
|
||||
$name = $row['name'] == $this->domain ? '@' : str_replace('.' . $this->domain, '', $row['name']);
|
||||
$value = '';
|
||||
$mx = null;
|
||||
$rData = $row['rData'];
|
||||
|
||||
if ($row['type'] == 'A' || $row['type'] == 'AAAA') {
|
||||
$value = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
|
||||
} elseif ($row['type'] == 'CNAME') {
|
||||
$value = isset($rData['cname']) ? $rData['cname'] : '';
|
||||
} elseif ($row['type'] == 'NS') {
|
||||
$value = isset($rData['nameServer']) ? $rData['nameServer'] : '';
|
||||
} elseif ($row['type'] == 'MX') {
|
||||
$value = isset($rData['exchange']) ? $rData['exchange'] : '';
|
||||
$mx = isset($rData['preference']) ? $rData['preference'] : 1;
|
||||
} elseif ($row['type'] == 'TXT') {
|
||||
$value = isset($rData['text']) ? $rData['text'] : '';
|
||||
} elseif ($row['type'] == 'SRV') {
|
||||
$value = (isset($rData['priority']) ? $rData['priority'] : 0) . ' ' . (isset($rData['weight']) ? $rData['weight'] : 0) . ' ' . (isset($rData['port']) ? $rData['port'] : 0) . ' ' . (isset($rData['target']) ? $rData['target'] : '');
|
||||
} elseif ($row['type'] == 'PTR') {
|
||||
$value = isset($rData['ptrName']) ? $rData['ptrName'] : '';
|
||||
} elseif ($row['type'] == 'CAA') {
|
||||
$value = (isset($rData['flags']) ? $rData['flags'] : 0) . ' ' . (isset($rData['tag']) ? $rData['tag'] : '') . ' "' . (isset($rData['value']) ? $rData['value'] : '') . '"';
|
||||
} elseif ($row['type'] == 'ANAME') {
|
||||
$value = isset($rData['aname']) ? $rData['aname'] : '';
|
||||
} elseif ($row['type'] == 'DNAME') {
|
||||
$value = isset($rData['dname']) ? $rData['dname'] : '';
|
||||
} elseif ($row['type'] == 'APP') {
|
||||
$value = (isset($rData['appName']) ? $rData['appName'] : '') . ' ' . (isset($rData['classPath']) ? $rData['classPath'] : '');
|
||||
if (!empty($rData['recordData'])) {
|
||||
$value .= ' ' . $rData['recordData'];
|
||||
}
|
||||
}
|
||||
|
||||
$list[] = [
|
||||
'RecordId' => $i,
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['type'],
|
||||
'Value' => $value,
|
||||
'Line' => 'default',
|
||||
'TTL' => $row['ttl'],
|
||||
'MX' => $mx,
|
||||
'Status' => $row['disabled'] ? '0' : '1',
|
||||
'Weight' => null,
|
||||
'Remark' => isset($row['comments']) ? $row['comments'] : null,
|
||||
'UpdateTime' => null,
|
||||
];
|
||||
}
|
||||
cache('technitium_' . $this->domain, $records, 86400);
|
||||
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($SubDomain) {
|
||||
return strcasecmp($v['Name'], $SubDomain) === 0;
|
||||
}));
|
||||
} else {
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
|
||||
return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Value)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Value) {
|
||||
return $v['Value'] == $Value;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Type)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Type) {
|
||||
return $v['Type'] == $Type;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Status)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Status) {
|
||||
return $v['Status'] == $Status;
|
||||
}));
|
||||
}
|
||||
}
|
||||
return ['total' => count($list), 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
|
||||
{
|
||||
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
|
||||
}
|
||||
|
||||
public function getDomainRecordInfo($RecordId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildRecordParams($Type, $Value, $MX = 1)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['ipAddress'] = $Value;
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['cname'] = $Value;
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['nameServer'] = $Value;
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['exchange'] = $Value;
|
||||
$params['preference'] = intval($MX);
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['text'] = $Value;
|
||||
} elseif ($Type == 'SRV') {
|
||||
$parts = explode(' ', $Value);
|
||||
if (count($parts) == 4) {
|
||||
$params['priority'] = $parts[0];
|
||||
$params['weight'] = $parts[1];
|
||||
$params['port'] = $parts[2];
|
||||
$params['target'] = $parts[3];
|
||||
}
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['ptrName'] = $Value;
|
||||
} elseif ($Type == 'CAA') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) == 3) {
|
||||
$params['flags'] = $parts[0];
|
||||
$params['tag'] = $parts[1];
|
||||
$params['value'] = trim($parts[2], '"');
|
||||
}
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['aname'] = $Value;
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['dname'] = $Value;
|
||||
} elseif ($Type == 'APP') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) >= 2) {
|
||||
$params['appName'] = $parts[0];
|
||||
$params['classPath'] = $parts[1];
|
||||
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
|
||||
} else {
|
||||
$params['appName'] = rtrim($Value);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function getOldValueParams($Type, $rData)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['ipAddress'] = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['cname'] = isset($rData['cname']) ? $rData['cname'] : '';
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['nameServer'] = isset($rData['nameServer']) ? $rData['nameServer'] : '';
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['exchange'] = isset($rData['exchange']) ? $rData['exchange'] : '';
|
||||
$params['preference'] = isset($rData['preference']) ? $rData['preference'] : 1;
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['text'] = isset($rData['text']) ? $rData['text'] : '';
|
||||
} elseif ($Type == 'SRV') {
|
||||
$params['priority'] = isset($rData['priority']) ? $rData['priority'] : 0;
|
||||
$params['weight'] = isset($rData['weight']) ? $rData['weight'] : 0;
|
||||
$params['port'] = isset($rData['port']) ? $rData['port'] : 0;
|
||||
$params['target'] = isset($rData['target']) ? $rData['target'] : '';
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['ptrName'] = isset($rData['ptrName']) ? $rData['ptrName'] : '';
|
||||
} elseif ($Type == 'CAA') {
|
||||
$params['flags'] = isset($rData['flags']) ? $rData['flags'] : 0;
|
||||
$params['tag'] = isset($rData['tag']) ? $rData['tag'] : '';
|
||||
$params['value'] = isset($rData['value']) ? $rData['value'] : '';
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['aname'] = isset($rData['aname']) ? $rData['aname'] : '';
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['dname'] = isset($rData['dname']) ? $rData['dname'] : '';
|
||||
} elseif ($Type == 'APP') {
|
||||
$params['appName'] = isset($rData['appName']) ? $rData['appName'] : '';
|
||||
$params['classPath'] = isset($rData['classPath']) ? $rData['classPath'] : '';
|
||||
if (!empty($rData['recordData'])) {
|
||||
$params['recordData'] = $rData['recordData'];
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function getNewValueParams($Type, $Value, $MX = 1)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['newIpAddress'] = $Value;
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['newCname'] = $Value;
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['newNameServer'] = $Value;
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['newExchange'] = $Value;
|
||||
$params['newPreference'] = intval($MX);
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['newText'] = $Value;
|
||||
} elseif ($Type == 'SRV') {
|
||||
$parts = explode(' ', $Value);
|
||||
if (count($parts) == 4) {
|
||||
$params['newPriority'] = $parts[0];
|
||||
$params['newWeight'] = $parts[1];
|
||||
$params['newPort'] = $parts[2];
|
||||
$params['newTarget'] = $parts[3];
|
||||
}
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['newPtrName'] = $Value;
|
||||
} elseif ($Type == 'CAA') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) == 3) {
|
||||
$params['newFlags'] = $parts[0];
|
||||
$params['newTag'] = $parts[1];
|
||||
$params['newValue'] = trim($parts[2], '"');
|
||||
}
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['newAName'] = $Value;
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['newDName'] = $Value;
|
||||
} elseif ($Type == 'APP') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) >= 2) {
|
||||
$params['appName'] = $parts[0];
|
||||
$params['classPath'] = $parts[1];
|
||||
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
|
||||
} else {
|
||||
$params['appName'] = rtrim($Value);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$domain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $Type,
|
||||
'ttl' => intval($TTL)
|
||||
];
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$params['comments'] = $Remark;
|
||||
}
|
||||
$valParams = $this->buildRecordParams($Type, $Value, $MX);
|
||||
if (empty($valParams) && $Type != 'SOA') {
|
||||
$this->setError('不受支持的记录类型或参数解析失败');
|
||||
return false;
|
||||
}
|
||||
$params = array_merge($params, $valParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/add', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
$newDomain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
|
||||
|
||||
if ($oldRecord['type'] == 'APP') {
|
||||
$oldValue = (isset($oldRecord['rData']['appName']) ? $oldRecord['rData']['appName'] : '') . ' ' . (isset($oldRecord['rData']['classPath']) ? $oldRecord['rData']['classPath'] : '');
|
||||
if (!empty($oldRecord['rData']['recordData'])) {
|
||||
$oldValue .= ' ' . $oldRecord['rData']['recordData'];
|
||||
}
|
||||
if ($oldValue != rtrim($Value) || $domain != $newDomain) {
|
||||
$this->deleteDomainRecord($RecordId);
|
||||
return $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark);
|
||||
}
|
||||
}
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
|
||||
if ($domain != $newDomain) {
|
||||
$params['newDomain'] = $newDomain;
|
||||
}
|
||||
|
||||
$params['comments'] = empty($Remark) ? "" : $Remark;
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$newValParams = $this->getNewValueParams($Type, $Value, $MX);
|
||||
|
||||
$params = array_merge($params, $oldValParams, $newValParams);
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'comments' => $Remark,
|
||||
];
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
];
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/delete', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'disable' => $Status == '0' ? 'true' : 'false',
|
||||
];
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRecordLine()
|
||||
{
|
||||
return ['default' => ['name' => '默认', 'parent' => null]];
|
||||
}
|
||||
|
||||
public function getMinTTL()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
$params = [
|
||||
'zone' => $Domain,
|
||||
'type' => 'Primary'
|
||||
];
|
||||
$result = $this->send_request('POST', '/zones/create', $params);
|
||||
if ($result && isset($result['response']['domain'])) {
|
||||
return ['id' => $result['response']['domain'], 'name' => $result['response']['domain']];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_request($method, $path, $params = [])
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
$params['token'] = $this->token;
|
||||
|
||||
$body = null;
|
||||
if ($method == 'GET' || $method == 'DELETE') {
|
||||
$url .= '?' . http_build_query($params);
|
||||
} else {
|
||||
$body = http_build_query($params);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, null, $this->proxy, $method);
|
||||
} catch (Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$arr = json_decode($response['body'], true);
|
||||
if (isset($arr['status']) && $arr['status'] == 'ok') {
|
||||
return $arr;
|
||||
} elseif (isset($arr['errorMessage'])) {
|
||||
$this->setError($arr['errorMessage']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError('API 请求失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
620
app/service/CloudflareEnhanceService.php
Normal file
620
app/service/CloudflareEnhanceService.php
Normal file
@@ -0,0 +1,620 @@
|
||||
<?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, string $sslMethod = 'http', string $minTlsVersion = '1.0'): array
|
||||
{
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$payload = [
|
||||
'hostname' => $hostname,
|
||||
'ssl' => [
|
||||
'method' => $sslMethod === 'txt' ? 'txt' : 'http',
|
||||
'type' => 'dv',
|
||||
'settings' => [
|
||||
'min_tls_version' => $minTlsVersion
|
||||
]
|
||||
],
|
||||
];
|
||||
$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 getDcvDelegationUuid(string $zoneId): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/zones/' . $zoneId . '/dcv_delegation/uuid', [], null, true);
|
||||
if ($result === null) {
|
||||
return '';
|
||||
}
|
||||
return trim((string)($result['uuid'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 DCV 委派 UUID', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class ExpireNoticeService
|
||||
$count = $this->refreshExpiringDomainList($max_day);
|
||||
if ($count > 0) return;
|
||||
|
||||
if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1') && date('H') >= 9) {
|
||||
if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1' || config_get('expire_notice_custom_webhook') == '1') && date('H') >= 9) {
|
||||
$this->noticeExpiringDomainList($max_day, $days);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', '1049');
|
||||
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');
|
||||
@@ -26,6 +26,7 @@ DROP TABLE IF EXISTS `dnsmgr_domain`;
|
||||
CREATE TABLE `dnsmgr_domain` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`aid` int(11) unsigned NOT NULL,
|
||||
`cid` int(11) unsigned NOT NULL DEFAULT '0',
|
||||
`name` varchar(255) NOT NULL,
|
||||
`thirdid` varchar(60) DEFAULT NULL,
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
@@ -40,7 +41,8 @@ CREATE TABLE `dnsmgr_domain` (
|
||||
`noticetime` datetime DEFAULT NULL,
|
||||
`checkstatus` tinyint(1) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `name` (`name`)
|
||||
KEY `name` (`name`),
|
||||
KEY `cid` (`cid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_user`;
|
||||
@@ -251,4 +253,25 @@ 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;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_domain_category`;
|
||||
CREATE TABLE `dnsmgr_domain_category` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
`sort` int(11) NOT NULL DEFAULT '0',
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -189,4 +189,27 @@ 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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dnsmgr_domain_category` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
`sort` int(11) NOT NULL DEFAULT '0',
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE `dnsmgr_domain`
|
||||
ADD COLUMN `cid` int(11) unsigned NOT NULL DEFAULT '0',
|
||||
ADD KEY `cid` (`cid`);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ class MsgNotice
|
||||
$content = str_replace(['<br/>', '<b>', '</b>'], ["\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('notice_custom_webhook') == 1) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function cert_order_send($id, $result)
|
||||
@@ -141,6 +144,9 @@ class MsgNotice
|
||||
$content = str_replace(['*', '<br/>', '<b>', '</b>'], ['\*', "\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('cert_notice_custom_webhook') == 1 || config_get('cert_notice_custom_webhook') == 2 && !$result) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function expire_notice_send($day, $list)
|
||||
@@ -169,6 +175,9 @@ class MsgNotice
|
||||
$content = str_replace(['*', '<br/>', '<b>', '</b>'], ['\*', "\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('expire_notice_custom_webhook') == 1) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function send_mail($to, $sub, $msg)
|
||||
@@ -223,6 +232,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 +256,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 +359,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;
|
||||
@@ -356,6 +368,85 @@ class MsgNotice
|
||||
}
|
||||
}
|
||||
|
||||
public static function send_custom_webhook($title, $content)
|
||||
{
|
||||
$url = config_get('custom_webhook_url');
|
||||
if (!$url || !parse_url($url)) return false;
|
||||
|
||||
$method = strtoupper(config_get('custom_webhook_method') ?: 'POST');
|
||||
$contentType = config_get('custom_webhook_content_type') ?: 'application/json';
|
||||
$headersRaw = config_get('custom_webhook_headers');
|
||||
$bodyTemplate = config_get('custom_webhook_body') ?: '{"title":"{title}","content":"{content}"}';
|
||||
$contentFormat = config_get('custom_webhook_content_format') ?: 'text';
|
||||
|
||||
if ($contentFormat === 'markdown') {
|
||||
$content = str_replace(['<br/>', '<b>', '</b>'], ["\n", '**', '**'], $content);
|
||||
$content = strip_tags($content);
|
||||
} elseif ($contentFormat === 'text') {
|
||||
$content = str_replace('<br/>', "\n", $content);
|
||||
$content = strip_tags($content);
|
||||
}
|
||||
|
||||
$body = str_replace(['{title}', '{content}'], [$title, $content], $bodyTemplate);
|
||||
|
||||
$headers = [];
|
||||
if (!empty($headersRaw)) {
|
||||
$lines = explode("\n", $headersRaw);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) continue;
|
||||
$pos = strpos($line, ':');
|
||||
if ($pos !== false) {
|
||||
$key = trim(substr($line, 0, $pos));
|
||||
$val = trim(substr($line, $pos + 1));
|
||||
if ($key !== '') $headers[$key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$options = [
|
||||
'timeout' => 10,
|
||||
'verify' => false,
|
||||
'headers' => $headers,
|
||||
'http_errors' => false,
|
||||
];
|
||||
|
||||
if ($method === 'GET') {
|
||||
$params = [];
|
||||
if ($contentType === 'application/json') {
|
||||
$decoded = json_decode($body, true);
|
||||
if (is_array($decoded)) {
|
||||
$params = $decoded;
|
||||
}
|
||||
} else {
|
||||
parse_str($body, $params);
|
||||
}
|
||||
$connector = strpos($url, '?') !== false ? '&' : '?';
|
||||
$url = $url . $connector . http_build_query($params);
|
||||
} else {
|
||||
$options['headers']['Content-Type'] = $contentType;
|
||||
if ($contentType === 'application/json') {
|
||||
json_decode($body);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$body = json_encode(['title' => $title, 'content' => $content]);
|
||||
}
|
||||
}
|
||||
$options['body'] = $body;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->request($method, $url, $options);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
return true;
|
||||
}
|
||||
return '请求失败,HTTP状态码:' . $statusCode;
|
||||
} catch (\Exception $e) {
|
||||
return '请求失败:' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private static function telegram_curl($url, $post)
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
<label class="col-sm-3 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="cert_notice_webhook" default="{:config_get('cert_notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="cert_notice_custom_webhook" default="{:config_get('cert_notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></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"/></div>
|
||||
</div>
|
||||
|
||||
3421
app/view/cloudflare/hostnames.html
Normal file
3421
app/view/cloudflare/hostnames.html
Normal file
File diff suppressed because it is too large
Load Diff
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}
|
||||
@@ -106,13 +106,16 @@
|
||||
{if request()->user['type'] eq 'user'}<li class="{:checkIfActive('index')}">
|
||||
<a href="/"><i class="fa fa-home fa-fw"></i> <span>后台首页</span></a>
|
||||
</li>{/if}
|
||||
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice')}">
|
||||
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice,smartparse')}">
|
||||
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
|
||||
</li>
|
||||
{if request()->user['level'] eq 2}
|
||||
<li class="{:checkIfActive('account,account_add')}">
|
||||
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
|
||||
</li>
|
||||
<li class="{:checkIfActive('category')}">
|
||||
<a href="/domain/category"><i class="fa fa-folder fa-fw"></i> <span>域名分类</span></a>
|
||||
</li>
|
||||
<li class="treeview {:checkIfActive('overview,task,taskinfo,taskform')}">
|
||||
<a href="javascript:;">
|
||||
<i class="fa fa-heartbeat fa-fw"></i>
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
<label class="col-sm-4 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-8"><select class="form-control" name="notice_webhook" default="{:config_get('notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-8"><select class="form-control" name="notice_custom_webhook" default="{:config_get('notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
204
app/view/domain/category.html
Normal file
204
app/view/domain/category.html
Normal file
@@ -0,0 +1,204 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}域名分类管理{/block}
|
||||
{block name="main"}
|
||||
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" 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 aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||
<h4 class="modal-title" id="modal-title">添加分类</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<input type="hidden" name="id"/>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">分类名称</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" name="name" placeholder="输入分类名称" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">排序</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" name="sort" value="0" placeholder="数字越小越靠前">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">备注</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" name="remark" placeholder="可选">
|
||||
</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" id="store" onclick="save()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">域名分类管理</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加分类</a>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
</form>
|
||||
<table id="listTable"></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?v=1003"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
let defaultPageSize = getCookie('category_pagesize') ? getCookie('category_pagesize') : 10;
|
||||
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
|
||||
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
|
||||
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/domain/category/data',
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID'
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称'
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
formatter: function(value, row, index) {
|
||||
return value ? value : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序'
|
||||
},
|
||||
{
|
||||
field: 'domain_count',
|
||||
title: '域名数量',
|
||||
formatter: function(value, row, index) {
|
||||
return '<span class="label label-info">' + value + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'addtime',
|
||||
title: '添加时间'
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row, index) {
|
||||
var html = '<a href="javascript:editframe(\''+row.id+'\')" class="btn btn-primary btn-xs">修改</a> ';
|
||||
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a> ';
|
||||
html += '<a href="/domain?cid='+row.id+'" class="btn btn-default btn-xs">域名</a>';
|
||||
return html;
|
||||
}
|
||||
},
|
||||
],
|
||||
onPageChange: function(number, size){
|
||||
if(size != defaultPageSize){
|
||||
setCookie('category_pagesize', size, 24 * 3600 * 30);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$("#form-store").bootstrapValidator();
|
||||
});
|
||||
|
||||
function addframe(){
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("添加分类");
|
||||
$("#form-store input[name=id]").val('');
|
||||
$("#form-store input[name=name]").val('');
|
||||
$("#form-store input[name=sort]").val('0');
|
||||
$("#form-store input[name=remark]").val('');
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
}
|
||||
|
||||
function editframe(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("修改分类");
|
||||
$("#form-store input[name=id]").val(id);
|
||||
$("#form-store input[name=name]").val(row.name);
|
||||
$("#form-store input[name=sort]").val(row.sort);
|
||||
$("#form-store input[name=remark]").val(row.remark);
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
}
|
||||
|
||||
function save(){
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var id = $("#form-store input[name=id]").val();
|
||||
var action = id ? 'edit' : 'add';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/category/' + action,
|
||||
data : $("#form-store").serialize(),
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.alert(data.msg, {
|
||||
icon: 1,
|
||||
closeBtn: false
|
||||
}, function(){
|
||||
layer.closeAll();
|
||||
$("#modal-store").modal('hide');
|
||||
searchRefresh();
|
||||
});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delItem(id) {
|
||||
layer.confirm('确定要删除此分类吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/category/del',
|
||||
data : {id: id},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg('删除成功', {icon: 1, time:800});
|
||||
searchRefresh();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -107,12 +107,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="remark" placeholder="">
|
||||
</div>
|
||||
<label class="col-sm-3 control-label">所属分类</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="cid" class="form-control">
|
||||
<option value="0">未分类</option>
|
||||
{foreach $categorys as $item}
|
||||
<option value="{$item.id}">{$item.name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="remark" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
@@ -138,6 +149,9 @@
|
||||
<option value="{$k}">{$v}</option>
|
||||
{/foreach}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="cid" class="form-control"><option value="">所有分类</option>{foreach $categorys as $item}<option value="{$item.id}">{$item.name}</option>{/foreach}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="status" class="form-control"><option value="">所有状态</option><option value="1">即将到期</option><option value="2">已到期</option></select>
|
||||
</div>
|
||||
@@ -146,10 +160,10 @@
|
||||
</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>
|
||||
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('setcategory')">设置分类</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><li><a href="/record/smartparse">智能添加解析</a></li></ul>
|
||||
</div>
|
||||
<a href="/domain/expirenotice" class="btn btn-default">到期提醒设置</a>{/if}
|
||||
</form>
|
||||
@@ -172,7 +186,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;
|
||||
@@ -284,6 +298,13 @@ $(document).ready(function(){
|
||||
return value==1?'<font color="green">是</font>':'<font color="red">否</font>';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'category_name',
|
||||
title: '分类',
|
||||
formatter: function(value, row, index) {
|
||||
return value ? '<span class="label label-default">' + value + '</span>' : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注'
|
||||
@@ -400,6 +421,7 @@ function editframe(id){
|
||||
$("#form-store2 select[name=is_hide]").val(row.is_hide);
|
||||
$("#form-store2 select[name=is_sso]").val(row.is_sso);
|
||||
$("#form-store2 select[name=is_notice]").val(row.is_notice);
|
||||
$("#form-store2 select[name=cid]").val(row.cid ? row.cid : 0);
|
||||
$("#form-store2 input[name=remark]").val(row.remark);
|
||||
|
||||
$("#form-store2 input[name=expiretime]").datetimepicker({
|
||||
@@ -504,6 +526,9 @@ function operation(action){
|
||||
if(action == 'editremark'){
|
||||
batch_edit_remark(ids)
|
||||
return;
|
||||
}else if(action == 'setcategory'){
|
||||
batch_set_category(ids)
|
||||
return;
|
||||
}else if(action == 'addrecord'){
|
||||
sessionStorage.setItem('domains', JSON.stringify(rows));
|
||||
window.location.href = '/record/batchadd';
|
||||
@@ -607,6 +632,56 @@ function batch_edit_remark(ids) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function batch_set_category(ids) {
|
||||
var categoryOptions = '<option value="0">未分类</option>';
|
||||
$.ajax({
|
||||
type : 'GET',
|
||||
url : '/domain/category/list',
|
||||
dataType : 'json',
|
||||
async: false,
|
||||
success : function(data) {
|
||||
if(data.code == 0 && data.data){
|
||||
$.each(data.data, function(index, item){
|
||||
categoryOptions += '<option value="' + item.id + '">' + item.name + '</option>';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
area: ['350px'],
|
||||
closeBtn: 2,
|
||||
title: '批量设置分类',
|
||||
content: '<div style="padding:15px"><div class="form-group"><select class="form-control" name="category_id">' + categoryOptions + '</select></div></div>',
|
||||
btn: ['确认', '取消'],
|
||||
yes: function(){
|
||||
var cid = $("select[name='category_id']").val();
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/setcategory',
|
||||
data : {ids:ids, cid:cid},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
layer.alert(data.msg,{
|
||||
icon: 1,
|
||||
closeBtn: false
|
||||
}, function(){
|
||||
layer.closeAll();
|
||||
searchRefresh();
|
||||
});
|
||||
},
|
||||
error:function(data){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateDate(id){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<label class="col-sm-3 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="expire_notice_webhook" default="{:config_get('expire_notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="expire_notice_custom_webhook" default="{:config_get('expire_notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></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"/>
|
||||
|
||||
@@ -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>';
|
||||
@@ -348,6 +350,10 @@ $(document).ready(function(){
|
||||
if(dnsconfig.remark == 1){
|
||||
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a> ';
|
||||
}
|
||||
var supportedTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'PTR', 'LOC', 'LUA', 'REDIRECT_URL', 'FORWARD_URL'];
|
||||
if(supportedTypes.includes(row.Type)){
|
||||
html += '<a href="javascript:checkRecord(\''+row.RecordId+'\')" class="btn btn-success btn-xs" title="检测解析生效"><i class="fa fa-check-circle-o"></i></a> ';
|
||||
}
|
||||
if(row.Type == 'A' || row.Type == 'CNAME' || row.Type == 'AAAA' || row.Type == 'REDIRECT_URL' || row.Type == 'FORWARD_URL'){
|
||||
if(row.Name === "@") var domain = "{$domainName}";
|
||||
else var domain = row.Name + ".{$domainName}";
|
||||
@@ -723,6 +729,48 @@ function advanceSearch(){
|
||||
$("#searchbox1").slideDown();
|
||||
}
|
||||
}
|
||||
function checkRecord(recordid) {
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', recordid);
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/check/{$domainId}',
|
||||
data : {recordid: recordid, name: row.Name, type: row.Type, value: Array.isArray(row.Value) ? row.Value[0] : row.Value},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
var result = data.data;
|
||||
var title = result.status === 'active' ? '<font color="green"><i class="fa fa-check-circle"></i> 解析已生效</font>' : (result.status === 'not_found' ? '<font color="red"><i class="fa fa-times-circle"></i> 未查询到解析</font>' : '<font color="red"><i class="fa fa-times-circle"></i> 解析值不匹配</font>');
|
||||
var content = '<div style="padding:0 10px;">';
|
||||
content += '<p><strong>主机记录:</strong>' + row.Name + '</p>';
|
||||
content += '<p><strong>记录类型:</strong>' + row.Type + '</p>';
|
||||
content += '<p><strong>记录值:</strong>' + htmlEscape(row.Value) + '</p>';
|
||||
content += '<hr style="margin:10px 0;">';
|
||||
content += '<p><strong>检测结果:</strong>' + title + '</p>';
|
||||
if(result.actual && result.actual.length > 0){
|
||||
content += '<p><strong>实际解析值:</strong></p>';
|
||||
content += '<ul style="max-height:150px;overflow-y:auto;">';
|
||||
for(var i = 0; i < result.actual.length; i++){
|
||||
content += '<li>' + htmlEscape(result.actual[i]) + '</li>';
|
||||
}
|
||||
content += '</ul>';
|
||||
}
|
||||
if(result.expected){
|
||||
content += '<p><strong>期望解析值:</strong>' + htmlEscape(result.expected) + '</p>';
|
||||
}
|
||||
content += '</div>';
|
||||
layer.alert(content, {title: 'DNS解析检测', area: ['450px'], shadeClose: true});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
function copyToClipboard(text, selector) {
|
||||
if (!text && selector) {
|
||||
var el = document.querySelector(selector);
|
||||
|
||||
825
app/view/domain/smartparse.html
Normal file
825
app/view/domain/smartparse.html
Normal file
@@ -0,0 +1,825 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}智能批量添加{/block}
|
||||
{block name="main"}
|
||||
<style>
|
||||
.modal-body .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.batch-input-area {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.batch-preview {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
.batch-preview table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.batch-preview th,
|
||||
.batch-preview td {
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
border: 0.5px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.batch-preview th {
|
||||
background-color: #f9f9f9;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.batch-preview tr:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.batch-preview .label {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.batch-preview .label-primary {
|
||||
background-color: #337ab7;
|
||||
color: #fff;
|
||||
}
|
||||
.batch-preview .status-success {
|
||||
color: #52c41a;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.domain-select-modal {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.domain-item {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.domain-item:hover {
|
||||
border-color: #337ab7;
|
||||
background-color: #f5f9fc;
|
||||
}
|
||||
.domain-item.selected {
|
||||
border-color: #337ab7;
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
.domain-item.selected::after {
|
||||
content: '✓';
|
||||
float: right;
|
||||
color: #337ab7;
|
||||
font-weight: bold;
|
||||
}
|
||||
</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="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>智能批量添加解析</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" id="batchForm">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">批量数据 <span class="text-danger">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<textarea class="form-control batch-input-area" id="batchInput" rows="10"
|
||||
placeholder="请按以下格式输入(每行一条记录): 格式1:主机记录 记录值 格式2:主机记录 记录值 域名 格式3:记录值 主机记录.域名 格式4:主机记录.域名(使用下方记录值) 示例: www 1.2.3.4 example.com api app.example.com example.com 1.1.1.1 www.example.com example.com 说明: - 如果使用格式4,将使用下方的记录值 - 如果不指定域名,将使用下方选择的默认域名 - 如果检测到多个不同域名,会提示您选择对应的DNS配置"></textarea>
|
||||
<p class="help-block">每行一条记录,支持混合输入多个域名的记录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">记录值</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="batchValueInput" placeholder="当使用格式4时,将使用此记录值">
|
||||
<p class="help-block">留空则不使用格式4</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">默认域名</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="defaultDomain" id="defaultDomainSelect" class="form-control select2">
|
||||
<option value="">不使用默认域名(必须每行都指定域名)</option>
|
||||
{foreach $domainList as $domain}
|
||||
<option value="{$domain.id}">{$domain.name} [{$domain.dnsType}]</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
<p class="help-block">当某行没有指定域名时,使用此默认域名</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">记录类型</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="defaultType" id="defaultTypeSelect" class="form-control">
|
||||
<option value="">自动检测</option>
|
||||
<option value="A">A</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="CAA">CAA</option>
|
||||
</select>
|
||||
<p class="help-block">留空则根据记录值自动判断类型</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">线路</label>
|
||||
<div class="col-sm-6" id="batch_line_list">
|
||||
<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">
|
||||
<option value="">自动选择</option>
|
||||
</select>
|
||||
<p class="help-block">留空则使用默认线路</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">TTL</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="number" class="form-control" name="defaultTtl" id="defaultTtlInput" value="600" min="1">
|
||||
<p class="help-block">默认TTL时间(秒)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-6">
|
||||
<button type="button" class="btn btn-info" onclick="previewBatchData()"><i class="fa fa-eye"></i> 预览解析结果</button>
|
||||
<button type="button" class="btn btn-primary" id="btnBatchAdd" onclick="submitBatchData()"><i class="fa fa-plus-circle"></i> 批量添加解析</button>
|
||||
<button type="button" class="btn btn-default" onclick="resetBatchForm()"><i class="fa fa-refresh"></i> 重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-group col-sm-12" id="previewSection" style="display:none;margin-top:20px;">
|
||||
<label>解析预览</label>
|
||||
<div class="table-responsive batch-preview">
|
||||
<table style="min-width: 800px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%">序号</th>
|
||||
<th style="width:15%">主机记录</th>
|
||||
<th style="width:10%">类型</th>
|
||||
<th style="width:25%">记录值</th>
|
||||
<th style="width:18%">DNS域名</th>
|
||||
<th style="width:12%">线路</th>
|
||||
<th style="width:8%">TTL</th>
|
||||
<th style="width:7%">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="previewBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="alert alert-info" id="previewSummary" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-domain-select" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||
<h4 class="modal-title">选择DNS配置</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i> 检测到多个不同的域名,请为每个域名选择对应的DNS配置:
|
||||
</div>
|
||||
<div class="domain-select-modal" id="domainSelectModal">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="confirmDomainSelection()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script>
|
||||
var domainList = [];
|
||||
{foreach $domainList as $domain}
|
||||
domainList.push({
|
||||
id: '{$domain.id}',
|
||||
name: '{$domain.name}',
|
||||
dnsType: '{$domain.dnsType}'
|
||||
});
|
||||
{/foreach}
|
||||
|
||||
var parsedBatchData = [];
|
||||
var domainMapping = {};
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#defaultDomainSelect').select2({
|
||||
placeholder: '选择默认域名',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function(){ return '未找到匹配的域名'; },
|
||||
searching: function(){ return '搜索中...'; }
|
||||
}
|
||||
});
|
||||
|
||||
$('#defaultDomainSelect').on('change', function(){
|
||||
var domainId = $(this).val();
|
||||
if(domainId){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/quickinfo/' + domainId,
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
var lineOptions = '<option value="">自动选择</option>';
|
||||
var firstOption = null;
|
||||
$.each(data.data.recordLine, function(index, item){
|
||||
if(item.parent == null){
|
||||
if(!firstOption) firstOption = item.id;
|
||||
lineOptions += '<option value="'+item.id+'">'+item.name+'</option>';
|
||||
}
|
||||
});
|
||||
$('#batch_line_list').html('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">'+lineOptions+'</select>');
|
||||
window.currentRecordLine = data.data.recordLine;
|
||||
if(firstOption){
|
||||
$('#defaultLineSelect').val(firstOption).trigger('change');
|
||||
}
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error : function() {
|
||||
layer.close(ii);
|
||||
layer.alert('获取域名信息失败', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function changeBatchLine(obj){
|
||||
var line = $(obj).val();
|
||||
var flag = false;
|
||||
$("#batch_line_list").children().each(function(index, elem){
|
||||
if(flag) $(elem).remove()
|
||||
if(obj == elem){ flag = true; }
|
||||
})
|
||||
if($(obj).find("option:selected").text() == '子集线路(非必填)') return;
|
||||
if(window.currentRecordLine){
|
||||
var tempLine = window.currentRecordLine.filter((x) => x.parent == line)
|
||||
if(tempLine.length > 0){
|
||||
var option = line.substr(0,2) == 'N.' ? '' : '<option value="'+line+'">子集线路(非必填)</option>';
|
||||
$.each(tempLine, function(index, item){
|
||||
option += '<option value="'+item.id+'">'+item.name+'</option>';
|
||||
})
|
||||
$("#batch_line_list").append('<select name="defaultLine" class="form-control" onchange="changeBatchLine(this)">'+option+'</select>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetBatchForm(){
|
||||
$('#batchInput').val('');
|
||||
$('#batchValueInput').val('');
|
||||
$('#defaultDomainSelect').val(null).trigger('change');
|
||||
$('#defaultTypeSelect').val('');
|
||||
$('#defaultTtlInput').val(600);
|
||||
$('#defaultLineSelect').val('').trigger('change');
|
||||
|
||||
$('#batch_line_list').empty();
|
||||
$('#batch_line_list').append('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)"><option value="">自动选择</option></select>');
|
||||
|
||||
$('#previewSection').hide();
|
||||
parsedBatchData = [];
|
||||
domainMapping = {};
|
||||
}
|
||||
|
||||
function previewBatchData(){
|
||||
var inputText = $('#batchInput').val().trim();
|
||||
|
||||
if(!inputText){
|
||||
layer.alert('请输入批量解析数据', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = inputText.split('\n');
|
||||
var defaultDomainId = $('#defaultDomainSelect').val();
|
||||
var defaultType = $('#defaultTypeSelect').val();
|
||||
var defaultLine = $('#batch_line_list select[name=defaultLine]').last().val() || $('#defaultLineSelect').val();
|
||||
var defaultTtl = $('#defaultTtlInput').val();
|
||||
var batchValue = $('#batchValueInput').val();
|
||||
|
||||
parsedBatchData = [];
|
||||
domainMapping = {};
|
||||
var uniqueDomains = new Set();
|
||||
var errors = [];
|
||||
|
||||
$.each(lines, function(index, line){
|
||||
line = $.trim(line);
|
||||
if(!line) return;
|
||||
|
||||
var parts = line.split(/\s+/);
|
||||
|
||||
if(parts.length == 1 && batchValue){
|
||||
var domainPart = parts[0];
|
||||
var found = false;
|
||||
var host = '@';
|
||||
var domainName = domainPart;
|
||||
|
||||
var sortedDomains = domainList.slice().sort(function(a, b){
|
||||
return b.name.length - a.name.length;
|
||||
});
|
||||
|
||||
$.each(sortedDomains, function(i, domain){
|
||||
var dnsDomainName = domain.name;
|
||||
|
||||
if(domainPart === dnsDomainName){
|
||||
host = '@';
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
else if(domainPart.endsWith('.' + dnsDomainName)){
|
||||
host = domainPart.substring(0, domainPart.length - (dnsDomainName.length + 1));
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
errors.push('第' + (index + 1) + '行:域名 "' + domainPart + '" 不在你的域名列表中');
|
||||
return;
|
||||
}
|
||||
|
||||
value = batchValue;
|
||||
} else if(parts.length < 2){
|
||||
errors.push('第' + (index + 1) + '行格式错误:至少需要主机记录和记录值');
|
||||
return;
|
||||
} else if(parts.length == 2){
|
||||
var hostDomainPart = parts[1];
|
||||
var found = false;
|
||||
|
||||
var sortedDomains = domainList.slice().sort(function(a, b){
|
||||
return b.name.length - a.name.length;
|
||||
});
|
||||
|
||||
$.each(sortedDomains, function(i, domain){
|
||||
var dnsDomainName = domain.name;
|
||||
|
||||
if(hostDomainPart.endsWith('.' + dnsDomainName)){
|
||||
host = hostDomainPart.substring(0, hostDomainPart.length - (dnsDomainName.length + 1));
|
||||
value = parts[0];
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
$.each(domainList, function(i, domain){
|
||||
if(hostDomainPart === domain.name){
|
||||
host = '@';
|
||||
value = parts[0];
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
host = parts[0];
|
||||
value = parts[1];
|
||||
domainName = parts[2] || null;
|
||||
}
|
||||
}
|
||||
} else if(parts.length >= 2){
|
||||
host = parts[0];
|
||||
value = parts[1];
|
||||
domainName = parts[2] || null;
|
||||
}
|
||||
|
||||
var finalDomainId;
|
||||
var finalDomainName;
|
||||
|
||||
if(domainName){
|
||||
finalDomainName = domainName;
|
||||
|
||||
var foundDomain = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name.toLowerCase() === domainName.toLowerCase()){
|
||||
foundDomain = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(foundDomain){
|
||||
finalDomainId = foundDomain.id;
|
||||
domainMapping[domainName] = foundDomain.id;
|
||||
}else{
|
||||
errors.push('第' + (index + 1) + '行:域名 "' + domainName + '" 不在你的域名列表中');
|
||||
return;
|
||||
}
|
||||
}else if(defaultDomainId){
|
||||
finalDomainId = defaultDomainId;
|
||||
var defaultDomainObj = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === defaultDomainId){
|
||||
defaultDomainObj = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
finalDomainName = defaultDomainObj ? defaultDomainObj.name : '';
|
||||
}else{
|
||||
errors.push('第' + (index + 1) + '行:未指定域名且没有设置默认域名');
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueDomains.add(finalDomainName);
|
||||
|
||||
var type = defaultType;
|
||||
if(!type){
|
||||
type = getDnsType(value);
|
||||
}
|
||||
|
||||
parsedBatchData.push({
|
||||
host: host,
|
||||
value: value,
|
||||
type: type,
|
||||
domainId: finalDomainId,
|
||||
domainName: finalDomainName,
|
||||
line: defaultLine,
|
||||
ttl: defaultTtl,
|
||||
lineNumber: index + 1,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
if(errors.length > 0){
|
||||
layer.alert('发现以下错误:\n\n' + errors.join('\n'), {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if(parsedBatchData.length === 0){
|
||||
layer.alert('没有有效的解析记录', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
var uniqueDnsTypes = new Set();
|
||||
var domainDnsMap = {};
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === row.domainId){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
uniqueDnsTypes.add(domainInfo.dnsType);
|
||||
domainDnsMap[row.domainId] = domainInfo.dnsType;
|
||||
}
|
||||
});
|
||||
|
||||
var uniqueDomainIds = new Set(parsedBatchData.map(r => r.domainId));
|
||||
|
||||
if(uniqueDomainIds.size > 1){
|
||||
showDomainSelectionModal(Array.from(uniqueDomainIds));
|
||||
return;
|
||||
}
|
||||
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
function getDnsType(value){
|
||||
value = value.toLowerCase();
|
||||
if(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value)){
|
||||
return 'A';
|
||||
}else if(/^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(value)){
|
||||
return 'CNAME';
|
||||
}else if(/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/.test(value)){
|
||||
return 'AAAA';
|
||||
}else if(/^\d+$/.test(value) && parseInt(value) <= 65535){
|
||||
return 'MX';
|
||||
}else{
|
||||
return 'A';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview(){
|
||||
var html = '';
|
||||
var validCount = 0;
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
var statusHtml = '<span class="status-success"><i class="fa fa-check"></i> 待添加</span>';
|
||||
validCount++;
|
||||
|
||||
html += '<tr>';
|
||||
html += '<td style="text-align:center;">' + row.lineNumber + '</td>';
|
||||
html += '<td>' + (row.host == '@' ? '@ (主域名)' : row.host) + '</td>';
|
||||
html += '<td style="text-align:center;"><span class="label label-primary">' + row.type + '</span></td>';
|
||||
html += '<td title="' + htmlEscape(row.value) + '">' + row.value + '</td>';
|
||||
html += '<td><strong>' + row.domainName + '</strong></td>';
|
||||
html += '<td style="text-align:center;">' + (row.line ? row.line : '默认') + '</td>';
|
||||
html += '<td style="text-align:center;">' + row.ttl + '</td>';
|
||||
html += '<td style="text-align:center;">' + statusHtml + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
$('#previewBody').html(html);
|
||||
$('#previewSummary').html('<strong>共 ' + validCount + ' 条记录待添加</strong>');
|
||||
$('#previewSection').show();
|
||||
}
|
||||
|
||||
function showDomainSelectionModal(domains){
|
||||
var html = '';
|
||||
|
||||
$.each(domains, function(index, domainIdentifier){
|
||||
var domainName = '';
|
||||
var domainId = '';
|
||||
|
||||
if(!isNaN(domainIdentifier)){
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === domainIdentifier){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
domainName = domainInfo.name;
|
||||
domainId = domainInfo.id;
|
||||
}
|
||||
}else{
|
||||
domainName = domainIdentifier;
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name === domainIdentifier){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
domainId = domainInfo.id;
|
||||
}
|
||||
}
|
||||
|
||||
if(!domainName) return;
|
||||
|
||||
var matches = [];
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name === domainName){
|
||||
matches.push(d);
|
||||
}
|
||||
});
|
||||
|
||||
if(matches.length === 0){
|
||||
matches = domainList;
|
||||
}
|
||||
|
||||
html += '<div style="margin-bottom:20px;">';
|
||||
html += '<h5><strong>' + domainName + '</strong></h5>';
|
||||
html += '<div class="row">';
|
||||
$.each(matches, function(j, match){
|
||||
var isSelected = j === 0;
|
||||
html += '<div class="col-md-6">';
|
||||
html += '<div class="domain-item' + (isSelected ? ' selected' : '') + '" ';
|
||||
html += 'data-domain="' + domainName + '" data-id="' + match.id + '" ';
|
||||
html += 'onclick="selectDomainItem(this)">';
|
||||
html += '<strong>' + match.name + '</strong> [' + match.dnsType + ']';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if(isSelected){
|
||||
domainMapping[domainName] = match.id;
|
||||
}
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
||||
$('#domainSelectModal').html(html);
|
||||
$('#modal-domain-select').modal('show');
|
||||
}
|
||||
|
||||
function selectDomainItem(element){
|
||||
var $element = $(element);
|
||||
var domainName = $element.data('domain');
|
||||
var domainId = $element.data('id');
|
||||
|
||||
var $row = $element.closest('.row');
|
||||
$row.find('.domain-item').removeClass('selected');
|
||||
$element.addClass('selected');
|
||||
|
||||
domainMapping[domainName] = domainId;
|
||||
}
|
||||
|
||||
function confirmDomainSelection(){
|
||||
$('#modal-domain-select').modal('hide');
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
if(domainMapping[row.domainName]){
|
||||
row.domainId = domainMapping[row.domainName];
|
||||
}
|
||||
});
|
||||
|
||||
renderPreview();
|
||||
|
||||
layer.msg('域名配置已更新', {icon: 1, time: 1500});
|
||||
}
|
||||
|
||||
function submitBatchData(){
|
||||
if(parsedBatchData.length === 0){
|
||||
layer.alert('请先预览解析结果', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要批量添加这 <strong>' + parsedBatchData.length + '</strong> 条解析记录吗?', {
|
||||
title: '确认批量添加',
|
||||
icon: 0,
|
||||
btn: ['确定添加', '取消']
|
||||
}, function(){
|
||||
executeBatchAdd();
|
||||
});
|
||||
}
|
||||
|
||||
function executeBatchAdd(){
|
||||
var groupedByDomain = {};
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
if(!groupedByDomain[row.domainId]){
|
||||
groupedByDomain[row.domainId] = [];
|
||||
}
|
||||
groupedByDomain[row.domainId].push(row);
|
||||
});
|
||||
|
||||
var totalSuccess = 0;
|
||||
var totalFail = 0;
|
||||
var completedCount = 0;
|
||||
var totalCount = Object.keys(groupedByDomain).length;
|
||||
var failReasons = [];
|
||||
|
||||
var $btn = $('#btnBatchAdd');
|
||||
var btnOrigHtml = $btn.html();
|
||||
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 正在添加...');
|
||||
var ii = layer.load(2);
|
||||
|
||||
$.each(groupedByDomain, function(domainId, records){
|
||||
var recordLines = [];
|
||||
$.each(records, function(i, r){
|
||||
recordLines.push(r.host + ' ' + r.value);
|
||||
});
|
||||
var recordStr = recordLines.join('\n');
|
||||
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/batchadd/' + domainId,
|
||||
data : function(){
|
||||
var data = {
|
||||
record: recordStr,
|
||||
type: records[0].type,
|
||||
ttl: records[0].ttl
|
||||
};
|
||||
if(records[0].line){
|
||||
data.line = records[0].line;
|
||||
}
|
||||
return data;
|
||||
}(),
|
||||
dataType : 'json',
|
||||
async: false,
|
||||
success : function(data) {
|
||||
completedCount++;
|
||||
if(data.code == 0){
|
||||
var match = data.msg.match(/成功(\d+)条/);
|
||||
if(match){
|
||||
totalSuccess += parseInt(match[1]);
|
||||
}
|
||||
var failMatch = data.msg.match(/失败(\d+)条/);
|
||||
if(failMatch){
|
||||
var failCount = parseInt(failMatch[1]);
|
||||
totalFail += failCount;
|
||||
|
||||
if(failCount > 0){
|
||||
var startIndex = records.length - failCount;
|
||||
for(var i = startIndex; i < records.length; i++){
|
||||
var record = records[i];
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:' + data.msg);
|
||||
}
|
||||
}
|
||||
} else if(data.msg.indexOf('失败') !== -1){
|
||||
failReasons.push('域名 ' + records[0].domainName + ':' + data.msg);
|
||||
}
|
||||
}else{
|
||||
totalFail += records.length;
|
||||
$.each(records, function(i, record){
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:' + data.msg);
|
||||
});
|
||||
}
|
||||
|
||||
if(completedCount >= totalCount){
|
||||
layer.close(ii);
|
||||
$btn.prop('disabled', false).html(btnOrigHtml);
|
||||
|
||||
var msg = '批量添加完成!';
|
||||
if(totalSuccess > 0){
|
||||
msg += '\n成功:' + totalSuccess + ' 条';
|
||||
}
|
||||
if(totalFail > 0){
|
||||
msg += '\n失败:' + totalFail + ' 条';
|
||||
if(failReasons.length > 0){
|
||||
msg += '\n\n失败原因:';
|
||||
$.each(failReasons, function(i, reason){
|
||||
msg += '\n' + (i + 1) + '. ' + reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layer.alert(msg, {
|
||||
icon: totalFail > 0 ? 2 : 1,
|
||||
btn: ['确定'],
|
||||
yes: function(index){
|
||||
layer.close(index);
|
||||
try {
|
||||
resetBatchForm();
|
||||
} catch(e) {
|
||||
console.error('Error in callback:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error : function() {
|
||||
completedCount++;
|
||||
totalFail += records.length;
|
||||
$.each(records, function(i, record){
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:网络错误,无法连接服务器');
|
||||
});
|
||||
|
||||
if(completedCount >= totalCount){
|
||||
layer.close(ii);
|
||||
$btn.prop('disabled', false).html(btnOrigHtml);
|
||||
var msg = '批量添加完成!';
|
||||
if(totalSuccess > 0){
|
||||
msg += '\n成功:' + totalSuccess + ' 条';
|
||||
}
|
||||
if(totalFail > 0){
|
||||
msg += '\n失败:' + totalFail + ' 条';
|
||||
if(failReasons.length > 0){
|
||||
msg += '\n\n失败原因:';
|
||||
$.each(failReasons, function(i, reason){
|
||||
msg += '\n' + (i + 1) + '. ' + reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layer.alert(msg, {
|
||||
icon: totalFail > 0 ? 2 : 1,
|
||||
btn: ['确定'],
|
||||
yes: function(index){
|
||||
layer.close(index);
|
||||
try {
|
||||
resetBatchForm();
|
||||
} catch(e) {
|
||||
console.error('Error in callback:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -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}
|
||||
|
||||
@@ -139,6 +139,44 @@
|
||||
@用户不支持企业微信,飞书用户手机号需要填写<a href="https://open.feishu.cn/document/home/user-identity-introduction/open-id" target="_blank" rel="noreferrer">用户ID</a>。
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">自定义Webhook</h3></div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Webhook地址</label>
|
||||
<div class="col-sm-9"><input type="text" name="custom_webhook_url" value="{:config_get('custom_webhook_url')}" class="form-control" placeholder="https://example.com/webhook"/></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">请求方式</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_method" default="{:config_get('custom_webhook_method', 'POST')}"><option value="POST">POST</option><option value="GET">GET</option><option value="PUT">PUT</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Content-Type</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_content_type" default="{:config_get('custom_webhook_content_type', 'application/json')}"><option value="application/json">application/json</option><option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Headers</label>
|
||||
<div class="col-sm-9"><textarea name="custom_webhook_headers" class="form-control" rows="3" placeholder='每行一个,格式:HeaderName: HeaderValue'>{:config_get('custom_webhook_headers')}</textarea></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">请求Body</label>
|
||||
<div class="col-sm-9"><textarea name="custom_webhook_body" class="form-control" rows="4">{php}echo htmlspecialchars(config_get('custom_webhook_body') ?: '{"title":"{title}","content":"{content}"}');{/php}</textarea>
|
||||
<font color="green">支持变量:{title}、{content},如果是GET方式,将作为query参数拼接到url上</font></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">消息内容格式</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_content_format" default="{:config_get('custom_webhook_content_format', 'html')}"><option value="html">HTML</option><option value="markdown">Markdown</option><option value="text">纯文本</option></select></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:customwebhooktest()" class="btn btn-default btn-block">发送测试消息</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -254,5 +292,25 @@ function webhooktest(){
|
||||
}
|
||||
});
|
||||
}
|
||||
function customwebhooktest(){
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
type : 'GET',
|
||||
url : '/system/customwebhooktest',
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.alert(data.msg, {icon: 1});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
},
|
||||
error:function(data){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/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.37.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.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1122,7 +1123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.37.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.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1207,16 +1208,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.37.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.37.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.37.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.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1372,16 +1373,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.37.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.37.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' => '1047',
|
||||
'version' => '1050',
|
||||
|
||||
'dbversion' => '1045'
|
||||
'dbversion' => '1049'
|
||||
];
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
|
||||
RewriteRule ^(.*)$ index.php [L,E=PATH_INFO:/$1]
|
||||
</IfModule>
|
||||
|
||||
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 |
1
public/static/images/npm.svg
Normal file
1
public/static/images/npm.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
BIN
public/static/images/technitium.png
Normal file
BIN
public/static/images/technitium.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 325 B |
@@ -51,12 +51,48 @@ 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/delete/:id', 'cloudflare/hostnames_delete');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/txttargets/:id', 'cloudflare/hostnames_txt_targets');
|
||||
Route::post('/cloudflare/hostnames/batch_add/:id', 'cloudflare/hostnames_batch_add');
|
||||
Route::post('/cloudflare/hostnames/batch_delete/:id', 'cloudflare/hostnames_batch_delete');
|
||||
Route::post('/cloudflare/hostnames/batch_update/:id', 'cloudflare/hostnames_batch_update');
|
||||
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::post('/cloudflare/dcv_delegation_uuid/:id', 'cloudflare/dcv_delegation_uuid');
|
||||
Route::post('/cloudflare/get_domain_default_line', 'cloudflare/get_domain_default_line');
|
||||
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');
|
||||
Route::post('/domain/op', 'domain/domain_op');
|
||||
Route::post('/domain/list', 'domain/domain_list');
|
||||
Route::any('/domain/dnscheck', 'domain/dnscheck');
|
||||
Route::post('/domain/category/data', 'domain/category_data');
|
||||
Route::post('/domain/category/:action', 'domain/category_op');
|
||||
Route::get('/domain/category/list', 'domain/category_list');
|
||||
Route::post('/domain/setcategory', 'domain/domain_set_category');
|
||||
Route::get('/domain/add', 'domain/domain_add');
|
||||
Route::get('/domain/category', 'domain/category');
|
||||
Route::get('/domain', 'domain/domain');
|
||||
|
||||
Route::post('/record/data/:id', 'domain/record_data');
|
||||
@@ -65,6 +101,7 @@ Route::group(function () {
|
||||
Route::post('/record/delete/:id', 'domain/record_delete');
|
||||
Route::post('/record/status/:id', 'domain/record_status');
|
||||
Route::post('/record/remark/:id', 'domain/record_remark');
|
||||
Route::post('/record/check/:id', 'domain/record_check');
|
||||
Route::post('/record/batch/:id', 'domain/record_batch');
|
||||
Route::post('/record/batchedit/:id', 'domain/record_batch_edit');
|
||||
Route::any('/record/batchadd/:id', 'domain/record_batch_add');
|
||||
@@ -74,7 +111,10 @@ 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('/record/smartparse', 'domain/smartparse');
|
||||
Route::post('/record/quickinfo/:id', 'domain/quickinfo');
|
||||
|
||||
Route::get('/dmonitor/overview', 'dmonitor/overview');
|
||||
Route::post('/dmonitor/task/data', 'dmonitor/task_data');
|
||||
@@ -127,6 +167,7 @@ Route::group(function () {
|
||||
Route::get('/system/mailtest', 'system/mailtest');
|
||||
Route::get('/system/tgbottest', 'system/tgbottest');
|
||||
Route::get('/system/webhooktest', 'system/webhooktest');
|
||||
Route::get('/system/customwebhooktest', 'system/customwebhooktest');
|
||||
Route::post('/system/proxytest', 'system/proxytest');
|
||||
Route::get('/system/cronset', 'system/cronset');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user