mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-09 23:16:27 +02:00
Compare commits
93 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 | ||
|
|
ec16c3fc8b | ||
|
|
d1eaaec650 | ||
|
|
224c27d796 | ||
|
|
6aea445259 | ||
|
|
86c557face | ||
|
|
70d3922013 | ||
|
|
e56122d7d0 | ||
|
|
6694631a9a | ||
|
|
2c03dedba0 | ||
|
|
095063dcad | ||
|
|
b6eec27d06 | ||
|
|
b400a62ef9 | ||
|
|
36a731d672 | ||
|
|
dcd829586c | ||
|
|
fb69ed702b | ||
|
|
137193d465 | ||
|
|
d0eb096873 | ||
|
|
ebdc34cf4b | ||
|
|
b19cabcbfd | ||
|
|
64b5221787 | ||
|
|
41e719720c | ||
|
|
16a9c03b6c | ||
|
|
1beb731a6e | ||
|
|
6b026ce4e4 | ||
|
|
96ff262333 | ||
|
|
17ffe5704f | ||
|
|
d4c11b520f | ||
|
|
ba418da84c | ||
|
|
b58db855ca | ||
|
|
3bd45367b0 | ||
|
|
a22bc4fa37 |
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;
|
||||
}
|
||||
}
|
||||
@@ -605,3 +607,10 @@ function getDomainDate($domain)
|
||||
throw new Exception('查询域名whois失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function checkTableExists($table)
|
||||
{
|
||||
$prefix = env('database.prefix', 'dnsmgr_');
|
||||
$res = Db::query("SHOW TABLES LIKE '" . $prefix . $table . "'");
|
||||
return !empty($res);
|
||||
}
|
||||
@@ -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
|
||||
@@ -16,7 +17,6 @@ class Domain extends BaseController
|
||||
public function account()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
View::assign('dnsconfig', DnsHelper::$dns_config);
|
||||
return view();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class Domain extends BaseController
|
||||
|
||||
$select = Db::name('account');
|
||||
if (!empty($kw)) {
|
||||
$select->whereLike('ak|remark', '%' . $kw . '%');
|
||||
$select->whereLike('name|remark', '%' . $kw . '%');
|
||||
}
|
||||
$total = $select->count();
|
||||
$rows = $select->order('id', 'desc')->limit($offset, $limit)->select();
|
||||
@@ -37,39 +37,49 @@ class Domain extends BaseController
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
|
||||
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
return json(['total' => $total, 'rows' => $list]);
|
||||
}
|
||||
|
||||
public function account_add()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]);
|
||||
$action = input('param.action');
|
||||
|
||||
$account = null;
|
||||
if ($action == 'edit') {
|
||||
$id = input('get.id/d');
|
||||
$account = Db::name('account')->where('id', $id)->find();
|
||||
if (empty($account)) return $this->alert('error', '域名账户不存在');
|
||||
}
|
||||
|
||||
View::assign('info', $account);
|
||||
View::assign('typeList', DnsHelper::getList());
|
||||
View::assign('action', $action);
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
public function account_op()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$act = input('param.act');
|
||||
if ($act == 'get') {
|
||||
$id = input('post.id/d');
|
||||
$row = Db::name('account')->where('id', $id)->find();
|
||||
if (!$row) return json(['code' => -1, 'msg' => '域名账户不存在']);
|
||||
return json(['code' => 0, 'data' => $row]);
|
||||
} elseif ($act == 'add') {
|
||||
$action = input('param.action');
|
||||
if ($action == 'add') {
|
||||
$type = input('post.type');
|
||||
$ak = input('post.ak', null, 'trim');
|
||||
$sk = input('post.sk', null, 'trim');
|
||||
$ext = input('post.ext', null, 'trim');
|
||||
$name = input('post.name', null, 'trim');
|
||||
$config = input('post.config', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$proxy = input('post.proxy/d', 0);
|
||||
if (empty($ak) || empty($sk)) return json(['code' => -1, 'msg' => 'AccessKey和SecretKey不能为空']);
|
||||
if (Db::name('account')->where('type', $type)->where('ak', $ak)->find()) {
|
||||
if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']);
|
||||
if (Db::name('account')->where('type', $type)->where('name', $name)->find()) {
|
||||
return json(['code' => -1, 'msg' => '域名账户已存在']);
|
||||
}
|
||||
Db::startTrans();
|
||||
$id = Db::name('account')->insertGetId([
|
||||
'type' => $type,
|
||||
'ak' => $ak,
|
||||
'sk' => $sk,
|
||||
'ext' => $ext,
|
||||
'proxy' => $proxy,
|
||||
'name' => $name,
|
||||
'config' => $config,
|
||||
'remark' => $remark,
|
||||
'addtime' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
@@ -86,27 +96,24 @@ class Domain extends BaseController
|
||||
Db::rollback();
|
||||
return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']);
|
||||
}
|
||||
} elseif ($act == 'edit') {
|
||||
} elseif ($action == 'edit') {
|
||||
$id = input('post.id/d');
|
||||
$row = Db::name('account')->where('id', $id)->find();
|
||||
if (!$row) return json(['code' => -1, 'msg' => '域名账户不存在']);
|
||||
$type = input('post.type');
|
||||
$ak = input('post.ak', null, 'trim');
|
||||
$sk = input('post.sk', null, 'trim');
|
||||
$ext = input('post.ext', null, 'trim');
|
||||
$name = input('post.name', null, 'trim');
|
||||
$config = input('post.config', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$proxy = input('post.proxy/d', 0);
|
||||
if (empty($ak) || empty($sk)) return json(['code' => -1, 'msg' => 'AccessKey和SecretKey不能为空']);
|
||||
if (Db::name('account')->where('type', $type)->where('ak', $ak)->where('id', '<>', $id)->find()) {
|
||||
if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']);
|
||||
if (Db::name('account')->where('type', $type)->where('name', $name)->where('id', '<>', $id)->find()) {
|
||||
return json(['code' => -1, 'msg' => '域名账户已存在']);
|
||||
}
|
||||
Db::startTrans();
|
||||
Db::name('account')->where('id', $id)->update([
|
||||
'type' => $type,
|
||||
'ak' => $ak,
|
||||
'sk' => $sk,
|
||||
'ext' => $ext,
|
||||
'proxy' => $proxy,
|
||||
'name' => $name,
|
||||
'config' => $config,
|
||||
'remark' => $remark,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
$dns = DnsHelper::getModel($id);
|
||||
@@ -122,7 +129,7 @@ class Domain extends BaseController
|
||||
Db::rollback();
|
||||
return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']);
|
||||
}
|
||||
} elseif ($act == 'del') {
|
||||
} elseif ($action == 'del') {
|
||||
$id = input('post.id/d');
|
||||
$dcount = DB::name('domain')->where('aid', $id)->count();
|
||||
if ($dcount > 0) return json(['code' => -1, 'msg' => '该域名账户下存在域名,无法删除']);
|
||||
@@ -151,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();
|
||||
}
|
||||
|
||||
@@ -182,16 +191,28 @@ 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);
|
||||
$id = input('post.id');
|
||||
$aid = input('post.aid', null, 'trim');
|
||||
|
||||
$select = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id');
|
||||
if (!empty($kw)) {
|
||||
$select->whereLike('name|A.remark', '%' . $kw . '%');
|
||||
if (!empty($id)) {
|
||||
$select->where('A.id', $id);
|
||||
} elseif (!empty($kw)) {
|
||||
$select->whereLike('A.name|A.remark', '%' . $kw . '%');
|
||||
}
|
||||
if (!empty($aid)) {
|
||||
$select->where('A.aid', $aid);
|
||||
}
|
||||
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']);
|
||||
}
|
||||
@@ -203,11 +224,30 @@ class Domain extends BaseController
|
||||
}
|
||||
}
|
||||
$total = $select->count();
|
||||
$rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('A.id', 'desc')->limit($offset, $limit)->select();
|
||||
switch ($order) {
|
||||
case '1':
|
||||
$select->order('A.regtime', 'asc');
|
||||
break;
|
||||
case '2':
|
||||
$select->order('A.regtime', 'desc');
|
||||
break;
|
||||
case '3':
|
||||
$select->order('A.expiretime', 'asc');
|
||||
break;
|
||||
case '4':
|
||||
$select->order('A.expiretime', 'desc');
|
||||
break;
|
||||
default:
|
||||
$select->order('A.id', 'desc');
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -259,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;
|
||||
@@ -266,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,
|
||||
]);
|
||||
@@ -274,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();
|
||||
@@ -321,6 +364,12 @@ class Domain extends BaseController
|
||||
Db::name('optimizeip')->where('did', 'in', $ids)->delete();
|
||||
Db::name('sctask')->where('did', 'in', $ids)->delete();
|
||||
return json(['code' => 0, 'msg' => '成功删除' . count($ids) . '个域名!']);
|
||||
} elseif ($act == 'updateexpire') {
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$ids = input('post.ids');
|
||||
if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
$count = Db::name('domain')->where('id', 'in', $ids)->update(['checkstatus' => 0]);
|
||||
return json(['code' => 0, 'msg' => '已提交' . $count . '个域名,约' . ceil($count / 5) . '分钟后刷新完成。']);
|
||||
}
|
||||
return json(['code' => -3]);
|
||||
}
|
||||
@@ -420,6 +469,9 @@ class Domain extends BaseController
|
||||
View::assign('recordLine', $recordLineArr);
|
||||
View::assign('minTTL', $minTTL ? $minTTL : 1);
|
||||
View::assign('dnsconfig', $dnsconfig);
|
||||
if ($dnstype == 'qingcloud') {
|
||||
return view('qingcloud');
|
||||
}
|
||||
return view();
|
||||
}
|
||||
|
||||
@@ -800,6 +852,7 @@ class Domain extends BaseController
|
||||
$line = input('post.line', null, 'trim');
|
||||
$ttl = input('post.ttl/d', 600);
|
||||
$mx = input('post.mx/d', 1);
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$recordlist = explode("\n", $record);
|
||||
|
||||
if (empty($record) || empty($recordlist)) {
|
||||
@@ -821,7 +874,7 @@ class Domain extends BaseController
|
||||
$arr = explode(' ', $record);
|
||||
if (empty($record) || empty($arr[0]) || empty($arr[1])) continue;
|
||||
$thistype = empty($type) ? getDnsType($arr[1]) : $type;
|
||||
$recordid = $dns->addDomainRecord($arr[0], $thistype, $arr[1], $line, $ttl, $mx);
|
||||
$recordid = $dns->addDomainRecord($arr[0], $thistype, $arr[1], $line, $ttl, $mx, null, $remark);
|
||||
if ($recordid) {
|
||||
$this->add_log($drow['name'], '添加解析', $arr[0].' ['.$thistype.'] '.$arr[1].' (线路:'.$line.' TTL:'.$ttl.')');
|
||||
$success++;
|
||||
@@ -963,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);
|
||||
@@ -1065,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()
|
||||
{
|
||||
@@ -1096,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 . '个域名的分类!']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,6 @@ class Index extends BaseController
|
||||
|
||||
if (config('app.dbversion') && config_get('version') != config('app.dbversion')) {
|
||||
$this->db_update();
|
||||
config_set('version', config('app.dbversion'));
|
||||
Cache::clear();
|
||||
}
|
||||
|
||||
$tmp = 'version()';
|
||||
@@ -64,7 +62,7 @@ class Index extends BaseController
|
||||
'framework_version' => app()->version(),
|
||||
'php_version' => PHP_VERSION,
|
||||
'mysql_version' => $mysqlVersion,
|
||||
'software' => $_SERVER['SERVER_SOFTWARE'],
|
||||
'software' => $_SERVER['SERVER_SOFTWARE'] ?? '未知',
|
||||
'os' => php_uname(),
|
||||
'date' => date("Y-m-d H:i:s"),
|
||||
];
|
||||
@@ -87,6 +85,27 @@ class Index extends BaseController
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
config_set('version', config('app.dbversion'));
|
||||
Cache::clear();
|
||||
if(Db::name('account')->count() > 0 && Db::name('account')->whereNotNull('config')->count() == 0) {
|
||||
$accounts = Db::name('account')->select();
|
||||
foreach ($accounts as $account) {
|
||||
if (!empty($account['config']) || !isset(\app\lib\DnsHelper::$dns_config[$account['type']])) continue;
|
||||
$config = [];
|
||||
$account_fields = ['name', 'sk', 'ext'];
|
||||
$i = 0;
|
||||
foreach(\app\lib\DnsHelper::$dns_config[$account['type']]['config'] as $field => $item) {
|
||||
if ($field == 'proxy') {
|
||||
$config[$field] = $account['proxy'];
|
||||
break;
|
||||
}
|
||||
if ($i >= 3) break;
|
||||
$account_field = $account_fields[$i++];
|
||||
$config[$field] = isset($account[$account_field]) ? $account[$account_field] : '';
|
||||
}
|
||||
Db::name('account')->where('id', $account['id'])->update(['config' => json_encode($config)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function changeskin()
|
||||
|
||||
@@ -7,82 +7,128 @@ use Exception;
|
||||
use app\BaseController;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Request;
|
||||
use think\facade\View;
|
||||
use think\facade\Db;
|
||||
|
||||
class Install extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$dbconfig = '0';
|
||||
if (file_exists(app()->getRootPath() . '.env')) {
|
||||
return '当前已经安装成功,如果需要重新安装,请手动删除根目录.env文件';
|
||||
if (checkTableExists('config') || checkTableExists('user') || checkTableExists('domain')) {
|
||||
return '当前已经安装成功,如果需要重新安装,请手动删除根目录.env文件';
|
||||
} else {
|
||||
$dbconfig = '1';
|
||||
}
|
||||
}
|
||||
if (Request::isPost()) {
|
||||
$mysql_host = input('post.mysql_host', null, 'trim');
|
||||
$mysql_port = intval(input('post.mysql_port', '3306'));
|
||||
$mysql_user = input('post.mysql_user', null, 'trim');
|
||||
$mysql_pwd = input('post.mysql_pwd', null, 'trim');
|
||||
$mysql_name = input('post.mysql_name', null, 'trim');
|
||||
$mysql_prefix = input('post.mysql_prefix', 'cloud_', 'trim');
|
||||
$admin_username = input('post.admin_username', null, 'trim');
|
||||
$admin_password = input('post.admin_password', null, 'trim');
|
||||
if ($dbconfig == '1') {
|
||||
$admin_username = input('post.admin_username', null, 'trim');
|
||||
$admin_password = input('post.admin_password', null, 'trim');
|
||||
|
||||
if (!$mysql_host || !$mysql_user || !$mysql_pwd || !$mysql_name || !$admin_username || !$admin_password) {
|
||||
return json(['code' => 0, 'msg' => '必填项不能为空']);
|
||||
}
|
||||
if (!$admin_username || !$admin_password) {
|
||||
return json(['code' => 0, 'msg' => '必填项不能为空']);
|
||||
}
|
||||
|
||||
$configData = file_get_contents(app()->getRootPath() . '.example.env');
|
||||
$configData = str_replace(['{dbhost}', '{dbname}', '{dbuser}', '{dbpwd}', '{dbport}', '{dbprefix}'], [$mysql_host, $mysql_name, $mysql_user, $mysql_pwd, $mysql_port, $mysql_prefix], $configData);
|
||||
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
|
||||
$sqls = explode(';', $sqls);
|
||||
$mysql_prefix = env('database.prefix', 'dnsmgr_');
|
||||
|
||||
try {
|
||||
$DB = new PDO("mysql:host=" . $mysql_host . ";dbname=" . $mysql_name . ";port=" . $mysql_port, $mysql_user, $mysql_pwd);
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() == 2002) {
|
||||
$errorMsg = '连接数据库失败:数据库地址填写错误!';
|
||||
} elseif ($e->getCode() == 1045) {
|
||||
$errorMsg = '连接数据库失败:数据库用户名或密码填写错误!';
|
||||
} elseif ($e->getCode() == 1049) {
|
||||
$errorMsg = '连接数据库失败:数据库名不存在!';
|
||||
$password = password_hash($admin_password, PASSWORD_DEFAULT);
|
||||
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
|
||||
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
|
||||
|
||||
$success = 0;
|
||||
$error = 0;
|
||||
$errorMsg = null;
|
||||
foreach ($sqls as $value) {
|
||||
$value = trim($value);
|
||||
if (empty($value)) continue;
|
||||
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
|
||||
if (Db::execute($value) === false) {
|
||||
$error++;
|
||||
$dberror = Db::getErrorInfo();
|
||||
$errorMsg .= $dberror . "\n";
|
||||
} else {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
if (empty($errorMsg)) {
|
||||
Cache::clear();
|
||||
return json(['code' => 1, 'msg' => '安装完成!成功执行SQL语句' . $success . '条']);
|
||||
} else {
|
||||
$errorMsg = '连接数据库失败:' . $e->getMessage();
|
||||
return json(['code' => 0, 'msg' => $errorMsg]);
|
||||
}
|
||||
return json(['code' => 0, 'msg' => $errorMsg]);
|
||||
}
|
||||
$DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
|
||||
$DB->exec("set sql_mode = ''");
|
||||
$DB->exec("set names utf8");
|
||||
|
||||
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
|
||||
$sqls = explode(';', $sqls);
|
||||
|
||||
$password = password_hash($admin_password, PASSWORD_DEFAULT);
|
||||
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
|
||||
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
|
||||
|
||||
$success = 0;
|
||||
$error = 0;
|
||||
$errorMsg = null;
|
||||
foreach ($sqls as $value) {
|
||||
$value = trim($value);
|
||||
if (empty($value)) continue;
|
||||
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
|
||||
if ($DB->exec($value) === false) {
|
||||
$error++;
|
||||
$dberror = $DB->errorInfo();
|
||||
$errorMsg .= $dberror[2] . "\n";
|
||||
} else {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
if (empty($errorMsg)) {
|
||||
if (!file_put_contents(app()->getRootPath() . '.env', $configData)) {
|
||||
return json(['code' => 0, 'msg' => '保存失败,请确保网站根目录有写入权限']);
|
||||
}
|
||||
Cache::clear();
|
||||
return json(['code' => 1, 'msg' => '安装完成!成功执行SQL语句' . $success . '条']);
|
||||
} else {
|
||||
return json(['code' => 0, 'msg' => $errorMsg]);
|
||||
$mysql_host = input('post.mysql_host', null, 'trim');
|
||||
$mysql_port = intval(input('post.mysql_port', '3306'));
|
||||
$mysql_user = input('post.mysql_user', null, 'trim');
|
||||
$mysql_pwd = input('post.mysql_pwd', null, 'trim');
|
||||
$mysql_name = input('post.mysql_name', null, 'trim');
|
||||
$mysql_prefix = input('post.mysql_prefix', 'cloud_', 'trim');
|
||||
$admin_username = input('post.admin_username', null, 'trim');
|
||||
$admin_password = input('post.admin_password', null, 'trim');
|
||||
|
||||
if (!$mysql_host || !$mysql_user || !$mysql_pwd || !$mysql_name || !$admin_username || !$admin_password) {
|
||||
return json(['code' => 0, 'msg' => '必填项不能为空']);
|
||||
}
|
||||
|
||||
$configData = file_get_contents(app()->getRootPath() . '.example.env');
|
||||
$configData = str_replace(['{dbhost}', '{dbname}', '{dbuser}', '{dbpwd}', '{dbport}', '{dbprefix}'], [$mysql_host, $mysql_name, $mysql_user, $mysql_pwd, $mysql_port, $mysql_prefix], $configData);
|
||||
|
||||
try {
|
||||
$DB = new PDO("mysql:host=" . $mysql_host . ";dbname=" . $mysql_name . ";port=" . $mysql_port, $mysql_user, $mysql_pwd);
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() == 2002) {
|
||||
$errorMsg = '连接数据库失败:数据库地址填写错误!';
|
||||
} elseif ($e->getCode() == 1045) {
|
||||
$errorMsg = '连接数据库失败:数据库用户名或密码填写错误!';
|
||||
} elseif ($e->getCode() == 1049) {
|
||||
$errorMsg = '连接数据库失败:数据库名不存在!';
|
||||
} else {
|
||||
$errorMsg = '连接数据库失败:' . $e->getMessage();
|
||||
}
|
||||
return json(['code' => 0, 'msg' => $errorMsg]);
|
||||
}
|
||||
$DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
|
||||
$DB->exec("set sql_mode = ''");
|
||||
$DB->exec("set names utf8");
|
||||
|
||||
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
|
||||
$sqls = explode(';', $sqls);
|
||||
|
||||
$password = password_hash($admin_password, PASSWORD_DEFAULT);
|
||||
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
|
||||
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
|
||||
|
||||
$success = 0;
|
||||
$error = 0;
|
||||
$errorMsg = null;
|
||||
foreach ($sqls as $value) {
|
||||
$value = trim($value);
|
||||
if (empty($value)) continue;
|
||||
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
|
||||
if ($DB->exec($value) === false) {
|
||||
$error++;
|
||||
$dberror = $DB->errorInfo();
|
||||
$errorMsg .= $dberror[2] . "\n";
|
||||
} else {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
if (empty($errorMsg)) {
|
||||
if (!file_put_contents(app()->getRootPath() . '.env', $configData)) {
|
||||
return json(['code' => 0, 'msg' => '保存失败,请确保网站根目录有写入权限']);
|
||||
}
|
||||
Cache::clear();
|
||||
return json(['code' => 1, 'msg' => '安装完成!成功执行SQL语句' . $success . '条']);
|
||||
} else {
|
||||
return json(['code' => 0, 'msg' => $errorMsg]);
|
||||
}
|
||||
}
|
||||
}
|
||||
View::assign('dbconfig', $dbconfig);
|
||||
return view();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -174,6 +174,44 @@ location / {
|
||||
],
|
||||
]
|
||||
],
|
||||
'litessl' => [
|
||||
'name' => 'LiteSSL',
|
||||
'class' => 1,
|
||||
'icon' => 'litessl.ico',
|
||||
'wildcard' => true,
|
||||
'max_domains' => 100,
|
||||
'cname' => true,
|
||||
'note' => '<a href="https://freessl.cn/automation/eab-manager" target="_blank" rel="noreferrer">LiteSSL密钥获取</a>',
|
||||
'inputs' => [
|
||||
'email' => [
|
||||
'name' => '邮箱地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'EAB申请邮箱',
|
||||
'required' => true,
|
||||
],
|
||||
'kid' => [
|
||||
'name' => 'EAB KID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'key' => [
|
||||
'name' => 'EAB HMAC Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
]
|
||||
],
|
||||
'tencent' => [
|
||||
'name' => '腾讯云免费SSL',
|
||||
'class' => 2,
|
||||
@@ -219,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',
|
||||
@@ -233,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,
|
||||
@@ -370,9 +466,8 @@ class DeployHelper
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在网站管理->证书管理查看证书的ID,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -435,9 +530,8 @@ class DeployHelper
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在站点->证书管理查看证书的ID,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -559,7 +653,7 @@ class DeployHelper
|
||||
'icon' => 'opanel.png',
|
||||
'desc' => '更新面板证书管理内的SSL证书',
|
||||
'note' => null,
|
||||
'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书',
|
||||
'tasknote' => '',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
@@ -581,7 +675,7 @@ class DeployHelper
|
||||
'v1' => '1.x',
|
||||
'v2' => '2.x',
|
||||
],
|
||||
'value' => 'v1',
|
||||
'value' => 'v2',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
@@ -594,7 +688,32 @@ class DeployHelper
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [],
|
||||
'taskinputs' => [
|
||||
'type' => [
|
||||
'name' => '部署类型',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '更新已有证书',
|
||||
'3' => '面板本身的证书',
|
||||
],
|
||||
'value' => '0',
|
||||
'required' => true,
|
||||
],
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '在证书列表查看ID',
|
||||
'note' => '留空为根据关联SSL证书的域名,自动更新对应证书',
|
||||
'show' => 'type==0',
|
||||
],
|
||||
'node_name' => [
|
||||
'name' => '节点名称',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '每行一个子节点名称',
|
||||
'note' => '不填写时:只更新主节点证书;填写时:同时更新主节点和所有指定的子节点证书。每行填写一个子节点名称',
|
||||
'show' => 'type==0',
|
||||
],
|
||||
],
|
||||
],
|
||||
'mwpanel' => [
|
||||
'name' => 'MW面板',
|
||||
@@ -654,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,
|
||||
@@ -754,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,
|
||||
@@ -857,7 +1082,7 @@ class DeployHelper
|
||||
sudo visudo<br/>
|
||||
#在文件最后一行增加以下内容,需要将username替换成自己的用户名<br/>
|
||||
username ALL=(ALL) NOPASSWD: NOPASSWD: ALL<br/>
|
||||
ctrl+x 保存退出',
|
||||
ctrl+x 保存退出<br/>',
|
||||
'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书',
|
||||
'inputs' => [
|
||||
'host' => [
|
||||
@@ -1023,8 +1248,10 @@ ctrl+x 保存退出',
|
||||
['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'],
|
||||
@@ -1035,6 +1262,8 @@ ctrl+x 保存退出',
|
||||
['value'=>'vod', 'label'=>'视频点播'],
|
||||
['value'=>'fc', 'label'=>'函数计算3.0'],
|
||||
['value'=>'fc2', 'label'=>'函数计算2.0'],
|
||||
['value'=>'ga', 'label'=>'全球加速'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1043,7 +1272,14 @@ ctrl+x 保存退出',
|
||||
'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' => [
|
||||
@@ -1068,7 +1304,7 @@ ctrl+x 保存退出',
|
||||
['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' => [
|
||||
@@ -1124,6 +1360,29 @@ ctrl+x 保存退出',
|
||||
'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',
|
||||
@@ -1132,21 +1391,21 @@ ctrl+x 保存退出',
|
||||
['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\'',
|
||||
'placeholder' => '多个域名可用,隔开',
|
||||
'show' => 'product!=\'esa\'&&product!=\'esa_saas\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'&&product!=\'wafres\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1199,6 +1458,8 @@ ctrl+x 保存退出',
|
||||
['value'=>'tse', 'label'=>'云原生API网关TSE'],
|
||||
['value'=>'tcb', 'label'=>'云开发TCB'],
|
||||
['value'=>'lighthouse', 'label'=>'轻量应用服务器'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
['value'=>'update', 'label'=>'更新证书内容(证书ID不变)'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1298,10 +1559,18 @@ ctrl+x 保存退出',
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product!=\'clb\'&&product!=\'tke\'',
|
||||
'show' => 'product!=\'clb\'&&product!=\'tke\'&&product!=\'upload\'',
|
||||
'note' => 'CDN、EO、WAF多个域名可用,隔开,其他只能填写1个域名',
|
||||
'required' => true,
|
||||
],
|
||||
'cert_id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '要更新的证书ID,在我的证书列表查看',
|
||||
'show' => 'product==\'update\'',
|
||||
'required' => true,
|
||||
'note' => '当前接口需联系加白使用',
|
||||
],
|
||||
],
|
||||
],
|
||||
'huawei' => [
|
||||
@@ -1341,15 +1610,31 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'内容分发网络CDN'],
|
||||
['value'=>'elb', 'label'=>'弹性负载均衡ELB'],
|
||||
['value'=>'waf', 'label'=>'Web应用防火墙WAF'],
|
||||
['value'=>'obs', 'label'=>'对象存储服务OBS'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
],
|
||||
'obs_endpoint' => [
|
||||
'name' => 'Endpoint地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '填写示例:obs.cn-north-4.myhuaweicloud.com',
|
||||
'show' => 'product==\'obs\'',
|
||||
'required' => true,
|
||||
],
|
||||
'obs_bucket' => [
|
||||
'name' => '桶名称',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product==\'obs\'',
|
||||
'required' => true,
|
||||
],
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product==\'cdn\'',
|
||||
'show' => 'product==\'cdn\'||product==\'obs\'',
|
||||
'required' => true,
|
||||
],
|
||||
'project_id' => [
|
||||
@@ -1444,6 +1729,7 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'CDN'],
|
||||
['value'=>'oss', 'label'=>'OSS'],
|
||||
['value'=>'pili', 'label'=>'视频直播'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1459,6 +1745,7 @@ ctrl+x 保存退出',
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product!=\'upload\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1569,6 +1856,7 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'CDN'],
|
||||
['value'=>'blb', 'label'=>'普通型BLB'],
|
||||
['value'=>'appblb', 'label'=>'应用型BLB'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1702,7 +1990,9 @@ ctrl+x 保存退出',
|
||||
['value'=>'alb', 'label'=>'应用型负载均衡ALB'],
|
||||
['value'=>'tos', 'label'=>'对象存储TOS'],
|
||||
['value'=>'live', 'label'=>'视频直播'],
|
||||
['value'=>'vod', 'label'=>'视频点播'],
|
||||
['value'=>'imagex', 'label'=>'veImageX'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1714,11 +2004,28 @@ ctrl+x 保存退出',
|
||||
'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',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product!=\'clb\'&&product!=\'alb\'',
|
||||
'show' => 'product!=\'clb\'&&product!=\'alb\'&&product!=\'upload\'',
|
||||
'required' => true,
|
||||
],
|
||||
'listener_id' => [
|
||||
@@ -1914,14 +2221,26 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'CDN加速'],
|
||||
['value'=>'icdn', 'label'=>'全站加速'],
|
||||
['value'=>'accessone', 'label'=>'边缘安全加速平台'],
|
||||
['value'=>'cf', 'label'=>'函数计算'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
],
|
||||
'region_id' => [
|
||||
'name' => '所属地域',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value'=>'bb9fdb42056f11eda1610242ac110002', 'label'=>'华东1'],
|
||||
['value'=>'200000002368', 'label'=>'西南1'],
|
||||
],
|
||||
'value' => 'bb9fdb42056f11eda1610242ac110002',
|
||||
'show' => 'product==\'cf\'',
|
||||
'required' => true,
|
||||
],
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -2000,9 +2319,8 @@ ctrl+x 保存退出',
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在SSL证书->我的证书页面查看,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -2248,6 +2566,12 @@ ctrl+x 保存退出',
|
||||
'required' => true,
|
||||
'show' => 'auth==1',
|
||||
],
|
||||
'passphrase' => [
|
||||
'name' => '私钥密码',
|
||||
'type' => 'input',
|
||||
'placeholder' => '若私钥有设置密码,请填写此项',
|
||||
'show' => 'auth==1',
|
||||
],
|
||||
'windows' => [
|
||||
'name' => '是否Windows',
|
||||
'type' => 'radio',
|
||||
@@ -2413,6 +2737,73 @@ ctrl+x 保存退出',
|
||||
],
|
||||
],
|
||||
],
|
||||
'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,
|
||||
|
||||
@@ -9,9 +9,30 @@ class DnsHelper
|
||||
public static $dns_config = [
|
||||
'aliyun' => [
|
||||
'name' => '阿里云',
|
||||
'icon' => 'aliyun.png',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'AccessKeyId',
|
||||
'sk' => 'AccessKeySecret',
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'AccessKeySecret' => [
|
||||
'name' => 'AccessKeySecret',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 1, //是否支持备注,1单独设置备注,2和记录一起设置
|
||||
'status' => true, //是否支持启用暂停
|
||||
@@ -23,9 +44,30 @@ class DnsHelper
|
||||
],
|
||||
'dnspod' => [
|
||||
'name' => '腾讯云',
|
||||
'icon' => 'dnspod.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'SecretId',
|
||||
'sk' => 'SecretKey',
|
||||
'SecretId' => [
|
||||
'name' => 'SecretId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretKey' => [
|
||||
'name' => 'SecretKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 1,
|
||||
'status' => true,
|
||||
@@ -37,9 +79,30 @@ class DnsHelper
|
||||
],
|
||||
'huawei' => [
|
||||
'name' => '华为云',
|
||||
'icon' => 'huawei.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'AccessKeyId',
|
||||
'sk' => 'SecretAccessKey',
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretAccessKey' => [
|
||||
'name' => 'SecretAccessKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
@@ -51,9 +114,30 @@ class DnsHelper
|
||||
],
|
||||
'baidu' => [
|
||||
'name' => '百度云',
|
||||
'icon' => 'baidu.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'AccessKey',
|
||||
'sk' => 'SecretKey',
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretAccessKey' => [
|
||||
'name' => 'SecretAccessKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => false,
|
||||
@@ -65,9 +149,30 @@ class DnsHelper
|
||||
],
|
||||
'west' => [
|
||||
'name' => '西部数码',
|
||||
'icon' => 'west.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => '用户名',
|
||||
'sk' => 'API密码',
|
||||
'username' => [
|
||||
'name' => '用户名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'api_password' => [
|
||||
'name' => 'API密码',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => true,
|
||||
@@ -79,9 +184,30 @@ class DnsHelper
|
||||
],
|
||||
'huoshan' => [
|
||||
'name' => '火山引擎',
|
||||
'icon' => 'huoshan.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'AccessKeyId',
|
||||
'sk' => 'SecretAccessKey',
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretAccessKey' => [
|
||||
'name' => 'SecretAccessKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
@@ -93,9 +219,30 @@ class DnsHelper
|
||||
],
|
||||
'jdcloud' => [
|
||||
'name' => '京东云',
|
||||
'icon' => 'jdcloud.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'AccessKeyId',
|
||||
'sk' => 'AccessKeySecret',
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'AccessKeySecret' => [
|
||||
'name' => 'AccessKeySecret',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => true,
|
||||
@@ -107,9 +254,30 @@ class DnsHelper
|
||||
],
|
||||
'dnsla' => [
|
||||
'name' => 'DNSLA',
|
||||
'icon' => 'dnsla.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'APIID',
|
||||
'sk' => 'API密钥',
|
||||
'apiid' => [
|
||||
'name' => 'APIID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apisecret' => [
|
||||
'name' => 'API密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => true,
|
||||
@@ -119,11 +287,117 @@ class DnsHelper
|
||||
'page' => false,
|
||||
'add' => true,
|
||||
],
|
||||
'qingcloud' => [
|
||||
'name' => '青云',
|
||||
'icon' => 'qingcloud.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'access_key_id' => [
|
||||
'name' => 'Access Key ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'secret_access_key' => [
|
||||
'name' => 'Secret Access Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => true,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => true,
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
'bt' => [
|
||||
'name' => '宝塔域名',
|
||||
'icon' => 'bt.png',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'AccessKey' => [
|
||||
'name' => 'Access Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretKey' => [
|
||||
'name' => 'Secret Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'AccountID' => [
|
||||
'name' => 'Account ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => true,
|
||||
'page' => false,
|
||||
'add' => true,
|
||||
],
|
||||
'cloudflare' => [
|
||||
'name' => 'Cloudflare',
|
||||
'icon' => 'cloudflare.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => '邮箱地址',
|
||||
'sk' => 'API密钥/令牌',
|
||||
'email' => [
|
||||
'name' => '邮箱地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API密钥/令牌',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'auth' => [
|
||||
'name' => '认证方式',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => 'API密钥',
|
||||
'1' => 'API令牌',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
@@ -135,9 +409,30 @@ class DnsHelper
|
||||
],
|
||||
'namesilo' => [
|
||||
'name' => 'NameSilo',
|
||||
'icon' => 'namesilo.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => '账户名',
|
||||
'sk' => 'API Key',
|
||||
'username' => [
|
||||
'name' => '账户名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => false,
|
||||
@@ -147,12 +442,73 @@ class DnsHelper
|
||||
'page' => true,
|
||||
'add' => false,
|
||||
],
|
||||
'spaceship' => [
|
||||
'name' => 'Spaceship',
|
||||
'icon' => 'spaceship.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'apikey' => [
|
||||
'name' => 'API Key',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apisecret' => [
|
||||
'name' => 'API Secret',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => false,
|
||||
'redirect' => true,
|
||||
'log' => false,
|
||||
'weight' => false,
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
'powerdns' => [
|
||||
'name' => 'PowerDNS',
|
||||
'icon' => 'powerdns.ico',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'ak' => 'IP地址',
|
||||
'sk' => '端口',
|
||||
'ext' => 'API KEY',
|
||||
'ip' => [
|
||||
'name' => 'IP地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'port' => [
|
||||
'name' => '端口',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API KEY',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
@@ -162,6 +518,172 @@ 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',
|
||||
'note' => '仅支持以NS方式接入阿里云ESA的域名',
|
||||
'config' => [
|
||||
'AccessKeyId' => [
|
||||
'name' => 'AccessKeyId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'AccessKeySecret' => [
|
||||
'name' => 'AccessKeySecret',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'region' => [
|
||||
'name' => 'API接入点',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value' => 'cn-hangzhou', 'label' => '中国内地'],
|
||||
['value' => 'ap-southeast-1', 'label' => '非中国内地'],
|
||||
],
|
||||
'value' => 'cn-hangzhou',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => false,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => false,
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
'tencenteo' => [
|
||||
'name' => '腾讯云EO',
|
||||
'icon' => 'tencent.png',
|
||||
'note' => '仅支持以NS方式接入腾讯云EO的域名',
|
||||
'config' => [
|
||||
'SecretId' => [
|
||||
'name' => 'SecretId',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'SecretKey' => [
|
||||
'name' => 'SecretKey',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'site_type' => [
|
||||
'name' => 'API接入点',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value' => 'cn', 'label' => '中国内地'],
|
||||
['value' => 'intl', 'label' => '非中国内地'],
|
||||
],
|
||||
'value' => 'cn',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 0,
|
||||
'status' => true,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => true,
|
||||
'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 = [
|
||||
@@ -173,9 +695,15 @@ class DnsHelper
|
||||
'huoshan' => ['DEF' => 'default', 'CT' => 'telecom', 'CU' => 'unicom', 'CM' => 'mobile', 'AB' => 'oversea'],
|
||||
'baidu' => ['DEF' => 'default', 'CT' => 'ct', 'CU' => 'cnc', 'CM' => 'cmnet', 'AB' => ''],
|
||||
'jdcloud' => ['DEF' => '-1', 'CT' => '1', 'CU' => '2', 'CM' => '3', 'AB' => '4'],
|
||||
'bt' => ['DEF' => '0', 'CT' => '285344768', 'CU' => '285345792', 'CM' => '285346816'],
|
||||
'qingcloud' => ['DEF' => '0', 'CT' => '2', 'CU' => '3', 'CM' => '4', 'AB' => '8'],
|
||||
'cloudflare' => ['DEF' => '0'],
|
||||
'namesilo' => ['DEF' => 'default'],
|
||||
'powerdns' => ['DEF' => 'default'],
|
||||
'spaceship' => ['DEF' => 'default'],
|
||||
'aliyunesa' => ['DEF' => '0'],
|
||||
'tencenteo' => ['DEF' => 'Default'],
|
||||
'cccyun' => ['DEF' => 'default'],
|
||||
];
|
||||
|
||||
public static function getList()
|
||||
@@ -195,11 +723,12 @@ class DnsHelper
|
||||
*/
|
||||
public static function getModel($aid, $domain = null, $domainid = null)
|
||||
{
|
||||
$config = self::getConfig($aid);
|
||||
if (!$config) return false;
|
||||
$dnstype = $config['type'];
|
||||
$account = self::getConfig($aid);
|
||||
if (!$account) return false;
|
||||
$dnstype = $account['type'];
|
||||
$class = "\\app\\lib\\dns\\{$dnstype}";
|
||||
if (class_exists($class)) {
|
||||
$config = json_decode($account['config'] ?? '', true);
|
||||
$config['domain'] = $domain;
|
||||
$config['domainid'] = $domainid;
|
||||
$model = new $class($config);
|
||||
@@ -211,13 +740,14 @@ class DnsHelper
|
||||
/**
|
||||
* @return DnsInterface|bool
|
||||
*/
|
||||
public static function getModel2($config)
|
||||
public static function getModel2($account)
|
||||
{
|
||||
$dnstype = $config['type'];
|
||||
$dnstype = $account['type'];
|
||||
$class = "\\app\\lib\\dns\\{$dnstype}";
|
||||
if (class_exists($class)) {
|
||||
$config['domain'] = $config['name'];
|
||||
$config['domainid'] = $config['thirdid'];
|
||||
$config = json_decode($account['config'] ?? '', true);
|
||||
$config['domain'] = $account['name'];
|
||||
$config['domainid'] = $account['thirdid'];
|
||||
$model = new $class($config);
|
||||
return $model;
|
||||
}
|
||||
|
||||
@@ -27,18 +27,18 @@ class aliyun implements CertInterface
|
||||
|
||||
public function register()
|
||||
{
|
||||
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空');
|
||||
$param = ['Action' => 'ListUserCertificateOrder'];
|
||||
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) throw new Exception('必填参数不能为空');
|
||||
$param = ['Action' => 'ListInstances'];
|
||||
$this->request($param, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function buyCert($domainList, &$order)
|
||||
{
|
||||
$param = ['Action' => 'DescribePackageState', 'ProductCode' => 'digicert-free-1-free'];
|
||||
$param = ['Action' => 'GetInstanceSummary', 'InstanceType' => 'TEST'];
|
||||
$data = $this->request($param, true);
|
||||
if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('没有可用的免费证书资源包');
|
||||
$this->log('证书资源包总数量:' . $data['TotalCount'] . ',已使用数量:' . $data['UsedCount']);
|
||||
if (!isset($data['InactiveCount']) || $data['InactiveCount'] == 0) throw new Exception('没有待使用的测试证书实例,请先购买测试证书');
|
||||
$this->log('实例总个数:' . $data['TotalCount'] . ',实例待使用总数:' . $data['InactiveCount']);
|
||||
}
|
||||
|
||||
public function createOrder($domainList, &$order, $keytype, $keysize)
|
||||
@@ -46,31 +46,92 @@ class aliyun implements CertInterface
|
||||
if (empty($domainList)) throw new Exception('域名列表不能为空');
|
||||
$domain = $domainList[0];
|
||||
$param = [
|
||||
'Action' => 'CreateCertificateRequest',
|
||||
'ProductCode' => 'digicert-free-1-free',
|
||||
'Username' => $this->config['username'],
|
||||
'Phone' => $this->config['phone'],
|
||||
'Email' => $this->config['email'],
|
||||
'Domain' => $domain,
|
||||
'ValidateType' => 'DNS'
|
||||
'Action' => 'ListInstances',
|
||||
'Status' => 'inactive',
|
||||
'InstanceType' => 'TEST',
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
if (empty($data['OrderId'])) throw new Exception('证书申请失败,OrderId为空');
|
||||
$order['OrderId'] = $data['OrderId'];
|
||||
|
||||
sleep(3);
|
||||
if (empty($data['InstanceList'])) throw new Exception('待使用的测试证书实例列表为空');
|
||||
$instanceId = $data['InstanceList'][0]['InstanceId'];
|
||||
|
||||
$param = [
|
||||
'Action' => 'DescribeCertificateState',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'ListContact',
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
if (empty($data['ContactList'])) throw new Exception('联系人列表为空,请先添加联系人');
|
||||
$contactId = $data['ContactList'][0]['ContactId'];
|
||||
|
||||
if ($keytype == 'ECC') $KeyAlgorithm = 'ECC_256';
|
||||
else if ($keysize == '3072') $KeyAlgorithm = 'RSA_3072';
|
||||
else $KeyAlgorithm = 'RSA_2048';
|
||||
$param = [
|
||||
'Action' => 'UpdateInstance',
|
||||
'InstanceId' => $instanceId,
|
||||
'Domain' => $domain,
|
||||
'KeyAlgorithm' => $KeyAlgorithm,
|
||||
'AutoReissue' => 'disable',
|
||||
'ContactIdList.1' => $contactId,
|
||||
'ValidateType' => 'DNS'
|
||||
];
|
||||
try {
|
||||
$this->request($param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('更新证书实例失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$param = [
|
||||
'Action' => 'ApplyCertificate',
|
||||
'InstanceId' => $instanceId
|
||||
];
|
||||
try {
|
||||
$this->request($param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('申请证书失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
|
||||
$status = '';
|
||||
do {
|
||||
$param = [
|
||||
'Action' => 'GetTaskAttribute',
|
||||
'TaskId' => $instanceId
|
||||
];
|
||||
try {
|
||||
$data = $this->request($param, true);
|
||||
$status = $data['TaskStatus'];
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('申请证书提交结果查询失败:' . $e->getMessage());
|
||||
}
|
||||
if ($status == 'processing') {
|
||||
sleep(1);
|
||||
} elseif ($status == 'failed') {
|
||||
throw new Exception('申请证书失败:' . $data['TaskMessage']);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while ($status == 'processing');
|
||||
|
||||
|
||||
$param = [
|
||||
'Action' => 'GetInstanceDetail',
|
||||
'InstanceId' => $instanceId
|
||||
];
|
||||
try {
|
||||
$data = $this->request($param, true);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取实例详情失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$order['InstanceId'] = $instanceId;
|
||||
|
||||
$dnsList = [];
|
||||
if ($data['Type'] == 'domain_verify') {
|
||||
$mainDomain = getMainDomain($domain);
|
||||
$name = substr($data['RecordDomain'], 0, -(strlen($mainDomain) + 1));
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']];
|
||||
if (!empty($data['DomainValidationList'])) {
|
||||
foreach ($data['DomainValidationList'] as $opts) {
|
||||
$mainDomain = getMainDomain($opts['Domain']);
|
||||
$name = substr($opts['ValidationKey'] . '.' . $opts['RootDomain'], 0, - (strlen($mainDomain) + 1));
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $opts['ValidationType'], 'value' => $opts['ValidationValue']];
|
||||
}
|
||||
}
|
||||
|
||||
return $dnsList;
|
||||
@@ -81,13 +142,13 @@ class aliyun implements CertInterface
|
||||
public function getAuthStatus($domainList, $order)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'DescribeCertificateState',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'GetInstanceDetail',
|
||||
'InstanceId' => $order['InstanceId'],
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
if ($data['Type'] == 'certificate') {
|
||||
if ($data['Status'] == 'normal') {
|
||||
return true;
|
||||
} elseif ($data['Type'] == 'verify_fail') {
|
||||
} elseif ($data['Status'] == 'closed') {
|
||||
throw new Exception('证书审核失败');
|
||||
} else {
|
||||
return false;
|
||||
@@ -97,12 +158,19 @@ class aliyun implements CertInterface
|
||||
public function finalizeOrder($domainList, $order, $keytype, $keysize)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'DescribeCertificateState',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'GetInstanceDetail',
|
||||
'InstanceId' => $order['InstanceId'],
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
$fullchain = $data['Certificate'];
|
||||
$private_key = $data['PrivateKey'];
|
||||
if (empty($data['CertificateId'])) throw new Exception('证书ID不存在');
|
||||
|
||||
$param = [
|
||||
'Action' => 'GetUserCertificateDetail',
|
||||
'CertId' => $data['CertificateId'],
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
$fullchain = $data['Cert'];
|
||||
$private_key = $data['Key'];
|
||||
if (empty($fullchain) || empty($private_key)) throw new Exception('证书内容获取失败');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
@@ -113,8 +181,8 @@ class aliyun implements CertInterface
|
||||
public function revoke($order, $pem)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'CancelCertificateForPackageRequest',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'RevokeCertificate',
|
||||
'InstanceId' => $order['InstanceId'],
|
||||
];
|
||||
$this->request($param);
|
||||
}
|
||||
@@ -122,22 +190,14 @@ class aliyun implements CertInterface
|
||||
public function cancel($order)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'DescribeCertificateState',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'GetInstanceDetail',
|
||||
'InstanceId' => $order['InstanceId'],
|
||||
];
|
||||
$data = $this->request($param, true);
|
||||
if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process') {
|
||||
if ($data['Status'] == 'pending') {
|
||||
$param = [
|
||||
'Action' => 'CancelOrderRequest',
|
||||
'OrderId' => $order['OrderId'],
|
||||
];
|
||||
$this->request($param);
|
||||
usleep(500000);
|
||||
}
|
||||
if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process' || $data['Type'] == 'payed' || $data['Type'] == 'verify_fail') {
|
||||
$param = [
|
||||
'Action' => 'DeleteCertificateRequest',
|
||||
'OrderId' => $order['OrderId'],
|
||||
'Action' => 'CancelPendingCertificate',
|
||||
'InstanceId' => $order['InstanceId'],
|
||||
];
|
||||
$this->request($param);
|
||||
}
|
||||
|
||||
@@ -62,10 +62,14 @@ class customacme implements CertInterface
|
||||
|
||||
$dnsList = [];
|
||||
if (!empty($order['challenges'])) {
|
||||
$keys = [];
|
||||
foreach ($order['challenges'] as $opts) {
|
||||
$key = $opts['key'] . '|' .$opts['value'];
|
||||
if (in_array($key, $keys)) continue;
|
||||
$mainDomain = getMainDomain($opts['domain']);
|
||||
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
120
app/lib/cert/litessl.php
Normal file
120
app/lib/cert/litessl.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\cert;
|
||||
|
||||
use app\lib\CertInterface;
|
||||
use app\lib\acme\ACMECert;
|
||||
use Exception;
|
||||
|
||||
class litessl implements CertInterface
|
||||
{
|
||||
private $directory = 'https://acme.litessl.com/acme/v2/directory';
|
||||
private $ac;
|
||||
private $config;
|
||||
private $ext;
|
||||
|
||||
public function __construct($config, $ext = null)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->ac = new ACMECert($this->directory, (int)$config['proxy']);
|
||||
if ($ext) {
|
||||
$this->ext = $ext;
|
||||
$this->ac->loadAccountKey($ext['key']);
|
||||
$this->ac->setAccount($ext['kid']);
|
||||
}
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
|
||||
if (empty($this->config['kid']) || empty($this->config['key'])) {
|
||||
throw new Exception('EAB密钥不能为空');
|
||||
}
|
||||
|
||||
if (!empty($this->ext['key'])) {
|
||||
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
|
||||
return ['kid' => $kid, 'key' => $this->ext['key']];
|
||||
}
|
||||
|
||||
$key = $this->ac->generateRSAKey(2048);
|
||||
$this->ac->loadAccountKey($key);
|
||||
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
|
||||
return ['kid' => $kid, 'key' => $key];
|
||||
}
|
||||
|
||||
public function buyCert($domainList, &$order)
|
||||
{
|
||||
}
|
||||
|
||||
public function createOrder($domainList, &$order, $keytype, $keysize)
|
||||
{
|
||||
$domain_config = [];
|
||||
foreach ($domainList as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$domain_config[$domain] = ['challenge' => 'dns-01'];
|
||||
}
|
||||
if (empty($domain_config)) throw new Exception('域名列表不能为空');
|
||||
|
||||
$order = $this->ac->createOrder($domain_config);
|
||||
|
||||
$dnsList = [];
|
||||
if (!empty($order['challenges'])) {
|
||||
$keys = [];
|
||||
foreach ($order['challenges'] as $opts) {
|
||||
$key = $opts['key'] . '|' .$opts['value'];
|
||||
if (in_array($key, $keys)) continue;
|
||||
$mainDomain = getMainDomain($opts['domain']);
|
||||
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
|
||||
/*if (!array_key_exists($mainDomain, $dnsList)) {
|
||||
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "litessl.cn"'];
|
||||
}*/
|
||||
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $dnsList;
|
||||
}
|
||||
|
||||
public function authOrder($domainList, $order)
|
||||
{
|
||||
$this->ac->authOrder($order);
|
||||
}
|
||||
|
||||
public function getAuthStatus($domainList, $order)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function finalizeOrder($domainList, $order, $keytype, $keysize)
|
||||
{
|
||||
if (empty($domainList)) throw new Exception('域名列表不能为空');
|
||||
|
||||
if ($keytype == 'ECC') {
|
||||
if (empty($keysize)) $keysize = '384';
|
||||
$private_key = $this->ac->generateECKey($keysize);
|
||||
} else {
|
||||
if (empty($keysize)) $keysize = '2048';
|
||||
$private_key = $this->ac->generateRSAKey($keysize);
|
||||
}
|
||||
$fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key);
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']];
|
||||
}
|
||||
|
||||
public function revoke($order, $pem)
|
||||
{
|
||||
$this->ac->revoke($pem);
|
||||
}
|
||||
|
||||
public function cancel($order)
|
||||
{
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->ac->setLogger($func);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class Ctyun
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function request($method, $path, $query = null, $params = null)
|
||||
public function request($method, $path, $query = null, $params = null, $header = null)
|
||||
{
|
||||
if (!empty($query)) {
|
||||
$query = array_filter($query, function ($a) { return $a !== null;});
|
||||
@@ -50,6 +50,11 @@ class Ctyun
|
||||
if ($body) {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if (!empty($header)) {
|
||||
foreach ($header as $key => $value) {
|
||||
$headers[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$authorization = $this->generateSign($query, $headers, $body, $date);
|
||||
$headers['Eop-Authorization'] = $authorization;
|
||||
@@ -151,7 +156,7 @@ class Ctyun
|
||||
curl_close($ch);
|
||||
|
||||
$arr = json_decode($response, true);
|
||||
if (isset($arr['statusCode']) && $arr['statusCode'] == 100000) {
|
||||
if (isset($arr['statusCode']) && ($arr['statusCode'] == 100000 || $arr['statusCode'] == 0 && $this->endpoint == 'cf-global.ctapi.ctyun.cn')) {
|
||||
return isset($arr['returnObj']) ? $arr['returnObj'] : true;
|
||||
} elseif (isset($arr['errorMessage'])) {
|
||||
throw new Exception($arr['errorMessage']);
|
||||
|
||||
232
app/lib/client/HuaweiOBS.php
Normal file
232
app/lib/client/HuaweiOBS.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\client;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* 华为云OBS
|
||||
*/
|
||||
class HuaweiOBS
|
||||
{
|
||||
private $AccessKeyId;
|
||||
private $SecretAccessKey;
|
||||
private $Endpoint;
|
||||
private $proxy = false;
|
||||
|
||||
public function __construct($AccessKeyId, $SecretAccessKey, $Endpoint, $proxy = false)
|
||||
{
|
||||
$this->AccessKeyId = $AccessKeyId;
|
||||
$this->SecretAccessKey = $SecretAccessKey;
|
||||
$this->Endpoint = $Endpoint;
|
||||
$this->proxy = $proxy;
|
||||
}
|
||||
|
||||
public function setBucketCustomdomain($bucket, $domain, $cert_name, $fullchain, $privatekey)
|
||||
{
|
||||
$strXml = <<<EOF
|
||||
<CustomDomainConfiguration>
|
||||
</CustomDomainConfiguration>
|
||||
EOF;
|
||||
$xml = new \SimpleXMLElement($strXml);
|
||||
$xml->addChild('Name', $cert_name);
|
||||
$xml->addChild('Certificate', $fullchain);
|
||||
$xml->addChild('PrivateKey', $privatekey);
|
||||
$body = $xml->asXML();
|
||||
|
||||
$options = [
|
||||
'bucket' => $bucket,
|
||||
'key' => '',
|
||||
];
|
||||
$query = [
|
||||
'customdomain' => $domain
|
||||
];
|
||||
return $this->request('PUT', '/', $query, $body, $options);
|
||||
}
|
||||
|
||||
public function deleteBucketCustomdomain($bucket, $domain)
|
||||
{
|
||||
$options = [
|
||||
'bucket' => $bucket,
|
||||
'key' => '',
|
||||
];
|
||||
$query = [
|
||||
'customdomain' => $domain
|
||||
];
|
||||
return $this->request('DELETE', '/', $query, '', $options);
|
||||
}
|
||||
|
||||
public function getBucketCustomdomain($bucket)
|
||||
{
|
||||
$options = [
|
||||
'bucket' => $bucket,
|
||||
'key' => '',
|
||||
];
|
||||
$query = [
|
||||
'customdomain' => '',
|
||||
];
|
||||
return $this->request('GET', '/', $query, '', $options);
|
||||
}
|
||||
|
||||
private function request($method, $path, $query, $body, $options)
|
||||
{
|
||||
$hostname = $options['bucket'] . '.' . $this->Endpoint;
|
||||
$query_string = $this->toQueryString($query);
|
||||
$query_string = empty($query_string) ? '' : '?' . $query_string;
|
||||
$requestUrl = 'https://' . $hostname . $path . $query_string;
|
||||
$headers = [
|
||||
'Content-Type' => 'application/xml',
|
||||
'Content-MD5' => base64_encode(md5($body, true)),
|
||||
'Date' => gmdate('D, d M Y H:i:s \G\M\T'),
|
||||
];
|
||||
$headers['Authorization'] = $this->getAuthorization($method, $path, $query, $headers, $options);
|
||||
$header = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
$header[] = $key . ': ' . $value;
|
||||
}
|
||||
return $this->curl($method, $requestUrl, $body, $header);
|
||||
}
|
||||
|
||||
private function curl($method, $url, $body, $header)
|
||||
{
|
||||
$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, $header);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
if (!empty($body)) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($errno) {
|
||||
$errmsg = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception('Curl error: ' . $errmsg);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 300) {
|
||||
if (empty($response)) return true;
|
||||
return $this->xml2array($response);
|
||||
}
|
||||
$arr = $this->xml2array($response);
|
||||
if (isset($arr['Message'])) {
|
||||
throw new Exception($arr['Message']);
|
||||
} else {
|
||||
throw new Exception('HTTP Code: ' . $httpCode);
|
||||
}
|
||||
}
|
||||
|
||||
private function toQueryString($params = array())
|
||||
{
|
||||
$temp = array();
|
||||
uksort($params, 'strnatcasecmp');
|
||||
foreach ($params as $key => $value) {
|
||||
if (is_string($key) && !is_array($value)) {
|
||||
if (strlen($value) > 0) {
|
||||
$temp[] = rawurlencode($key) . '=' . rawurlencode($value);
|
||||
} else {
|
||||
$temp[] = rawurlencode($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode('&', $temp);
|
||||
}
|
||||
|
||||
private function xml2array($xml)
|
||||
{
|
||||
if (!$xml) {
|
||||
return false;
|
||||
}
|
||||
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
|
||||
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
|
||||
}
|
||||
|
||||
private function getAuthorization($method, $url, $query, $headers, $options)
|
||||
{
|
||||
$method = strtoupper($method);
|
||||
$date = $headers['Date'];
|
||||
$resourcePath = $this->getResourcePath($options);
|
||||
$stringToSign = $this->calcStringToSign($method, $date, $headers, $resourcePath, $query);
|
||||
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $this->SecretAccessKey, true));
|
||||
return 'OBS ' . $this->AccessKeyId . ':' . $signature;
|
||||
}
|
||||
|
||||
private function getResourcePath(array $options)
|
||||
{
|
||||
$resourcePath = '/';
|
||||
if (strlen($options['bucket']) > 0) {
|
||||
$resourcePath .= $options['bucket'] . '/';
|
||||
}
|
||||
if (strlen($options['key']) > 0) {
|
||||
$resourcePath .= $options['key'];
|
||||
}
|
||||
return $resourcePath;
|
||||
}
|
||||
|
||||
private function calcStringToSign($method, $date, array $headers, $resourcePath, array $query)
|
||||
{
|
||||
/*
|
||||
SignToString =
|
||||
VERB + "\n"
|
||||
+ Content-MD5 + "\n"
|
||||
+ Content-Type + "\n"
|
||||
+ Date + "\n"
|
||||
+ CanonicalizedOSSHeaders
|
||||
+ CanonicalizedResource
|
||||
Signature = base64(hmac-sha1(AccessKeySecret, SignToString))
|
||||
*/
|
||||
$contentMd5 = '';
|
||||
$contentType = '';
|
||||
// CanonicalizedOSSHeaders
|
||||
$signheaders = array();
|
||||
foreach ($headers as $key => $value) {
|
||||
$lowk = strtolower($key);
|
||||
if (strncmp($lowk, "x-obs-", 6) == 0) {
|
||||
$signheaders[$lowk] = $value;
|
||||
} else if ($lowk === 'content-md5') {
|
||||
$contentMd5 = $value;
|
||||
} else if ($lowk === 'content-type') {
|
||||
$contentType = $value;
|
||||
}
|
||||
}
|
||||
ksort($signheaders);
|
||||
$canonicalizedOSSHeaders = '';
|
||||
foreach ($signheaders as $key => $value) {
|
||||
$canonicalizedOSSHeaders .= $key . ':' . $value . "\n";
|
||||
}
|
||||
// CanonicalizedResource
|
||||
$signquery = array();
|
||||
foreach ($query as $key => $value) {
|
||||
if (in_array($key, $this->signKeyList)) {
|
||||
$signquery[$key] = $value;
|
||||
}
|
||||
}
|
||||
ksort($signquery);
|
||||
$sortedQueryList = array();
|
||||
foreach ($signquery as $key => $value) {
|
||||
if (strlen($value) > 0) {
|
||||
$sortedQueryList[] = $key . '=' . $value;
|
||||
} else {
|
||||
$sortedQueryList[] = $key;
|
||||
}
|
||||
}
|
||||
$queryStringSorted = implode('&', $sortedQueryList);
|
||||
$canonicalizedResource = $resourcePath;
|
||||
if (!empty($queryStringSorted)) {
|
||||
$canonicalizedResource .= '?' . $queryStringSorted;
|
||||
}
|
||||
return $method . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n" . $canonicalizedOSSHeaders . $canonicalizedResource;
|
||||
}
|
||||
|
||||
private $signKeyList = array(
|
||||
'acl', 'policy', 'torrent', 'logging', 'location', 'storageinfo', 'quota', 'storagepolicy', 'requestpayment', 'versions', 'versioning', 'versionid', 'uploads', 'uploadid', 'partnumber', 'website', 'notification', 'lifecycle', 'deletebucket', 'delete', 'cors', 'restore', 'tagging', 'response-content-type', 'response-content-language', 'response-expires', 'response-cache-control', 'response-content-disposition', 'response-content-encoding', 'x-image-process', 'backtosource', 'storageclass', 'replication', 'append', 'position', 'x-oss-process', 'CDNNotifyConfiguration', 'attname', 'customdomain', 'directcoldaccess', 'encryption', 'inventory', 'length', 'metadata', 'modify', 'name', 'rename', 'truncate', 'x-image-save-bucket', 'x-image-save-object', 'x-obs-security-token', 'x-obs-callback'
|
||||
);
|
||||
}
|
||||
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,11 @@ 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('未知的产品类型');
|
||||
}
|
||||
@@ -131,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)
|
||||
@@ -200,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) {
|
||||
@@ -214,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,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;
|
||||
}
|
||||
@@ -242,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 . ' 证书添加成功!');
|
||||
}
|
||||
@@ -255,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';
|
||||
@@ -287,49 +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['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';
|
||||
|
||||
@@ -348,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('证书解析失败');
|
||||
@@ -374,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);
|
||||
@@ -452,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);
|
||||
@@ -495,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)
|
||||
@@ -721,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;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ class baidu implements DeployInterface
|
||||
$this->deploy_blb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'appblb') {
|
||||
$this->deploy_appblb($cert_id, $config);
|
||||
} elseif ($config['product'] == 'upload') {
|
||||
} else {
|
||||
throw new Exception('不支持的产品类型');
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,39 @@ class cdnfly implements DeployInterface
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
$id = $config['id'];
|
||||
if (empty($id)) throw new Exception('证书ID不能为空');
|
||||
if (empty($id)) {
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
|
||||
$params = [
|
||||
'type' => 'custom',
|
||||
'name' => $cert_name,
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
if ($this->auth == 1) {
|
||||
$access_token = $this->login();
|
||||
$url = $this->url . '/v1/certs';
|
||||
$body = json_encode($params);
|
||||
$headers = [
|
||||
'Access-Token' => $access_token,
|
||||
];
|
||||
$response = http_request($url, $body, null, null, $headers, $this->proxy, 'POST');
|
||||
$result = json_decode($response['body'], true);
|
||||
if (isset($result['code']) && $result['code'] == 0) {
|
||||
$id = $result['data'];
|
||||
} elseif (isset($result['msg'])) {
|
||||
throw new Exception('证书添加失败,' . $result['msg']);
|
||||
} else {
|
||||
throw new Exception('证书添加失败,返回数据解析失败');
|
||||
}
|
||||
} else {
|
||||
$id = $this->request('/v1/certs', $params, 'POST');
|
||||
}
|
||||
$this->log("证书ID:{$id}添加成功!");
|
||||
$info['config']['id'] = $id;
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'type' => 'custom',
|
||||
|
||||
@@ -39,6 +39,8 @@ class ctyun implements DeployInterface
|
||||
$this->deploy_icdn($fullchain, $privatekey, $config);
|
||||
} elseif ($config['product'] == 'accessone') {
|
||||
$this->deploy_accessone($fullchain, $privatekey, $config);
|
||||
} elseif ($config['product'] == 'cf') {
|
||||
$this->deploy_cf($fullchain, $privatekey, $config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,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)
|
||||
@@ -96,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)
|
||||
@@ -131,43 +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/accessone/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);
|
||||
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());
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -52,7 +52,7 @@ class fnos implements DeployInterface
|
||||
$this->exec($connection, '上传证书文件', "sudo tee ".$certPath." > /dev/null <<'EOF'\n".$fullchain."\nEOF");
|
||||
$this->exec($connection, '上传私钥文件', "sudo tee ".$keyPath." > /dev/null <<'EOF'\n".$privatekey."\nEOF");
|
||||
$this->exec($connection, '刷新目录权限', 'sudo chmod 0755 "'.$certDir.'" -R');
|
||||
$this->exec($connection, '更新数据表', 'sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"');
|
||||
$this->exec($connection, '更新数据表', 'cd /tmp && sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"');
|
||||
$this->log('证书 '.$row['domain'].' 更新成功');
|
||||
$success++;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use app\lib\client\HuaweiCloud;
|
||||
use app\lib\client\HuaweiOBS;
|
||||
use Exception;
|
||||
|
||||
class huawei implements DeployInterface
|
||||
@@ -39,6 +40,11 @@ class huawei implements DeployInterface
|
||||
$this->deploy_elb($fullchain, $privatekey, $config);
|
||||
} elseif ($config['product'] == 'waf') {
|
||||
$this->deploy_waf($fullchain, $privatekey, $config);
|
||||
} elseif ($config['product'] == 'obs') {
|
||||
$this->deploy_obs($fullchain, $privatekey, $config);
|
||||
} elseif ($config['product'] == 'upload') {
|
||||
$cert_id = $this->get_cert_id($fullchain, $privatekey);
|
||||
$info['cert_id'] = $cert_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +123,19 @@ class huawei implements DeployInterface
|
||||
$this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_obs($fullchain, $privatekey, $config)
|
||||
{
|
||||
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
|
||||
if (empty($config['obs_endpoint'])) throw new Exception('OBS Endpoint不能为空');
|
||||
if (empty($config['obs_bucket'])) throw new Exception('OBS 桶名称不能为空');
|
||||
$obsClient = new HuaweiOBS($this->AccessKeyId, $this->SecretAccessKey, $config['obs_endpoint'], $this->proxy);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$obsClient->setBucketCustomdomain($config['obs_bucket'], $domain, $config['cert_name'], $fullchain, $privatekey);
|
||||
$this->log('OSS域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function get_cert_id($fullchain, $privatekey)
|
||||
{
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
|
||||
@@ -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('绑定的域名不能为空');
|
||||
@@ -191,7 +220,7 @@ class huoshan implements DeployInterface
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
|
||||
|
||||
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy);
|
||||
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'certificate-service.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy);
|
||||
$param = [
|
||||
'Tag' => $cert_name,
|
||||
'Repeatable' => false,
|
||||
@@ -207,10 +236,20 @@ class huoshan implements DeployInterface
|
||||
}
|
||||
if (!empty($data['InstanceId'])) {
|
||||
$cert_id = $data['InstanceId'];
|
||||
$this->log('上传证书成功 CertId=' . $cert_id);
|
||||
|
||||
$param = [
|
||||
'InstanceId' => $cert_id,
|
||||
'Options' => [
|
||||
'ExpiredNotice' => 'Disabled',
|
||||
],
|
||||
];
|
||||
$client->request('POST', 'CertificateUpdateInstance', $param);
|
||||
|
||||
} else {
|
||||
$cert_id = $data['RepeatId'];
|
||||
$this->log('找到已上传的证书 CertId=' . $cert_id);
|
||||
}
|
||||
$this->log('上传证书成功 CertId=' . $cert_id);
|
||||
return $cert_id;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class kuocai implements DeployInterface
|
||||
|
||||
private function request($path, $params = null, $json = false)
|
||||
{
|
||||
$url = 'https://kuocai.cn' . $path;
|
||||
$url = 'https://www.kuocaicdn.com' . $path;
|
||||
$body = $json ? json_encode($params) : $params;
|
||||
$headers = [];
|
||||
if ($json) $headers['Content-Type'] = 'application/json';
|
||||
|
||||
@@ -41,13 +41,29 @@ class lecdn implements DeployInterface
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
$id = $config['id'];
|
||||
if (empty($id)) throw new Exception('证书ID不能为空');
|
||||
|
||||
if ($this->auth == 0) {
|
||||
$this->login();
|
||||
}
|
||||
|
||||
$id = $config['id'];
|
||||
if (empty($id)) {
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
|
||||
$params = [
|
||||
'name' => $cert_name,
|
||||
'type' => 'upload',
|
||||
'ssl_pem' => base64_encode($fullchain),
|
||||
'ssl_key' => base64_encode($privatekey),
|
||||
'auto_renewal' => false,
|
||||
];
|
||||
$data = $this->request('/prod-api/certificate', $params, 'POST');
|
||||
$id = $data['id'];
|
||||
$this->log("证书ID:{$id}添加成功!");
|
||||
$info['config']['id'] = $id;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->request('/prod-api/certificate/' . $id);
|
||||
} catch (Exception $e) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -27,15 +27,131 @@ class opanel implements DeployInterface
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
// 解析节点名称列表
|
||||
$nodeNames = $this->parseNodeNames($config);
|
||||
|
||||
if (isset($config['type']) && $config['type'] == '3') {
|
||||
// 面板本身的证书部署
|
||||
$params = [
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
'ssl' => 'Enable',
|
||||
'sslID' => null,
|
||||
'sslType' => 'import-paste',
|
||||
];
|
||||
|
||||
if (empty($nodeNames)) {
|
||||
// 没有指定节点,只部署到主控节点
|
||||
try {
|
||||
$this->request('/core/settings/ssl/update', $params);
|
||||
$this->log("面板证书更新成功!");
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
throw new Exception("面板证书更新失败:" . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
// 同时部署到主节点和所有指定的子节点
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
// 先更新主节点
|
||||
try {
|
||||
$this->request('/core/settings/ssl/update', $params);
|
||||
$this->log("主节点面板证书更新成功!");
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->log("主节点面板证书更新失败:" . $e->getMessage());
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
// 然后更新所有子节点
|
||||
foreach ($nodeNames as $nodeName) {
|
||||
try {
|
||||
$this->request('/core/settings/ssl/update', $params, $nodeName);
|
||||
$this->log("节点 [{$nodeName}] 面板证书更新成功!");
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->log("节点 [{$nodeName}] 面板证书更新失败:" . $e->getMessage());
|
||||
$failCount++;
|
||||
}
|
||||
}
|
||||
if ($failCount > 0 && $successCount == 0) {
|
||||
throw new Exception("所有节点证书更新失败");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有指定节点,则只部署到主控节点
|
||||
if (empty($nodeNames)) {
|
||||
$this->deployToNode($fullchain, $privatekey, $config, null);
|
||||
} else {
|
||||
// 同时部署到主节点和所有指定的子节点
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
// 先更新主节点
|
||||
try {
|
||||
$this->deployToNode($fullchain, $privatekey, $config, null);
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->log("主节点部署失败:" . $e->getMessage());
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
// 然后更新所有子节点
|
||||
foreach ($nodeNames as $nodeName) {
|
||||
try {
|
||||
$this->deployToNode($fullchain, $privatekey, $config, $nodeName);
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->log("节点 [{$nodeName}] 部署失败:" . $e->getMessage());
|
||||
$failCount++;
|
||||
}
|
||||
}
|
||||
if ($failCount > 0 && $successCount == 0) {
|
||||
throw new Exception("所有节点部署失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署到指定节点
|
||||
*/
|
||||
private function deployToNode($fullchain, $privatekey, $config, $nodeName = null)
|
||||
{
|
||||
if (!empty($config['id'])) {
|
||||
// 指定证书ID的情况
|
||||
$params = [
|
||||
'sslID' => intval($config['id']),
|
||||
'type' => 'paste',
|
||||
'certificate' => $fullchain,
|
||||
'privateKey' => $privatekey,
|
||||
'description' => '',
|
||||
];
|
||||
try {
|
||||
$this->request('/websites/ssl/upload', $params, $nodeName);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "证书ID:{$config['id']}更新成功!";
|
||||
$this->log($logMsg);
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新失败:" : "证书ID:{$config['id']}更新失败:";
|
||||
throw new Exception($logMsg . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 根据域名自动匹配证书
|
||||
$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);
|
||||
$this->log('获取证书列表成功(total=' . $data['total'] . ')');
|
||||
$data = $this->request("/websites/ssl/search", $params, $nodeName);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] " : "";
|
||||
$this->log($logMsg . '获取证书列表成功(total=' . $data['total'] . ')');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取证书列表失败:' . $e->getMessage());
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] " : "";
|
||||
throw new Exception($logMsg . '获取证书列表失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
@@ -62,18 +178,29 @@ class opanel implements DeployInterface
|
||||
'description' => '',
|
||||
];
|
||||
try {
|
||||
$this->request('/websites/ssl/upload', $params);
|
||||
$this->log("证书ID:{$row['id']}更新成功!");
|
||||
$this->request('/websites/ssl/upload', $params, $nodeName);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新成功!" : "证书ID:{$row['id']}更新成功!";
|
||||
$this->log($logMsg);
|
||||
$success++;
|
||||
} catch (Exception $e) {
|
||||
$errmsg = $e->getMessage();
|
||||
$this->log("证书ID:{$row['id']}更新失败:" . $errmsg);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "证书ID:{$row['id']}更新失败:";
|
||||
$this->log($logMsg . $errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($success == 0) {
|
||||
throw new Exception($errmsg ? $errmsg : '没有要更新的证书');
|
||||
$params = [
|
||||
'sslID' => 0,
|
||||
'type' => 'paste',
|
||||
'certificate' => $fullchain,
|
||||
'privateKey' => $privatekey,
|
||||
'description' => '',
|
||||
];
|
||||
$this->request('/websites/ssl/upload', $params, $nodeName);
|
||||
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "证书上传成功!";
|
||||
$this->log($logMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +216,32 @@ class opanel implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function request($path, $params = null)
|
||||
/**
|
||||
* 解析节点名称列表
|
||||
*/
|
||||
private function parseNodeNames($config)
|
||||
{
|
||||
if (!isset($config['node_name']) || empty($config['node_name'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$nodeNameStr = trim($config['node_name']);
|
||||
if (empty($nodeNameStr)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 按行分割,过滤空行
|
||||
$nodeNames = array_filter(
|
||||
array_map('trim', explode("\n", $nodeNameStr)),
|
||||
function($name) {
|
||||
return !empty($name);
|
||||
}
|
||||
);
|
||||
|
||||
return array_values($nodeNames);
|
||||
}
|
||||
|
||||
private function request($path, $params = null, $nodeName = null)
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
|
||||
@@ -97,8 +249,12 @@ class opanel implements DeployInterface
|
||||
$token = md5('1panel' . $this->key . $timestamp);
|
||||
$headers = [
|
||||
'1Panel-Token' => $token,
|
||||
'1Panel-Timestamp' => $timestamp
|
||||
'1Panel-Timestamp' => $timestamp,
|
||||
];
|
||||
// 只有子节点时才设置 CurrentNode 头,主节点时不设置该头
|
||||
if (!empty($nodeName)) {
|
||||
$headers['CurrentNode'] = $nodeName;
|
||||
}
|
||||
$body = $params ? json_encode($params) : '{}';
|
||||
if ($body) $headers['Content-Type'] = 'application/json';
|
||||
$response = http_request($url, $body, null, null, $headers, $this->proxy);
|
||||
|
||||
@@ -37,6 +37,9 @@ class qiniu implements DeployInterface
|
||||
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
|
||||
|
||||
$cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name);
|
||||
$info['cert_id'] = $cert_id;
|
||||
$info['cert_name'] = $cert_name;
|
||||
if ($config['product'] == 'upload') return;
|
||||
|
||||
foreach (explode(',', $domains) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
@@ -50,8 +53,6 @@ class qiniu implements DeployInterface
|
||||
throw new Exception('未知的产品类型');
|
||||
}
|
||||
}
|
||||
$info['cert_id'] = $cert_id;
|
||||
$info['cert_name'] = $cert_name;
|
||||
}
|
||||
|
||||
private function deploy_cdn($domain, $cert_id)
|
||||
|
||||
@@ -26,19 +26,43 @@ class rainyun implements DeployInterface
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if (empty($config['id'])) throw new Exception('证书ID不能为空');
|
||||
if (empty($config['id'])) {
|
||||
$params = [
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
try {
|
||||
$this->request('/product/sslcenter/', $params, 'POST');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('上传证书失败,' . $e->getMessage());
|
||||
}
|
||||
|
||||
$params = [
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
try {
|
||||
$this->request('/product/sslcenter/' . $config['id'], $params, 'PUT');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception($e->getMessage());
|
||||
$params = [
|
||||
'options' => '{"columnFilters":{"Domain":""},"sort":[],"page":1,"perPage":1}',
|
||||
];
|
||||
try {
|
||||
$data = $this->request('/product/sslcenter/?' . http_build_query($params), null, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取证书列表失败,' . $e->getMessage());
|
||||
}
|
||||
if (empty($data['Records'])) throw new Exception('未找到已上传的证书');
|
||||
$cert_id = $data['Records'][0]['ID'];
|
||||
$info['config']['id'] = $cert_id;
|
||||
|
||||
$this->log('证书ID:' . $cert_id . '添加成功!');
|
||||
} else {
|
||||
$params = [
|
||||
'cert' => $fullchain,
|
||||
'key' => $privatekey,
|
||||
];
|
||||
try {
|
||||
$this->request('/product/sslcenter/' . $config['id'], $params, 'PUT');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception($e->getMessage());
|
||||
}
|
||||
|
||||
$this->log('证书ID:' . $config['id'] . '更新成功!');
|
||||
}
|
||||
|
||||
$this->log('证书ID:' . $config['id'] . '更新成功!');
|
||||
}
|
||||
|
||||
private function request($path, $params = null, $method = null)
|
||||
@@ -55,7 +79,7 @@ class rainyun implements DeployInterface
|
||||
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
|
||||
$result = json_decode($response['body'], true);
|
||||
if (isset($result['code']) && $result['code'] == 200) {
|
||||
return $result;
|
||||
return isset($result['data']) ? $result['data'] : null;
|
||||
} elseif (isset($result['message'])) {
|
||||
throw new Exception($result['message']);
|
||||
} else {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\CertHelper;
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
@@ -49,7 +50,8 @@ class ssh implements DeployInterface
|
||||
fclose($stream);
|
||||
$this->log('私钥已保存到:' . $config['pem_key_file']);
|
||||
} elseif ($config['format'] == 'pfx') {
|
||||
$pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
|
||||
$pfx_pass = $config['pfx_pass'] ?? null;
|
||||
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_pass);
|
||||
|
||||
$stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w');
|
||||
if (!$stream) {
|
||||
@@ -157,8 +159,20 @@ class ssh implements DeployInterface
|
||||
file_put_contents($privateKeyPath, $this->config['privatekey']);
|
||||
file_put_contents($publicKeyPath, $publicKey);
|
||||
umask($umask);
|
||||
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'])) {
|
||||
|
||||
@@ -31,8 +31,12 @@ class tencent implements DeployInterface
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if ($config['product'] == 'update') {
|
||||
return $this->update_cert($fullchain, $privatekey, $config);
|
||||
}
|
||||
$cert_id = $this->get_cert_id($fullchain, $privatekey);
|
||||
if (!$cert_id) throw new Exception('证书ID获取失败');
|
||||
$info['cert_id'] = $cert_id;
|
||||
if ($config['product'] == 'cos') {
|
||||
if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空');
|
||||
if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空');
|
||||
@@ -62,6 +66,8 @@ class tencent implements DeployInterface
|
||||
return $this->deploy_scf($cert_id, $config);
|
||||
} elseif ($config['product'] == 'teo' && isset($config['site_id'])) {
|
||||
return $this->deploy_teo($cert_id, $config);
|
||||
} elseif ($config['product'] == 'upload') {
|
||||
return;
|
||||
} else {
|
||||
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
|
||||
if ($config['product'] == 'waf') {
|
||||
@@ -74,7 +80,6 @@ class tencent implements DeployInterface
|
||||
}
|
||||
try {
|
||||
$record_id = $this->deploy_common($config['product'], $cert_id, $instance_id);
|
||||
$info['cert_id'] = $cert_id;
|
||||
$info['record_id'] = $record_id;
|
||||
} catch (Exception $e) {
|
||||
if (isset($info['record_id'])) {
|
||||
@@ -281,6 +286,95 @@ class tencent implements DeployInterface
|
||||
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function update_cert($fullchain, $privatekey, $config)
|
||||
{
|
||||
if (empty($config['cert_id'])) throw new Exception('证书ID不能为空');
|
||||
|
||||
$param = [
|
||||
'CertificateIds' => [$config['cert_id']],
|
||||
'IsCache' => 1,
|
||||
];
|
||||
try {
|
||||
$data = $this->client->request('CreateCertificateBindResourceSyncTask', $param);
|
||||
if (empty($data['CertTaskIds'])) throw new Exception('返回任务ID为空');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('创建关联云资源查询任务失败:' . $e->getMessage());
|
||||
}
|
||||
$task_id = $data['CertTaskIds'][0]['TaskId'];
|
||||
$this->log('创建关联云资源查询任务成功 TaskId=' . $task_id);
|
||||
|
||||
$retry = 0;
|
||||
$resource_result = null;
|
||||
while ($retry++ < 30) {
|
||||
sleep(2);
|
||||
$param = [
|
||||
'TaskIds' => [$task_id],
|
||||
];
|
||||
try {
|
||||
$data = $this->client->request('DescribeCertificateBindResourceTaskResult', $param);
|
||||
if (empty($data['SyncTaskBindResourceResult'])) throw new Exception('返回结果为空');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询关联云资源任务结果失败:' . $e->getMessage());
|
||||
}
|
||||
$taskResult = $data['SyncTaskBindResourceResult'][0];
|
||||
if ($taskResult['Status'] == 1) {
|
||||
$resource_result = $taskResult['BindResourceResult'];
|
||||
break;
|
||||
} elseif ($taskResult['Status'] == 2) {
|
||||
throw new Exception('关联云资源查询任务执行失败:' . isset($taskResult['Error']) ? $taskResult['Error']['Message'] : '未知错误');
|
||||
}
|
||||
};
|
||||
if (!$resource_result) {
|
||||
throw new Exception('关联云资源查询任务超时未完成,请稍后重试');
|
||||
}
|
||||
|
||||
$resourceTypes = [];
|
||||
$resourceTypesRegions = [];
|
||||
foreach ($resource_result as $res) {
|
||||
if ($res['ResourceType'] != 'clb') continue;
|
||||
$totalCount = 0;
|
||||
$regions = [];
|
||||
foreach ($res['BindResourceRegionResult'] as $regionRes) {
|
||||
if ($regionRes['TotalCount'] > 0) {
|
||||
$totalCount += $regionRes['TotalCount'];
|
||||
if (!empty($regionRes['Region'])) {
|
||||
$regions[] = $regionRes['Region'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($totalCount > 0) {
|
||||
$resourceTypes[] = $res['ResourceType'];
|
||||
if (!empty($regions)) {
|
||||
$resourceTypesRegions[] = [
|
||||
'ResourceType' => $res['ResourceType'],
|
||||
'Regions' => $regions,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$param = [
|
||||
'OldCertificateId' => $config['cert_id'],
|
||||
'CertificatePublicKey' => $fullchain,
|
||||
'CertificatePrivateKey' => $privatekey,
|
||||
'ResourceTypes' => $resourceTypes,
|
||||
'ResourceTypesRegions' => $resourceTypesRegions,
|
||||
];
|
||||
$retry = 0;
|
||||
while ($retry++ < 10) {
|
||||
try {
|
||||
$data = $this->client->request('UploadUpdateCertificateInstance', $param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('更新证书内容失败:' . $e->getMessage());
|
||||
}
|
||||
if ($data['DeployStatus'] == 1) {
|
||||
break;
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
$this->log('更新证书内容成功,可能需要一些时间完成各资源的证书更新部署');
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
|
||||
@@ -31,9 +31,15 @@ class upyun implements DeployInterface
|
||||
$this->login();
|
||||
|
||||
$url = 'https://console.upyun.com/api/https/certificate/';
|
||||
// 如果是 EC 证书,调整私钥头为 EC PRIVATE KEY
|
||||
$privatekey_send = $privatekey;
|
||||
if ($this->isEcCertificate($fullchain)) {
|
||||
$privatekey_send = str_replace('-----BEGIN PRIVATE KEY-----', '-----BEGIN EC PRIVATE KEY-----', $privatekey_send);
|
||||
$privatekey_send = str_replace('-----END PRIVATE KEY-----', '-----END EC PRIVATE KEY-----', $privatekey_send);
|
||||
}
|
||||
$params = [
|
||||
'certificate' => $fullchain,
|
||||
'private_key' => $privatekey,
|
||||
'private_key' => $privatekey_send,
|
||||
];
|
||||
$response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy);
|
||||
$result = json_decode($response['body'], true);
|
||||
@@ -86,8 +92,11 @@ class upyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
if ($i == 0) throw new Exception('未找到可迁移的证书');
|
||||
$this->log('共迁移' . $i . '个证书,关联域名' . $d . '个');
|
||||
if ($i == 0) {
|
||||
$this->log('未找到可迁移的证书');
|
||||
} else {
|
||||
$this->log('共迁移' . $i . '个证书,关联域名' . $d . '个');
|
||||
}
|
||||
}
|
||||
|
||||
private function login()
|
||||
@@ -130,4 +139,22 @@ class upyun implements DeployInterface
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 EC (ECDSA) 证书
|
||||
*/
|
||||
private function isEcCertificate($fullchain)
|
||||
{
|
||||
// 提取第一个证书
|
||||
if (!preg_match('/-----BEGIN CERTIFICATE-----\s*(.+?)\s*-----END CERTIFICATE-----/s', $fullchain, $m)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pubKey = openssl_pkey_get_public($m[0]);
|
||||
if (!$pubKey) return false;
|
||||
|
||||
$details = openssl_pkey_get_details($pubKey);
|
||||
|
||||
return $details && ($details['type'] ?? 0) === OPENSSL_KEYTYPE_EC;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class aliyun implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['ak'];
|
||||
$this->AccessKeySecret = $config['sk'];
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->AccessKeySecret = $config['AccessKeySecret'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
|
||||
284
app/lib/dns/aliyunesa.php
Normal file
284
app/lib/dns/aliyunesa.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use app\lib\client\Aliyun as AliyunClient;
|
||||
use Exception;
|
||||
|
||||
class aliyunesa implements DnsInterface
|
||||
{
|
||||
private $AccessKeyId;
|
||||
private $AccessKeySecret;
|
||||
private $Endpoint = 'esa.cn-hangzhou.aliyuncs.com'; //API接入域名
|
||||
private $Version = '2024-09-10'; //API版本号
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private AliyunClient $client;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->AccessKeySecret = $config['AccessKeySecret'];
|
||||
if (!empty($config['region'])) {
|
||||
$this->Endpoint = 'esa.'.$config['region'].'.aliyuncs.com';
|
||||
}
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $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)
|
||||
{
|
||||
$param = ['Action' => 'ListSites', 'SiteName' => $KeyWord, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'AccessType' => 'NS'];
|
||||
$data = $this->request($param, 'GET', true);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['Sites'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['SiteId'],
|
||||
'Domain' => $row['SiteName'],
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
return ['total' => $data['TotalCount'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析记录列表
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$param = ['Action' => 'ListRecords', 'SiteId' => $this->domainid, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$RecordName = $SubDomain == '@' ? $this->domain : $SubDomain . '.' . $this->domain;
|
||||
$param += ['RecordName' => $RecordName];
|
||||
} elseif (!isNullOrEmpty($KeyWord)) {
|
||||
$RecordName = $KeyWord == '@' ? $this->domain : $KeyWord . '.' . $this->domain;
|
||||
$param += ['RecordName' => $RecordName];
|
||||
}
|
||||
if (!isNullOrEmpty($Type)) {
|
||||
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
|
||||
$param += ['Type' => $Type];
|
||||
}
|
||||
if (!isNullOrEmpty($Line)) {
|
||||
$param += ['Proxied' => $Line == '1' ? 'true' : 'false'];
|
||||
}
|
||||
$data = $this->request($param, 'GET', true);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['Records'] as $row) {
|
||||
$name = substr($row['RecordName'], 0, - (strlen($this->domain) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$value = $row['Data']['Value'];
|
||||
if ($row['RecordType'] == 'CAA') $value = $row['Data']['Flag'] . ' ' . $row['Data']['Tag'] . ' ' . $row['Data']['Value'];
|
||||
else if ($row['RecordType'] == 'SRV') $value = $row['Data']['Priority'] . ' ' . $row['Data']['Weight'] . ' ' . $row['Data']['Port'] . ' ' . $row['Data']['Value'];
|
||||
if ($row['RecordType'] == 'A/AAAA') {
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$row['RecordType'] = 'A';
|
||||
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$row['RecordType'] = 'AAAA';
|
||||
}
|
||||
}
|
||||
$list[] = [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['RecordType'],
|
||||
'Value' => $value,
|
||||
'Line' => $row['Proxied'] ? '1' : '0',
|
||||
'TTL' => $row['Ttl'],
|
||||
'MX' => isset($row['Data']['Priority']) ? $row['Data']['Priority'] : null,
|
||||
'Status' => '1',
|
||||
'Weight' => null,
|
||||
'Remark' => isset($row['Comment']) ? $row['Comment'] : null,
|
||||
'UpdateTime' => isset($row['UpdateTime']) ? date('Y-m-d H:i:s', strtotime($row['UpdateTime'])) : null,
|
||||
];
|
||||
}
|
||||
return ['total' => $data['TotalCount'], '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)
|
||||
{
|
||||
$param = ['Action' => 'GetRecord', 'RecordId' => $RecordId];
|
||||
$data = $this->request($param, 'GET', true);
|
||||
if ($data) {
|
||||
$row = $data['RecordModel'];
|
||||
$name = substr($row['RecordName'], 0, - (strlen($this->domain) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$value = $row['Data']['Value'];
|
||||
if ($row['RecordType'] == 'CAA') $value = $row['Data']['Flag'] . ' ' . $row['Data']['Tag'] . ' ' . $row['Data']['Value'];
|
||||
else if ($row['RecordType'] == 'SRV') $value = $row['Data']['Priority'] . ' ' . $row['Data']['Weight'] . ' ' . $row['Data']['Port'] . ' ' . $row['Data']['Value'];
|
||||
if ($row['RecordType'] == 'A/AAAA') {
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$row['RecordType'] = 'A';
|
||||
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$row['RecordType'] = 'AAAA';
|
||||
}
|
||||
}
|
||||
return [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['RecordType'],
|
||||
'Value' => $value,
|
||||
'Line' => $row['Proxied'] ? '1' : '0',
|
||||
'TTL' => $row['Ttl'],
|
||||
'MX' => isset($row['Data']['Priority']) ? $row['Data']['Priority'] : null,
|
||||
'Status' => '1',
|
||||
'Weight' => null,
|
||||
'Remark' => isset($row['Comment']) ? $row['Comment'] : null,
|
||||
'UpdateTime' => isset($row['UpdateTime']) ? date('Y-m-d H:i:s', strtotime($row['UpdateTime'])) : null,
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//添加解析记录
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
|
||||
{
|
||||
if ($Name == '@') {
|
||||
$Name = $this->domain;
|
||||
} else {
|
||||
$Name = $Name . '.' . $this->domain;
|
||||
}
|
||||
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
|
||||
$data = ['Value' => $Value];
|
||||
if ($Type == 'CAA') {
|
||||
list($flag, $tag, $val) = explode(' ', $Value, 3);
|
||||
$data = ['Flag' => intval($flag), 'Tag' => $tag, 'Value' => $val];
|
||||
} elseif ($Type == 'SRV') {
|
||||
list($priority, $weight, $port, $val) = explode(' ', $Value, 4);
|
||||
$data = ['Priority' => intval($priority), 'Weight' => intval($weight), 'Port' => intval($port), 'Value' => $val];
|
||||
} elseif ($Type == 'MX') {
|
||||
$data['Priority'] = intval($MX);
|
||||
}
|
||||
$param = ['Action' => 'CreateRecord', 'SiteId' => $this->domainid, 'RecordName' => $Name, 'Type' => $Type, 'Proxied' => $Line == '1' ? 'true' : 'false', 'Ttl' => intval($TTL), 'Data' => json_encode($data), 'Comment' => $Remark];
|
||||
if ($Line == '1') $param['BizName'] = 'web';
|
||||
$data = $this->request($param, 'POST', true);
|
||||
if ($data) {
|
||||
return $data['RecordId'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//修改解析记录
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
|
||||
{
|
||||
if ($Name == '@') {
|
||||
$Name = $this->domain;
|
||||
} else {
|
||||
$Name = $Name . '.' . $this->domain;
|
||||
}
|
||||
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
|
||||
$data = ['Value' => $Value];
|
||||
if ($Type == 'CAA') {
|
||||
list($flag, $tag, $val) = explode(' ', $Value, 3);
|
||||
$data = ['Flag' => intval($flag), 'Tag' => $tag, 'Value' => $val];
|
||||
} elseif ($Type == 'SRV') {
|
||||
list($priority, $weight, $port, $val) = explode(' ', $Value, 4);
|
||||
$data = ['Priority' => intval($priority), 'Weight' => intval($weight), 'Port' => intval($port), 'Value' => $val];
|
||||
} elseif ($Type == 'MX') {
|
||||
$data['Priority'] = intval($MX);
|
||||
}
|
||||
$param = ['Action' => 'UpdateRecord', 'RecordId' => $RecordId, 'Type' => $Type, 'Proxied' => $Line == '1' ? 'true' : 'false', 'Ttl' => intval($TTL), 'Data' => json_encode($data), 'Comment' => $Remark];
|
||||
if ($Line == '1') $param['BizName'] = 'web';
|
||||
return $this->request($param, 'POST');
|
||||
}
|
||||
|
||||
//修改解析记录备注
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//删除解析记录
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$param = ['Action' => 'DeleteRecord', 'RecordId' => $RecordId];
|
||||
return $this->request($param, 'POST');
|
||||
}
|
||||
|
||||
//设置解析记录状态
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析记录操作日志
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析线路列表
|
||||
public function getRecordLine()
|
||||
{
|
||||
return ['0' => ['name' => '仅DNS', 'parent' => null], '1' => ['name' => '已代理', 'parent' => null]];
|
||||
}
|
||||
|
||||
//获取域名信息
|
||||
public function getDomainInfo()
|
||||
{
|
||||
$param = ['Action' => 'GetSite', 'SiteId' => $this->domainid];
|
||||
$data = $this->request($param, 'GET', true);
|
||||
if ($data) {
|
||||
return $data;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取域名最低TTL
|
||||
public function getMinTTL()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function request($param, $method, $returnData = false)
|
||||
{
|
||||
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false;
|
||||
try {
|
||||
$result = $this->client->request($param, $method);
|
||||
} catch (Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return $returnData ? $result : true;
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ class baidu implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['ak'];
|
||||
$this->SecretAccessKey = $config['sk'];
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->SecretAccessKey = $config['SecretAccessKey'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
|
||||
276
app/lib/dns/bt.php
Normal file
276
app/lib/dns/bt.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
|
||||
class bt implements DnsInterface
|
||||
{
|
||||
private $accountId;
|
||||
private $accessKey;
|
||||
private $secretKey;
|
||||
private $baseUrl = 'https://dmp.bt.cn';
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $domainType;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->accountId = $config['AccountID'];
|
||||
$this->accessKey = $config['AccessKey'];
|
||||
$this->secretKey = $config['SecretKey'];
|
||||
$this->domain = $config['domain'];
|
||||
if ($config['domainid']) {
|
||||
$a = explode('|', $config['domainid']);
|
||||
$this->domainid = intval($a[0]);
|
||||
$this->domainType = isset($a[1]) ? intval($a[1]) : 1;
|
||||
}
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$param = ['p' => $PageNumber, 'rows' => $PageSize, 'keyword' => $KeyWord];
|
||||
$data = $this->execute('/api/v1/dns/manage/list_domains', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['data'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['local_id'] . '|' . $row['domain_type'],
|
||||
'Domain' => $row['full_domain'],
|
||||
'RecordCount' => $row['record_count'],
|
||||
];
|
||||
}
|
||||
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)
|
||||
{
|
||||
$param = ['domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'p' => $PageNumber, 'rows' => $PageSize];
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$param['searchKey'] = 'record';
|
||||
$param['searchValue'] = $SubDomain;
|
||||
} elseif (!isNullOrEmpty($KeyWord)) {
|
||||
$param['searchKey'] = 'record';
|
||||
$param['searchValue'] = $KeyWord;
|
||||
} elseif (!isNullOrEmpty($Value)) {
|
||||
$param['searchKey'] = 'value';
|
||||
$param['searchValue'] = $Value;
|
||||
} elseif (!isNullOrEmpty($Type)) {
|
||||
$param['searchKey'] = 'type';
|
||||
$param['searchValue'] = $Type;
|
||||
} elseif (!isNullOrEmpty($Status)) {
|
||||
$param['searchKey'] = 'state';
|
||||
$param['searchValue'] = $Status == '0' ? '1' : '0';
|
||||
} elseif (!isNullOrEmpty($Line)) {
|
||||
$param['searchKey'] = 'line';
|
||||
$param['searchValue'] = $Line;
|
||||
}
|
||||
$data = $this->execute('/api/v1/dns/record/list', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['data'] as $row) {
|
||||
$list[] = [
|
||||
'RecordId' => $row['record_id'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $row['record'],
|
||||
'Type' => $row['type'],
|
||||
'Value' => $row['value'],
|
||||
'Line' => $row['viewID'],
|
||||
'TTL' => $row['TTL'],
|
||||
'MX' => $row['MX'],
|
||||
'Status' => $row['state'] == 1 ? '0' : '1',
|
||||
'Weight' => $row['MX'],
|
||||
'Remark' => $row['remark'],
|
||||
'UpdateTime' => date('Y-m-d H:i:s', strtotime($row['created_at'])),
|
||||
];
|
||||
}
|
||||
return ['total' => $data['count'], '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 = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = ['domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'type' => $Type, 'record' => $Name, 'value' => $Value, 'ttl' => intval($TTL), 'view_id' => intval($Line), 'remark' => $Remark];
|
||||
if (!$Weight) $Weight = 1;
|
||||
if ($Type == 'MX') $param['mx'] = intval($MX);
|
||||
else $param['mx'] = intval($Weight);
|
||||
$data = $this->execute('/api/v1/dns/record/create', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//修改解析记录
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = ['record_id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'type' => $Type, 'record' => $Name, 'value' => $Value, 'ttl' => intval($TTL), 'view_id' => intval($Line), 'remark' => $Remark];
|
||||
if (!$Weight) $Weight = 1;
|
||||
if ($Type == 'MX') $param['mx'] = intval($MX);
|
||||
else $param['mx'] = intval($Weight);
|
||||
$data = $this->execute('/api/v1/dns/record/update', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//修改解析记录备注
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//删除解析记录
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$param = ['id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType];
|
||||
$data = $this->execute('/api/v1/dns/record/delete', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//设置解析记录状态
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$param = ['record_id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType];
|
||||
$data = $this->execute($Status == '0' ? '/api/v1/dns/record/pause' : '/api/v1/dns/record/start', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//获取解析记录操作日志
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析线路列表
|
||||
public function getRecordLine()
|
||||
{
|
||||
$param = [];
|
||||
$data = $this->execute('/api/v1/dns/record/get_views', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
$this->processLineList($list, $data, null);
|
||||
return $list;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processLineList(&$list, $line_list, $parent)
|
||||
{
|
||||
foreach ($line_list as $row) {
|
||||
if ($row['free'] && !isset($list[$row['viewId']])) {
|
||||
$list[$row['viewId']] = ['name' => $row['name'], 'parent' => $parent];
|
||||
if ($row['children']) {
|
||||
$this->processLineList($list, $row['children'], $row['viewId']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//获取域名信息
|
||||
public function getDomainInfo()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取域名最低TTL
|
||||
public function getMinTTL()
|
||||
{
|
||||
return 300;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
$param = ['full_domain' => $Domain];
|
||||
$data = $this->execute('/api/v1/dns/manage/add_external_domain', $param);
|
||||
if ($data) {
|
||||
return ['id' => $data['domain_id'], 'name' => $data['full_domain']];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function execute($path, $params)
|
||||
{
|
||||
$method = 'POST';
|
||||
$timestamp = (string)time();
|
||||
$body = json_encode($params);
|
||||
$signingString = implode("\n", [
|
||||
$this->accountId,
|
||||
$timestamp,
|
||||
$method,
|
||||
$path,
|
||||
$body
|
||||
]);
|
||||
$signature = hash_hmac('sha256', $signingString, $this->secretKey);
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Account-ID' => $this->accountId,
|
||||
'X-Access-Key' => $this->accessKey,
|
||||
'X-Timestamp' => $timestamp,
|
||||
'X-Signature' => $signature
|
||||
];
|
||||
$response = $this->curl($method, $path, $headers, $body);
|
||||
if (!$response) {
|
||||
return false;
|
||||
}
|
||||
$arr = json_decode($response, true);
|
||||
if ($arr) {
|
||||
if ($arr['code'] == 0) {
|
||||
return $arr['data'];
|
||||
} else {
|
||||
$this->setError($arr['msg']);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$this->setError('返回数据解析失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function curl($method, $path, $header, $body = null)
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return $response['body'];
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class cloudflare implements DnsInterface
|
||||
{
|
||||
private $Email;
|
||||
private $ApiKey;
|
||||
private $auth;
|
||||
private $baseUrl = 'https://api.cloudflare.com/client/v4';
|
||||
private $error;
|
||||
private $domain;
|
||||
@@ -16,11 +17,33 @@ class cloudflare implements DnsInterface
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->Email = $config['ak'];
|
||||
$this->ApiKey = $config['sk'];
|
||||
$this->Email = $config['email'];
|
||||
$this->ApiKey = $config['apikey'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$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()
|
||||
@@ -64,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)) {
|
||||
@@ -75,9 +99,13 @@ class cloudflare implements DnsInterface
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['result'] as $row) {
|
||||
$name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']);
|
||||
$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'];
|
||||
}
|
||||
$list[] = [
|
||||
'RecordId' => $row['id'],
|
||||
'Domain' => $this->domain,
|
||||
@@ -109,9 +137,13 @@ class cloudflare implements DnsInterface
|
||||
{
|
||||
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
|
||||
if ($data) {
|
||||
$name = $this->domain == $data['result']['name'] ? '@' : str_replace('.' . $this->domain, '', $data['result']['name']);
|
||||
$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'];
|
||||
}
|
||||
return [
|
||||
'RecordId' => $data['result']['id'],
|
||||
'Domain' => $this->domain,
|
||||
@@ -174,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']);
|
||||
}
|
||||
|
||||
@@ -257,14 +295,14 @@ class cloudflare implements DnsInterface
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
|
||||
if (preg_match('/^[0-9a-z]+$/i', $this->ApiKey)) {
|
||||
if ($this->auth == 0) {
|
||||
$headers = [
|
||||
'X-Auth-Email: ' . $this->Email,
|
||||
'X-Auth-Key: ' . $this->ApiKey,
|
||||
'X-Auth-Email' => $this->Email,
|
||||
'X-Auth-Key' => $this->ApiKey,
|
||||
];
|
||||
} else {
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $this->ApiKey,
|
||||
'Authorization' => 'Bearer ' . $this->ApiKey,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -275,39 +313,17 @@ class cloudflare implements DnsInterface
|
||||
}
|
||||
} else {
|
||||
$body = json_encode($params);
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
if ($this->proxy) {
|
||||
curl_set_proxy($ch);
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
if ($method == 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
} elseif ($method == 'PUT') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
} elseif ($method == 'PATCH') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
} elseif ($method == 'DELETE') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
if ($errno) {
|
||||
$this->setError('Curl error: ' . curl_error($ch));
|
||||
}
|
||||
curl_close($ch);
|
||||
if ($errno) return false;
|
||||
|
||||
$arr = json_decode($response, true);
|
||||
$arr = json_decode($response['body'], true);
|
||||
if ($arr) {
|
||||
if ($arr['success']) {
|
||||
return $arr;
|
||||
|
||||
@@ -17,8 +17,8 @@ class dnsla implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->apiid = $config['ak'];
|
||||
$this->apisecret = $config['sk'];
|
||||
$this->apiid = $config['apiid'];
|
||||
$this->apisecret = $config['apisecret'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
@@ -60,19 +60,19 @@ class dnsla implements DnsInterface
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$param = ['domainId' => $this->domainid, 'pageIndex' => $PageNumber, 'pageSize' => $PageSize];
|
||||
if (!isNullOrEmpty(($KeyWord))) {
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$param['host'] = $KeyWord;
|
||||
}
|
||||
if (!isNullOrEmpty(($Type))) {
|
||||
if (!isNullOrEmpty($Type)) {
|
||||
$param['type'] = $this->convertType($Type);
|
||||
}
|
||||
if (!isNullOrEmpty(($Line))) {
|
||||
if (!isNullOrEmpty($Line)) {
|
||||
$param['lineId'] = $Line;
|
||||
}
|
||||
if (!isNullOrEmpty(($SubDomain))) {
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$param['host'] = $SubDomain;
|
||||
}
|
||||
if (!isNullOrEmpty(($Value))) {
|
||||
if (!isNullOrEmpty($Value)) {
|
||||
$param['data'] = $Value;
|
||||
}
|
||||
$data = $this->execute('GET', '/api/recordList', $param);
|
||||
@@ -235,7 +235,10 @@ class dnsla implements DnsInterface
|
||||
private function execute($method, $path, $params = null)
|
||||
{
|
||||
$token = base64_encode($this->apiid.':'.$this->apisecret);
|
||||
$header = ['Authorization: Basic '.$token, 'Content-Type: application/json; charset=utf-8'];
|
||||
$header = [
|
||||
'Authorization' => 'Basic '.$token,
|
||||
'Content-Type' => 'application/json; charset=utf-8'
|
||||
];
|
||||
if ($method == 'POST' || $method == 'PUT') {
|
||||
$response = $this->curl($method, $path, $header, json_encode($params));
|
||||
} else {
|
||||
@@ -264,34 +267,19 @@ class dnsla implements DnsInterface
|
||||
private function curl($method, $path, $header, $body = null)
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
$ch = curl_init($url);
|
||||
if ($this->proxy) {
|
||||
curl_set_proxy($ch);
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
if ($body) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
if ($errno) {
|
||||
$this->setError('Curl error: ' . curl_error($ch));
|
||||
}
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($errno) return false;
|
||||
if ($httpCode == 200) {
|
||||
return $response;
|
||||
} elseif ($httpCode == 401) {
|
||||
if ($response['code'] == 200) {
|
||||
return $response['body'];
|
||||
} elseif ($response['code'] == 401) {
|
||||
$this->setError('认证失败');
|
||||
return false;
|
||||
} else {
|
||||
$this->setError('http code: '.$httpCode);
|
||||
$this->setError('http code: '.$response['code']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ class dnspod implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->SecretId = $config['ak'];
|
||||
$this->SecretKey = $config['sk'];
|
||||
$this->SecretId = $config['SecretId'];
|
||||
$this->SecretKey = $config['SecretKey'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
@@ -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'];
|
||||
|
||||
@@ -18,8 +18,8 @@ class huawei implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['ak'];
|
||||
$this->SecretAccessKey = $config['sk'];
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->SecretAccessKey = $config['SecretAccessKey'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
@@ -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;
|
||||
@@ -77,12 +77,12 @@ class huawei implements DnsInterface
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['recordsets'] as $row) {
|
||||
if ($row['name'] == $row['zone_name']) $row['name'] = '@';
|
||||
if ($row['type'] == 'MX') list($row['mx'], $row['records']) = explode(' ', $row['records'][0]);
|
||||
$name = substr($row['name'], 0, -(strlen($row['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$list[] = [
|
||||
'RecordId' => $row['id'],
|
||||
'Domain' => rtrim($row['zone_name'], '.'),
|
||||
'Name' => str_replace('.'.$row['zone_name'], '', $row['name']),
|
||||
'Name' => $name,
|
||||
'Type' => $row['type'],
|
||||
'Value' => $row['records'],
|
||||
'Line' => $row['line'],
|
||||
@@ -110,12 +110,12 @@ class huawei implements DnsInterface
|
||||
{
|
||||
$data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId);
|
||||
if ($data) {
|
||||
if ($data['name'] == $data['zone_name']) $data['name'] = '@';
|
||||
if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]);
|
||||
$name = substr($data['name'], 0, -(strlen($data['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
return [
|
||||
'RecordId' => $data['id'],
|
||||
'Domain' => rtrim($data['zone_name'], '.'),
|
||||
'Name' => str_replace('.'.$data['zone_name'], '', $data['name']),
|
||||
'Name' => $name,
|
||||
'Type' => $data['type'],
|
||||
'Value' => $data['records'],
|
||||
'Line' => $data['line'],
|
||||
@@ -137,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;
|
||||
@@ -150,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);
|
||||
|
||||
@@ -30,8 +30,8 @@ class huoshan implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['ak'];
|
||||
$this->SecretAccessKey = $config['sk'];
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->SecretAccessKey = $config['SecretAccessKey'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $this->service, $this->version, $this->region, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
|
||||
@@ -23,8 +23,8 @@ class jdcloud implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->AccessKeyId = $config['ak'];
|
||||
$this->AccessKeySecret = $config['sk'];
|
||||
$this->AccessKeyId = $config['AccessKeyId'];
|
||||
$this->AccessKeySecret = $config['AccessKeySecret'];
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new JdcloudClient($this->AccessKeyId, $this->AccessKeySecret, $this->endpoint, $this->service, $this->region, $proxy);
|
||||
$this->domain = $config['domain'];
|
||||
|
||||
@@ -16,7 +16,7 @@ class namesilo implements DnsInterface
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->apikey = $config['sk'];
|
||||
$this->apikey = $config['apikey'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
@@ -63,11 +63,10 @@ class namesilo implements DnsInterface
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['resource_record'] as $row) {
|
||||
$name = $row['host'] == $this->domain ? '@' : str_replace('.'.$this->domain, '', $row['host']);
|
||||
$list[] = [
|
||||
'RecordId' => $row['record_id'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Name' => $row['host'],
|
||||
'Type' => $row['type'],
|
||||
'Value' => $row['value'],
|
||||
'Line' => 'default',
|
||||
|
||||
@@ -17,8 +17,8 @@ class powerdns implements DnsInterface
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->url = 'http://' . $config['ak'] . ':' . $config['sk'] . '/api/v1';
|
||||
$this->apikey = $config['ext'];
|
||||
$this->url = 'http://' . $config['ip'] . ':' . $config['port'] . '/api/v1';
|
||||
$this->apikey = $config['apikey'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
|
||||
387
app/lib/dns/qingcloud.php
Normal file
387
app/lib/dns/qingcloud.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
|
||||
class qingcloud implements DnsInterface
|
||||
{
|
||||
private $access_key_id;
|
||||
private $secret_access_key;
|
||||
private $baseUrl = 'http://api.routewize.com';
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->access_key_id = $config['access_key_id'];
|
||||
$this->secret_access_key = $config['secret_access_key'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
|
||||
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 (!empty($KeyWord)) {
|
||||
$param['zone_name'] = $KeyWord;
|
||||
}
|
||||
$data = $this->execute('GET', '/v1/user/zones', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['zones'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['zone_name'],
|
||||
'Domain' => rtrim($row['zone_name'], '.'),
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total_count'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析记录列表
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
if ($SubDomain) {
|
||||
return $this->getHostRecords($SubDomain);
|
||||
}
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$param = ['zone_name' => $this->domainid, 'offset' => $offset, 'limit' => $PageSize];
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$param['search_word'] = $KeyWord;
|
||||
}
|
||||
$data = $this->execute('GET', '/v1/dns/host/', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['domains'] as $row) {
|
||||
$name = substr($row['domain_name'], 0, -(strlen($row['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$list[] = [
|
||||
'RecordId' => $row['domain_name'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => null,
|
||||
'Value' => null,
|
||||
'Line' => null,
|
||||
'TTL' => null,
|
||||
'MX' => null,
|
||||
'Status' => $row['status'] == 'enabled' ? '0' : '1',
|
||||
'Weight' => null,
|
||||
'Remark' => $row['description'],
|
||||
'UpdateTime' => $row['create_time'],
|
||||
'Count' => $row['count'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total_count'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHostRecords($SubDomain)
|
||||
{
|
||||
$param = ['zone_name' => $this->domainid, 'domain_name' => $SubDomain];
|
||||
$data = $this->execute('GET', '/v1/dns/host_info/', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['records'] as $record) {
|
||||
$name = substr($record['domain_name'], 0, -(strlen($record['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
foreach ($record['record'] as $record_group) {
|
||||
foreach ($record_group['data'] as $row) {
|
||||
$mx = null;
|
||||
if ($record['rd_type'] == 'MX') {
|
||||
$value = explode(' ', $row['value'], 2);
|
||||
$row['value'] = isset($value[1]) ? $value[1] : '';
|
||||
$mx = intval($value[0]);
|
||||
}
|
||||
if ($record['rd_type'] == 'TXT') {
|
||||
$row['value'] = trim($row['value'], '"');
|
||||
}
|
||||
$list[] = [
|
||||
'RecordId' => $record['domain_record_id'].'_'.$row['record_value_id'],
|
||||
'Domain' => $record['domain_name'],
|
||||
'Name' => $name,
|
||||
'Type' => $record['rd_type'],
|
||||
'Mode' => $record['mode'],
|
||||
'Value' => $row['value'],
|
||||
'Line' => $record['view_id'],
|
||||
'TTL' => $record['ttl'],
|
||||
'MX' => $mx,
|
||||
'Status' => $row['status'] == 1 ? '1' : '0',
|
||||
'Weight' => $record_group['weight'] > 0 ? $record_group['weight'] : null,
|
||||
'Remark' => null,
|
||||
'UpdateTime' => $record['create_time'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['total' => $data['total_count'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取子域名解析记录列表
|
||||
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
|
||||
{
|
||||
$SubDomain = $this->getHost($SubDomain);
|
||||
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
|
||||
}
|
||||
|
||||
//获取解析记录详细信息
|
||||
public function getDomainRecordInfo($RecordId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//添加解析记录
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$mode = input('post.mode', '1');
|
||||
|
||||
if ($Type == 'MX') {
|
||||
$Value = intval($MX).' '.$Value;
|
||||
} elseif ($Type == 'TXT' && substr($Value, 0, 1) != '"') {
|
||||
$Value = '"'.$Value.'"';
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach (explode(',', $Value) as $val) {
|
||||
$values[] = ['value' => trim($val), 'status' => 1];
|
||||
}
|
||||
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
|
||||
else $Weight = 0;
|
||||
$record = [['weight' => $Weight, 'values' => $values]];
|
||||
|
||||
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode), 'auto_merge' => 2];
|
||||
$data = $this->execute('POST', '/v1/record/', $param);
|
||||
return is_array($data) ? $data['domain_record_id'] : false;
|
||||
}
|
||||
|
||||
//修改解析记录
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$mode = input('post.mode', '1');
|
||||
|
||||
if ($Type == 'MX') {
|
||||
$Value = intval($MX).' '.$Value;
|
||||
} elseif ($Type == 'TXT' && substr($Value, 0, 1) != '"') {
|
||||
$Value = '"'.$Value.'"';
|
||||
}
|
||||
|
||||
$recordId = explode('_', $RecordId);
|
||||
$domain_record_id = $recordId[0];
|
||||
$record_value_id = $recordId[1];
|
||||
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
|
||||
if (!$data) return false;
|
||||
|
||||
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
|
||||
else $Weight = 0;
|
||||
$record = [];
|
||||
foreach ($data['data']['record'] as $record_group) {
|
||||
$values = [];
|
||||
$flag = false;
|
||||
foreach ($record_group['data'] as $row) {
|
||||
if ($row['record_value_id'] == $record_value_id) {
|
||||
$row['value'] = $Value;
|
||||
$flag = true;
|
||||
}
|
||||
$values[] = ['value' => $row['value'], 'status' => $row['status']];
|
||||
}
|
||||
if (count($values) > 0) {
|
||||
$record[] = ['weight' => $flag ? $Weight : $record_group['weight'], 'values' => $values];
|
||||
}
|
||||
}
|
||||
|
||||
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode)];
|
||||
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//修改解析记录备注
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
$param = ['zone_name' => $this->domainid, 'domain_name' => $RecordId, 'description' => $Remark];
|
||||
$data = $this->execute('POST', '/v1/dns/host/', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//删除解析记录
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
if (strpos($RecordId, $this->domainid) !== false) {
|
||||
$param = ['domain_names' => json_encode([$RecordId]), 'zone_name' => $this->domainid];
|
||||
$data = $this->execute('DELETE', '/v1/domain/', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
$recordId = explode('_', $RecordId);
|
||||
$domain_record_id = $recordId[0];
|
||||
$record_value_id = $recordId[1];
|
||||
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
|
||||
if (!$data) return false;
|
||||
|
||||
$record = [];
|
||||
foreach ($data['data']['record'] as $record_group) {
|
||||
$values = [];
|
||||
foreach ($record_group['data'] as $row) {
|
||||
if ($row['record_value_id'] == $record_value_id) {
|
||||
continue;
|
||||
}
|
||||
$values[] = ['value' => $row['value'], 'status' => $row['status']];
|
||||
}
|
||||
if (count($values) > 0) {
|
||||
$record[] = ['weight' => $record_group['weight'], 'values' => $values];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($record) == 0) {
|
||||
$param = ['ids' => json_encode([$domain_record_id]), 'target' => 'record', 'action' => 'delete'];
|
||||
$data = $this->execute('POST', '/v1/change_record_status/', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
$name = substr($data['data']['domain_name'], 0, -(strlen($data['data']['zone_name']) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$param = ['zone_name' => $this->domainid, 'domain_name' => $name, 'view_id' => $data['data']['view_id'], 'type' => $data['data']['rd_type'], 'ttl' => $data['data']['ttl'], 'record' => json_encode($record), 'mode' => $data['data']['mode']];
|
||||
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//设置解析记录状态
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$recordId = explode('_', $RecordId);
|
||||
$record_value_id = $recordId[1];
|
||||
$param = ['ids' => json_encode([$record_value_id]), 'target' => 'value', 'action' => $Status == '0' ? 'stop' : 'enable'];
|
||||
$data = $this->execute('POST', '/v1/change_record_status/', $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
//获取解析记录操作日志
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取解析线路列表
|
||||
public function getRecordLine()
|
||||
{
|
||||
$param = ['zone_name' => $this->domainid, 'type' => 'GET_FULL'];
|
||||
$data = $this->execute('GET', '/v1/zone/view/', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['zone_views'] as $row) {
|
||||
if ($row['name'] == '*') $row['name'] = '默认';
|
||||
$list[$row['id']] = ['name' => $row['name'], 'parent' => null];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取域名信息
|
||||
public function getDomainInfo()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取域名最低TTL
|
||||
public function getMinTTL()
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
$param = ['zone_name' => $Domain];
|
||||
$data = $this->execute('POST', '/v1/zone/', $param);
|
||||
if ($data) {
|
||||
return ['id' => $data['zone_name'], 'name' => $Domain];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHost($Name)
|
||||
{
|
||||
if ($Name == '@' || $Name == '') $Name = '';
|
||||
else $Name .= '.';
|
||||
$Name .= $this->domain . '.';
|
||||
return $Name;
|
||||
}
|
||||
|
||||
private function execute($method, $path, $params = null)
|
||||
{
|
||||
$date = gmdate('D, d M Y H:i:s \G\M\T');
|
||||
$string_to_sign = $method."\n".$date."\n".$path;
|
||||
if($method == 'GET' && $params){
|
||||
ksort($params);
|
||||
$string_to_sign .= '?'.http_build_query($params);
|
||||
}
|
||||
$signature = base64_encode(hash_hmac('sha256', $string_to_sign, $this->secret_access_key, true));
|
||||
$authorization = 'QC-HMAC-SHA256 '.$this->access_key_id.':'.$signature;
|
||||
$header = [
|
||||
'Authorization' => $authorization,
|
||||
'Date' => $date,
|
||||
];
|
||||
if ($method == 'POST' || $method == 'PUT' || $method == 'DELETE') {
|
||||
$header['Content-Type'] = 'application/json; charset=utf-8';
|
||||
$response = $this->curl($method, $path, $header, json_encode($params));
|
||||
} else {
|
||||
if ($params) {
|
||||
$path .= '?'.http_build_query($params);
|
||||
}
|
||||
$response = $this->curl($method, $path, $header);
|
||||
}
|
||||
$arr = json_decode($response['body'], true);
|
||||
if (isset($arr['code']) && $arr['code'] == 0 || isset($arr['domains']) || $method == 'DELETE' && $response['code'] == 204) {
|
||||
return $arr;
|
||||
} elseif(isset($arr['message'])) {
|
||||
$this->setError($arr['message']);
|
||||
return false;
|
||||
} elseif(isset($arr['msg'])) {
|
||||
$this->setError($arr['msg']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError('返回数据解析失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function curl($method, $path, $header, $body = null)
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
294
app/lib/dns/spaceship.php
Normal file
294
app/lib/dns/spaceship.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
|
||||
/**
|
||||
* @see https://docs.spaceship.dev/
|
||||
*/
|
||||
class spaceship implements DnsInterface
|
||||
{
|
||||
private $apiKey;
|
||||
private $apiSecret;
|
||||
private $baseUrl = 'https://spaceship.dev/api/v1';
|
||||
private $error;
|
||||
private $domain;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->apiKey = $config['apikey'];
|
||||
$this->apiSecret = $config['apisecret'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
|
||||
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 = 100)
|
||||
{
|
||||
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
|
||||
$data = $this->send_reuqest('GET', '/domains', $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['items'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['name'],
|
||||
'Domain' => $row['name'],
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
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)
|
||||
{
|
||||
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$param['take'] = 100;
|
||||
$param['skip'] = 0;
|
||||
}
|
||||
$data = $this->send_reuqest('GET', '/dns/records/' . $this->domain, $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['items'] as $row) {
|
||||
$type = $row['type'];
|
||||
$name = $row['name'];
|
||||
$mx = 0;
|
||||
if ('MX' == $type) {
|
||||
$address = $row['exchange'];
|
||||
$mx = $row['preference'];
|
||||
} else if ('CNAME' == $type) {
|
||||
$address = $row['cname'];
|
||||
} else if ('TXT' == $type) {
|
||||
$address = $row['value'];
|
||||
} else if ('PTR' == $type) {
|
||||
$address = $row['pointer'];
|
||||
} else if ('NS' == $type) {
|
||||
$address = $row['nameserver'];
|
||||
} else if ('CAA' == $type) {
|
||||
$address = $row['flag'] . ' ' . $row['tag'] . ' ' . $row['value'];
|
||||
} else if ('SRV' == $type) {
|
||||
$address = $row['priority'] . ' ' . $row['weight'] . ' ' . $row['port'] . ' ' . $row['target'];
|
||||
} else if ('ALIAS' == $type) {
|
||||
$address = $row['aliasName'];
|
||||
} else {
|
||||
$address = $row['address'];
|
||||
}
|
||||
|
||||
$list[] = [
|
||||
'RecordId' => $row['type'] . '|' . $name . '|' . $address . '|' . $mx,
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $row['name'],
|
||||
'Type' => $row['type'],
|
||||
'Value' => $address,
|
||||
'TTL' => $row['ttl'],
|
||||
'Line' => 'default',
|
||||
'MX' => $mx,
|
||||
'Status' => '1',
|
||||
'Weight' => null,
|
||||
'Remark' => null,
|
||||
'UpdateTime' => null,
|
||||
];
|
||||
}
|
||||
if(!isNullOrEmpty($SubDomain)){
|
||||
$list = array_values(array_filter($list, function($v) use ($SubDomain){
|
||||
return strcasecmp($v['Name'], $SubDomain) === 0;
|
||||
}));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private function convertRecordItem($Name, $Type, $Value, $MX)
|
||||
{
|
||||
$item = [
|
||||
'type' => $Type,
|
||||
'name' => $Name,
|
||||
];
|
||||
if ($Type == 'MX') {
|
||||
$item['exchange'] = $Value;
|
||||
$item['preference'] = (int)$MX;
|
||||
} else if ($Type == 'TXT') {
|
||||
$item['value'] = $Value;
|
||||
} else if ($Type == 'CNAME') {
|
||||
$item['cname'] = $Value;
|
||||
} else if ($Type == 'ALIAS') {
|
||||
$item['aliasName'] = $Value;
|
||||
} else if ($Type == 'NS') {
|
||||
$item['nameserver'] = $Value;
|
||||
} else if ($Type == 'PTR') {
|
||||
$item['pointer'] = $Value;
|
||||
} else if ($Type == 'CAA') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) >= 3) {
|
||||
$item['flag'] = (int)$parts[0];
|
||||
$item['tag'] = $parts[1];
|
||||
$item['value'] = trim($parts[2], '"');
|
||||
}
|
||||
} else if ($Type == 'SRV') {
|
||||
$parts = explode(' ', $Value, 4);
|
||||
if (count($parts) >= 4) {
|
||||
$item['priority'] = (int)$parts[0];
|
||||
$item['weight'] = (int)$parts[1];
|
||||
$item['port'] = (int)$parts[2];
|
||||
$item['target'] = $parts[3];
|
||||
}
|
||||
} else {
|
||||
$item['address'] = $Value;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
//添加解析记录
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$item = $this->convertRecordItem($Name, $Type, $Value, $MX);
|
||||
$item['ttl'] = (int)$TTL;
|
||||
$param = [
|
||||
'force' => false,
|
||||
'items' => [
|
||||
$item
|
||||
]
|
||||
];
|
||||
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
|
||||
return !isset($data);
|
||||
}
|
||||
|
||||
//修改解析记录
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$item = $this->convertRecordItem($Name, $Type, $Value, $MX);
|
||||
$item['ttl'] = (int)$TTL;
|
||||
$param = [
|
||||
'force' => true,
|
||||
'items' => [
|
||||
$item
|
||||
]
|
||||
];
|
||||
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
|
||||
return !isset($data);
|
||||
}
|
||||
|
||||
//修改解析记录备注
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//删除解析记录
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$array = explode("|", $RecordId);
|
||||
$type = $array[0];
|
||||
$name = $array[1];
|
||||
$address = $array[2];
|
||||
$mx = $array[3];
|
||||
$item = $this->convertRecordItem($name, $type, $address, $mx);
|
||||
$param = [$item];
|
||||
$data = $this->send_reuqest('DELETE', '/dns/records/' . $this->domain, $param);
|
||||
return !isset($data);
|
||||
}
|
||||
|
||||
//设置解析记录状态
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
return 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 getDomainInfo()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMinTTL()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_reuqest($method, $path, $params = null)
|
||||
{
|
||||
$url = $this->baseUrl . $path;
|
||||
$headers = [
|
||||
'X-API-Key' => $this->apiKey,
|
||||
'X-API-Secret' => $this->apiSecret,
|
||||
];
|
||||
$body = '';
|
||||
if ($method == 'GET') {
|
||||
if ($params) {
|
||||
$url .= '?' . http_build_query($params);
|
||||
}
|
||||
} else {
|
||||
$body = json_encode($params);
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
$arr = json_decode($response['body'], true);
|
||||
if ($response['code'] == 200 || $response['code'] == 204) {
|
||||
return $arr;
|
||||
} elseif (isset($arr['detail'])) {
|
||||
$this->setError($arr['detail']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError('http code: ' . $response['code']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
254
app/lib/dns/tencenteo.php
Normal file
254
app/lib/dns/tencenteo.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use app\lib\client\TencentCloud;
|
||||
use Exception;
|
||||
|
||||
class tencenteo implements DnsInterface
|
||||
{
|
||||
private $SecretId;
|
||||
private $SecretKey;
|
||||
private $endpoint = "teo.tencentcloudapi.com";
|
||||
private $service = "teo";
|
||||
private $version = "2022-09-01";
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $domainInfo;
|
||||
private TencentCloud $client;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->SecretId = $config['SecretId'];
|
||||
$this->SecretKey = $config['SecretKey'];
|
||||
if (isset($config['site_type']) && $config['site_type'] == 'intl') {
|
||||
$this->endpoint = "teo.intl.tencentcloudapi.com";
|
||||
}
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $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)
|
||||
{
|
||||
$action = 'DescribeZones';
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$filters = [['Name' => 'zone-type', 'Values' => ['full']]];
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$filters[] = ['Name' => 'zone-name', 'Values' => [$KeyWord]];
|
||||
}
|
||||
$param = ['Offset' => $offset, 'Limit' => $PageSize, 'Filters' => $filters];
|
||||
$data = $this->send_request($action, $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['Zones'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['ZoneId'],
|
||||
'Domain' => $row['ZoneName'],
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
return ['total' => $data['TotalCount'], '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;
|
||||
$action = 'DescribeDnsRecords';
|
||||
$filters = [];
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$name = $SubDomain == '@' ? $this->domain : $SubDomain . '.' . $this->domain;
|
||||
$filters[] = ['Name' => 'name', 'Values' => [$name]];
|
||||
} elseif (!isNullOrEmpty($KeyWord)) {
|
||||
$name = $KeyWord == '@' ? $this->domain : $KeyWord . '.' . $this->domain;
|
||||
$filters[] = ['Name' => 'name', 'Values' => [$name]];
|
||||
}
|
||||
if (!isNullOrEmpty($Value)) {
|
||||
$filters[] = ['Name' => 'content', 'Values' => [$Value], 'Fuzzy' => true];
|
||||
}
|
||||
if (!isNullOrEmpty($Type)) {
|
||||
$filters[] = ['Name' => 'type', 'Values' => [$Type]];
|
||||
}
|
||||
$param = ['ZoneId' => $this->domainid, 'Offset' => $offset, 'Limit' => $PageSize, 'Filters' => $filters];
|
||||
$data = $this->send_request($action, $param);
|
||||
if ($data) {
|
||||
$list = [];
|
||||
foreach ($data['DnsRecords'] as $row) {
|
||||
$name = substr($row['Name'], 0, - (strlen($this->domain) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
$list[] = [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['Type'],
|
||||
'Value' => $row['Content'],
|
||||
'Line' => $row['Location'],
|
||||
'TTL' => $row['TTL'],
|
||||
'MX' => $row['Priority'],
|
||||
'Status' => $row['Status'] == 'enable' ? '1' : '0',
|
||||
'Weight' => $row['Weight'] == -1 ? null : $row['Weight'],
|
||||
'Remark' => null,
|
||||
'UpdateTime' => $row['ModifiedOn'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['TotalCount'], '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)
|
||||
{
|
||||
$action = 'DescribeDnsRecords';
|
||||
$param = ['ZoneId' => $this->domainid, 'Filters' => [['Name' => 'id', 'Values' => [$RecordId]]]];
|
||||
$data = $this->send_request($action, $param);
|
||||
if ($data) {
|
||||
$row = $data['DnsRecords'][0];
|
||||
$name = substr($row['Name'], 0, - (strlen($this->domain) + 1));
|
||||
if ($name == '') $name = '@';
|
||||
return [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['Type'],
|
||||
'Value' => $row['Content'],
|
||||
'Line' => $row['Location'],
|
||||
'TTL' => $row['TTL'],
|
||||
'MX' => $row['Priority'],
|
||||
'Status' => $row['Status'] == 'enable' ? '1' : '0',
|
||||
'Weight' => $row['Weight'] == -1 ? null : $row['Weight'],
|
||||
'Remark' => null,
|
||||
'UpdateTime' => $row['ModifiedOn'],
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//添加解析记录
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$action = 'CreateDnsRecord';
|
||||
if ($Name == '@') {
|
||||
$Name = $this->domain;
|
||||
} else {
|
||||
$Name = $Name . '.' . $this->domain;
|
||||
}
|
||||
$param = ['ZoneId' => $this->domainid, 'Name' => $Name, 'Type' => $Type, 'Content' => $Value, 'Location' => $Line, 'TTL' => intval($TTL), 'Weight' => empty($Weight) ? -1 : intval($Weight)];
|
||||
if ($Type == 'MX') $param['Priority'] = intval($MX);
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data) ? $data['RecordId'] : false;
|
||||
}
|
||||
|
||||
//修改解析记录
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$action = 'ModifyDnsRecord';
|
||||
if ($Name == '@') {
|
||||
$Name = $this->domain;
|
||||
} else {
|
||||
$Name = $Name . '.' . $this->domain;
|
||||
}
|
||||
$param = ['ZoneId' => $this->domainid, 'DnsRecordId' => $RecordId, 'Name' => $Name, 'Type' => $Type, 'Content' => $Value, 'Location' => $Line, 'TTL' => intval($TTL), 'Weight' => empty($Weight) ? -1 : intval($Weight)];
|
||||
if ($Type == 'MX') $param['Priority'] = intval($MX);
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data);
|
||||
}
|
||||
|
||||
//修改解析记录备注
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//删除解析记录
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$action = 'DeleteDnsRecords';
|
||||
$param = ['ZoneId' => $this->domainid, 'RecordIds' => [$RecordId]];
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data);
|
||||
}
|
||||
|
||||
//设置解析记录状态
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$action = 'ModifyDnsRecordsStatus';
|
||||
$param = ['ZoneId' => $this->domainid];
|
||||
if ($Status == '1') $param['RecordsToEnable'] = [$RecordId];
|
||||
else $param['RecordsToDisable'] = [$RecordId];
|
||||
$data = $this->send_request($action, $param);
|
||||
return is_array($data);
|
||||
}
|
||||
|
||||
//获取解析记录操作日志
|
||||
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 getDomainInfo()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//获取域名最低TTL
|
||||
public function getMinTTL()
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_request($action, $param)
|
||||
{
|
||||
try{
|
||||
return $this->client->request($action, $param);
|
||||
}catch(Exception $e){
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ class west implements DnsInterface
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->username = $config['ak'];
|
||||
$this->api_password = $config['sk'];
|
||||
$this->username = $config['username'];
|
||||
$this->api_password = $config['api_password'];
|
||||
$this->domain = $config['domain'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class AuthUser
|
||||
$islogin = false;
|
||||
$cookie = cookie('user_token');
|
||||
$user = null;
|
||||
if ($cookie && config_get('sys_key')) {
|
||||
if ($cookie && config_get('sys_key') && strpos($request->url(), '/install') === false) {
|
||||
$token = authcode($cookie, 'DECODE', config_get('sys_key'));
|
||||
if ($token) {
|
||||
list($type, $uid, $sid, $expiretime) = explode("\t", $token);
|
||||
|
||||
@@ -30,6 +30,16 @@ class LoadConfig
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
if (!checkTableExists('config') && !checkTableExists('user')) {
|
||||
if (strpos($request->url(), '/install') === false) {
|
||||
return redirect((string)url('/install'))->header([
|
||||
'Cache-Control' => 'no-store, no-cache, must-revalidate',
|
||||
'Pragma' => 'no-cache',
|
||||
]);
|
||||
} else {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');
|
||||
|
||||
@@ -70,8 +70,15 @@ class CertDeployService
|
||||
$this->saveResult(-1, $e->getMessage(), date('Y-m-d H:i:s', time() + (array_key_exists($this->task['retry'], self::$retry_interval) ? self::$retry_interval[$this->task['retry']] : 3600)));
|
||||
throw $e;
|
||||
} finally {
|
||||
if($this->info){
|
||||
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]);
|
||||
if ($this->info && is_array($this->info)) {
|
||||
if (isset($this->info['config']) && is_array($this->info['config'])) {
|
||||
$config = array_merge(json_decode($this->task['config'], true), $this->info['config']);
|
||||
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['config' => json_encode($config)]);
|
||||
unset($this->info['config']);
|
||||
}
|
||||
if (!empty($this->info)) {
|
||||
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,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,14 +41,14 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
private function refreshDomainList()
|
||||
{
|
||||
$domainList = Db::name('domain')->field('id,name')->where('expiretime', null)->where('checkstatus', 0)->select();
|
||||
$domainList = Db::name('domain')->field('id,name')->where('checkstatus', 0)->select();
|
||||
$count = 0;
|
||||
foreach ($domainList as $domain) {
|
||||
$res = $this->updateDomainDate($domain['id'], $domain['name']);
|
||||
|
||||
@@ -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;
|
||||
@@ -132,7 +189,7 @@ class OptimizeService
|
||||
continue;
|
||||
}
|
||||
|
||||
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
|
||||
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type')->find();
|
||||
if (!$drow) {
|
||||
throw new Exception('域名不存在(ID:'.$row['did'].')');
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ScheduleService
|
||||
|
||||
public function execute_one($row)
|
||||
{
|
||||
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
|
||||
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.config')->find();
|
||||
if (!$drow) throw new Exception('域名不存在');
|
||||
|
||||
Db::name('sctask')->where('id', $row['id'])->update(['updatetime' => time()]);
|
||||
|
||||
@@ -71,7 +71,7 @@ class TaskRunner
|
||||
}
|
||||
|
||||
if ($action > 0) {
|
||||
$drow = $this->db()->name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
|
||||
$drow = $this->db()->name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.config')->find();
|
||||
if (!$drow) {
|
||||
echo '域名不存在(ID:'.$row['did'].')'."\n";
|
||||
$this->closeDb();
|
||||
|
||||
@@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `dnsmgr_config` VALUES ('version', '1040');
|
||||
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');
|
||||
@@ -15,10 +15,8 @@ DROP TABLE IF EXISTS `dnsmgr_account`;
|
||||
CREATE TABLE `dnsmgr_account` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`type` varchar(20) NOT NULL,
|
||||
`ak` varchar(256) DEFAULT NULL,
|
||||
`sk` varchar(256) DEFAULT NULL,
|
||||
`ext` varchar(256) DEFAULT NULL,
|
||||
`proxy` tinyint(1) NOT NULL DEFAULT '0',
|
||||
`name` varchar(255) NOT NULL,
|
||||
`config` text DEFAULT NULL,
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
@@ -28,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,
|
||||
@@ -42,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`;
|
||||
@@ -253,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;
|
||||
@@ -185,4 +185,31 @@ CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` (
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE `dnsmgr_account`
|
||||
ADD COLUMN `config` text DEFAULT 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;
|
||||
@@ -257,6 +268,7 @@ class MsgNotice
|
||||
public static function send_webhook($title, $content)
|
||||
{
|
||||
$url = config_get('webhook_url');
|
||||
$atuser = config_get('webhook_user');
|
||||
if (!$url || !parse_url($url)) return false;
|
||||
if (strpos($url, 'oapi.dingtalk.com')) {
|
||||
$content = '### '.$title." \n ".str_replace("\n", " \n ", $content);
|
||||
@@ -267,6 +279,14 @@ class MsgNotice
|
||||
'text' => $content,
|
||||
],
|
||||
];
|
||||
if (!empty($atuser)) {
|
||||
if ($atuser == 'all') {
|
||||
$post['at'] = ['isAtAll' => true];
|
||||
} else {
|
||||
$atusers = explode(',', $atuser);
|
||||
$post['at'] = ['atMobiles' => $atusers, 'isAtAll' => false];
|
||||
}
|
||||
}
|
||||
} elseif (strpos($url, 'qyapi.weixin.qq.com')) {
|
||||
$content = '## '.$title."\n".$content;
|
||||
$post = [
|
||||
@@ -276,17 +296,70 @@ class MsgNotice
|
||||
],
|
||||
];
|
||||
} elseif (strpos($url, 'open.feishu.cn') || strpos($url, 'open.larksuite.com')) {
|
||||
$content = str_replace(['\*', '**'], ['*', ''], strip_tags($content));
|
||||
$content = str_replace('<font color="warning">', '<font color="red">', $content);
|
||||
if (!empty($atuser)) {
|
||||
if ($atuser == 'all') {
|
||||
$content .= "\n".'<at id=all></at> ';
|
||||
} else {
|
||||
$atusers = explode(',', $atuser);
|
||||
$content .= "\n";
|
||||
foreach ($atusers as $u) {
|
||||
$content .= '<at user_id="'.$u.'"></at> ';
|
||||
}
|
||||
}
|
||||
}
|
||||
$template = 'blue';
|
||||
if(strpos($title, '发生告警') !== false || strpos($title, '失败') !== false) $template = 'red';
|
||||
else if(strpos($title, '恢复正常') !== false) $template = 'green';
|
||||
else if(strpos($title, '到期提醒') !== false) $template = 'yellow';
|
||||
$post = [
|
||||
'msg_type' => 'text',
|
||||
'content' => [
|
||||
'text' => $content,
|
||||
'msg_type' => 'interactive',
|
||||
'card' => [
|
||||
'schema' => '2.0',
|
||||
'config' => [
|
||||
'update_multi' => true,
|
||||
'style' => [
|
||||
'text_size' => [
|
||||
'normal_v2' => [
|
||||
'default' => 'normal',
|
||||
'pc' => 'normal',
|
||||
'mobile' => 'heading',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'header' => [
|
||||
'title' => [
|
||||
'tag' => 'plain_text',
|
||||
'content' => $title,
|
||||
],
|
||||
'subtitle' => [
|
||||
'tag' => 'plain_text',
|
||||
'content' => '',
|
||||
],
|
||||
'template' => $template,
|
||||
'padding' => '12px 12px 12px 12px',
|
||||
],
|
||||
'body' => [
|
||||
'direction' => 'vertical',
|
||||
'padding' => '12px 12px 12px 12px',
|
||||
'elements' => [
|
||||
[
|
||||
'tag' => 'markdown',
|
||||
'content' => $content,
|
||||
'text_align' => 'left',
|
||||
'text_size' => 'normal_v2',
|
||||
'margin' => '0px 0px 0px 0px',
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
} else {
|
||||
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;
|
||||
@@ -295,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();
|
||||
|
||||
@@ -124,7 +124,7 @@ $(document).ready(function(){
|
||||
field: 'end_day',
|
||||
title: '到期时间',
|
||||
formatter: function(value, row, index) {
|
||||
if(value){
|
||||
if(value != null){
|
||||
if(value > 7){
|
||||
return '<span title="'+row.expiretime+'" data-toggle="tooltip" data-placement="right" style="color:green">剩余' + value + '天<span>';
|
||||
}else if(value > 0){
|
||||
|
||||
@@ -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')}">
|
||||
<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">
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
{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/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js?v=2"></script>
|
||||
<script>
|
||||
var action = '{$action}';
|
||||
var info = {$info|json_encode|raw};
|
||||
|
||||
@@ -1,70 +1,6 @@
|
||||
{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="action"/>
|
||||
<input type="hidden" name="id"/>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">所属平台</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="type" class="form-control">
|
||||
{foreach $dnsconfig as $k=>$v}
|
||||
<option value="{$k}">{$v['name']}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right" id="ak_name">AccessKey</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="ak" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right" id="sk_name">SecretKey</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="sk" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="ext_name_div" style="display:none;">
|
||||
<label class="col-sm-3 control-label no-padding-right" id="ext_name">扩展字段</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="ext" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right" id="ext_name">使用代理</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="radio-inline"><input type="radio" name="proxy" value="0"> 否
|
||||
</label><label class="radio-inline"><input type="radio" name="proxy" value="1"> 是
|
||||
</label>
|
||||
</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>
|
||||
</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 panel-intro">
|
||||
@@ -73,11 +9,11 @@
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<div class="form-group">
|
||||
<label>搜索</label>
|
||||
<input type="text" class="form-control" name="kw" placeholder="AccessKey或备注">
|
||||
<input type="text" class="form-control" name="kw" placeholder="账户名称或备注">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新域名账户列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
|
||||
<a href="/account/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
|
||||
</form>
|
||||
|
||||
<table id="listTable">
|
||||
@@ -93,7 +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 dnsconfig = {$dnsconfig|json_encode|raw};
|
||||
var userLevel = "{$user['level']|default=''}";
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
const defaultPageSize = 15;
|
||||
@@ -114,12 +50,12 @@ $(document).ready(function(){
|
||||
field: 'typename',
|
||||
title: '所属平台',
|
||||
formatter: function(value, row, index) {
|
||||
return '<img src="/static/images/'+row.type+'.ico" class="type-logo"></img>'+value;
|
||||
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+value;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'ak',
|
||||
title: 'AccessKey'
|
||||
field: 'name',
|
||||
title: '账户名称'
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
@@ -133,101 +69,23 @@ $(document).ready(function(){
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row, index) {
|
||||
var html = '<a href="javascript:editframe('+row.id+')" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';
|
||||
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;
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
$("select[name=type]").change(function(){
|
||||
var type = $(this).val();
|
||||
if(dnsconfig[type] == undefined) return;
|
||||
$("#ak_name").html(dnsconfig[type].config.ak);
|
||||
$("#sk_name").html(dnsconfig[type].config.sk);
|
||||
if(dnsconfig[type].config.ext == undefined){
|
||||
$("#ext_name_div").hide();
|
||||
}else{
|
||||
$("#ext_name_div").show();
|
||||
$("#ext_name").html(dnsconfig[type].config.ext);
|
||||
}
|
||||
});
|
||||
})
|
||||
function addframe(){
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("添加域名账户");
|
||||
$("#form-store input[name=action]").val("add");
|
||||
$("#form-store input[name=id]").val('');
|
||||
$("#form-store input[name=ak]").val('');
|
||||
$("#form-store input[name=sk]").val('');
|
||||
$("#form-store input[name=ext]").val('');
|
||||
$("#form-store input[name=proxy]").eq(0).prop('checked',true);
|
||||
$("#form-store input[name=remark]").val('');
|
||||
$("select[name=type]").change();
|
||||
}
|
||||
function editframe(id){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/account/op/act/get',
|
||||
data : {id: id},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("修改域名账户");
|
||||
$("#form-store input[name=action]").val("edit");
|
||||
$("#form-store input[name=id]").val(data.data.id);
|
||||
$("#form-store select[name=type]").val(data.data.type);
|
||||
$("#form-store input[name=ak]").val(data.data.ak);
|
||||
$("#form-store input[name=sk]").val(data.data.sk);
|
||||
$("#form-store input[name=ext]").val(data.data.ext);
|
||||
$("#form-store input[name=proxy]").eq(data.data.proxy).prop('checked',true);
|
||||
$("#form-store input[name=remark]").val(data.data.remark);
|
||||
$("select[name=type]").change();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function save(){
|
||||
if($("#form-store input[name=username]").val()==''){
|
||||
layer.alert('请确保各项不能为空!');return false;
|
||||
}
|
||||
var act = $("#form-store input[name=action]").val();
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/account/op/act/'+act,
|
||||
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) {
|
||||
var confirmobj = layer.confirm('确定要删除此域名账户吗?', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要删除此域名账户吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/account/op/act/del',
|
||||
url : '/account/del',
|
||||
data : {id: id},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
@@ -240,8 +98,6 @@ function delItem(id) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
243
app/view/domain/account_add.html
Normal file
243
app/view/domain/account_add.html
Normal file
@@ -0,0 +1,243 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}域名账户{/block}
|
||||
{block name="main"}
|
||||
<style>
|
||||
.tips{color: #f6a838; padding-left: 5px;}
|
||||
.input-note{color: green;}
|
||||
.control-label[is-required]:before {
|
||||
content: "*";
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
<div class="row" id="app">
|
||||
<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="javascript:window.history.back()" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}域名账户</h3></div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="accountform">
|
||||
<div class="form-group">
|
||||
<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" :data-icon="item.icon">{{item.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item,name) in inputs" v-show="isShow(item.show)">
|
||||
<div class="form-group" v-if="item.type=='input'">
|
||||
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled" :data-bv-id="item.validator=='id'" :data-bv-phone="item.validator=='phone'" :data-bv-numeric="item.validator=='numeric'" :data-bv-digits="item.validator=='digits'" :data-bv-integer="item.validator=='integer'" :data-bv-email="item.validator=='email'" :data-bv-uri="item.validator=='uri'" :min="item.min" :max="item.max"><span v-if="item.note" class="input-note" v-html="item.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="item.type=='textarea'">
|
||||
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
|
||||
<div class="col-sm-6">
|
||||
<textarea class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled"></textarea><span v-if="item.note" class="input-note" v-html="item.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="item.type=='select'">
|
||||
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" :name="name" v-model="config[name]" :required="item.required" :disabled="item.disabled" :placeholder="item.placeholder">
|
||||
<option v-for="option in item.options" :value="option.value">{{option.label}}</option>
|
||||
</select><span v-if="item.note" class="input-note" v-html="item.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="item.type=='radio'">
|
||||
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
|
||||
<div class="col-sm-6">
|
||||
<label class="radio-inline" v-for="(optionname, optionvalue) in item.options">
|
||||
<input type="radio" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
|
||||
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="item.type=='checkbox'">
|
||||
<div class="col-sm-offset-3 col-sm-7">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" :name="name" v-model="config[name]" :disabled="item.disabled"> {{item.name}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="item.type=='checkboxes'">
|
||||
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
|
||||
<div class="col-sm-6">
|
||||
<label class="checkbox-inline" v-for="(optionname, optionvalue) in item.options">
|
||||
<input type="checkbox" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
|
||||
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-show="note">
|
||||
<div class="col-sm-offset-3 col-sm-6">
|
||||
<div class="alert alert-dismissible alert-info">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<strong>提示:</strong><span v-html="note"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{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};
|
||||
var typeList = {$typeList|json_encode|raw};
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
action: '{$action}',
|
||||
set: {
|
||||
id: '',
|
||||
type: '',
|
||||
name: '',
|
||||
config : '',
|
||||
remark: '',
|
||||
},
|
||||
inputs: {},
|
||||
config: {},
|
||||
typeList: typeList,
|
||||
note: '',
|
||||
},
|
||||
watch: {
|
||||
'set.type': function(val){
|
||||
if(this.action == 'add' && val && typeList[val]){
|
||||
this.inputs = typeList[val].config;
|
||||
this.note = typeList[val].note;
|
||||
this.config = {};
|
||||
$.each(this.inputs, (name, item) => {
|
||||
if(typeof item.value == 'undefined'){
|
||||
if(item.type == 'checkbox'){
|
||||
item.value = false;
|
||||
}else if(item.type == 'checkboxes'){
|
||||
item.value = [];
|
||||
}else{
|
||||
item.value = null;
|
||||
}
|
||||
}
|
||||
this.$set(this.config, name, item.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if(this.action == 'edit'){
|
||||
Object.keys(info).forEach((key) => {
|
||||
this.set[key] = info[key]
|
||||
})
|
||||
var config = JSON.parse(info.config);
|
||||
this.inputs = typeList[this.set.type].config;
|
||||
this.note = typeList[this.set.type].note;
|
||||
$.each(this.inputs, (name, item) => {
|
||||
if(typeof config[name] != 'undefined'){
|
||||
item.value = config[name];
|
||||
}
|
||||
if(typeof item.value == 'undefined'){
|
||||
if(item.type == 'checkbox'){
|
||||
item.value = false;
|
||||
}else if(item.type == 'checkboxes'){
|
||||
item.value = [];
|
||||
}else{
|
||||
item.value = null;
|
||||
}
|
||||
}
|
||||
this.$set(this.config, name, item.value)
|
||||
})
|
||||
}else{
|
||||
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: {
|
||||
submit(){
|
||||
var that=this;
|
||||
Object.keys(this.config).forEach((key) => {
|
||||
if(this.config[key] && typeof this.config[key] == 'string'){
|
||||
this.config[key] = this.trim(this.config[key]);
|
||||
}
|
||||
})
|
||||
this.set.config = JSON.stringify(this.config);
|
||||
this.set.name = this.config[Object.keys(this.config)[0]];
|
||||
let loading = layer.msg('正在进行账户有效性检查', {icon: 16,shade: 0.1,time: 0});
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "",
|
||||
data: this.set,
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
layer.close(loading);
|
||||
if(data.code == 0){
|
||||
layer.alert(data.msg, {icon: 1}, function(){
|
||||
window.location.href = '/account';
|
||||
});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(data){
|
||||
layer.close(loading);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
},
|
||||
isShow(show){
|
||||
if(typeof show == 'boolean' && show){
|
||||
return show;
|
||||
}else if(typeof show == 'string' && show){
|
||||
var that=this;
|
||||
Object.keys(this.config).forEach((key) => {
|
||||
show = show.replace(new RegExp(key, 'g'), 'that.config["'+key+'"]')
|
||||
})
|
||||
return eval(show);
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
},
|
||||
trim(str){
|
||||
return str.replace(/(^\s*)|(\s*$)/g, "");
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{/block}
|
||||
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}
|
||||
@@ -48,6 +48,12 @@
|
||||
<input type="text" class="form-control" name="ttl" value="600" placeholder="指解析结果在DNS服务器中的缓存时间" required min="{$minTTL}">
|
||||
</div>
|
||||
</div>
|
||||
{if $dnsconfig.remark == 2}<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="remark" placeholder="">
|
||||
</div>
|
||||
</div>{/if}
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" onclick="save()">添加</button></div>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
|
||||
<tbody>
|
||||
<tr v-for="item in domainList">
|
||||
<td>{{item.id}}</td>
|
||||
<td><img :src="'/static/images/'+item.type+'.ico'" class="type-logo"></img><a :href="'/record/'+item.id" target="_blank">{{item.name}}</a></td>
|
||||
<td><img :src="'/static/images/'+item.icon+''" class="type-logo"></img><a :href="'/record/'+item.id" target="_blank">{{item.name}}</a></td>
|
||||
<td v-html="item.result"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
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>
|
||||
@@ -127,6 +138,8 @@
|
||||
<div class="panel-body">
|
||||
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<input type="hidden" name="id" value="">
|
||||
<input type="hidden" name="aid" value="">
|
||||
<div class="form-group">
|
||||
<label>搜索</label>
|
||||
<input type="text" class="form-control" name="kw" placeholder="域名或备注">
|
||||
@@ -136,15 +149,21 @@
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<select name="order" class="form-control"><option value="">默认排序</option><option value="1">注册时间↑</option><option value="2">注册时间↓</option><option value="3">到期时间↑</option><option value="4">到期时间↓</option></select>
|
||||
</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('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>
|
||||
@@ -167,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;
|
||||
@@ -193,7 +212,7 @@ $(document).ready(function(){
|
||||
field: 'typename',
|
||||
title: '平台账户',
|
||||
formatter: function(value, row, index) {
|
||||
return '<img src="/static/images/'+row.type+'.ico" class="type-logo"></img>'+(row.aremark?row.aremark:value+'('+row.aid+')');
|
||||
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+(row.aremark?row.aremark:value+'('+row.aid+')');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -279,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: '备注'
|
||||
@@ -395,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({
|
||||
@@ -423,9 +450,7 @@ function saveEdit(){
|
||||
});
|
||||
}
|
||||
function delItem(id) {
|
||||
var confirmobj = layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
@@ -442,8 +467,6 @@ function delItem(id) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function getDomainList(){
|
||||
@@ -503,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';
|
||||
@@ -512,9 +538,7 @@ function operation(action){
|
||||
window.location.href = '/record/batchedit';
|
||||
return;
|
||||
}else if(action == 'delete'){
|
||||
var confirmobj = layer.confirm('确定要删除所选域名吗?', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要删除所选域名吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
@@ -532,8 +556,26 @@ function operation(action){
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}else if(action == 'updateexpire'){
|
||||
layer.confirm('提交后将异步刷新所选域名的到期时间', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/op/act/updateexpire',
|
||||
data : {ids: ids},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.alert(data.msg, {icon: 1});
|
||||
searchRefresh();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}else{
|
||||
var is_notice = action == 'opennotice' ? 1 : 0;
|
||||
@@ -590,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"/>
|
||||
|
||||
556
app/view/domain/qingcloud.html
Normal file
556
app/view/domain/qingcloud.html
Normal file
@@ -0,0 +1,556 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}解析管理 - {$domainName}{/block}
|
||||
{block name="main"}
|
||||
<style>
|
||||
td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;}
|
||||
.dns-parent-row { cursor: pointer; }
|
||||
.dns-child-row td { background: #fafafa; }
|
||||
.dns-child-empty td { background:#fafafa; }
|
||||
.glyphicon-spin { animation: spin 1s infinite linear; }
|
||||
@keyframes spin { from {transform:rotate(0deg);} to {transform:rotate(360deg);} }
|
||||
.form-group .radio-inline {position: unset;}
|
||||
.tips {color: #f6a838;padding-left: 5px;}
|
||||
.text-remark {margin-left: 10px;color: #329a29;font-size: 12px;}
|
||||
</style>
|
||||
<div class="row" id="app">
|
||||
<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">{{form.action=='add'?'添加解析':'修改解析'}}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<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="name" placeholder="填写域名前缀,支持多级" v-model="form.name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">记录类型</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="type" class="form-control" v-model="form.type">
|
||||
<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="TXT">TXT</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">线路类型</label>
|
||||
<div class="col-sm-9" id="line_list">
|
||||
<select name="line" class="form-control" v-model="form.line">
|
||||
<option v-for="line in recordLine" :value="line.id">{{line.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-show="form.type=='A' || form.type=='CNAME'">
|
||||
<label class="col-sm-3 control-label">模式</label>
|
||||
<div class="col-sm-9" id="line_list">
|
||||
<label class="radio-inline"><input type="radio" name="mode" value="1" v-model="form.mode"> 普通<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将按您填写的顺序返回解析结果,查询性能更好。" class="tips"><i class="fa fa-question-circle"></i></span></label>
|
||||
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="2" v-model="form.mode"> 轮询<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询的解析结果排序都将会较上一次发生变化,业务负载更均衡。" class="tips"><i class="fa fa-question-circle"></i></span></label>
|
||||
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="4" v-model="form.mode"> 智能<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="根据访问来源的运营商及地理位置将解析结果按匹配度排序并最多返回前 5 个,可减少您对精细化线路配置的烦恼。" class="tips"><i class="fa fa-question-circle"></i></span></label>
|
||||
<label class="radio-inline"><input type="radio" name="mode" value="3" v-model="form.mode"> 权重<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将根据每组解析结果的权值按比例返回,使得业务负载可以随心所欲。" class="tips"><i class="fa fa-question-circle"></i></span></label>
|
||||
</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="value" :placeholder="'输入记录值' + (form.type=='A'&&form.action=='add'?'(多个IP用,间隔)':'')" v-model="form.value" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-show="form.type=='MX'">
|
||||
<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" v-model="form.mx">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">TTL</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="ttl" v-model="form.ttl" placeholder="指解析结果在DNS服务器中的缓存时间" required min="{$minTTL}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-show="(form.type=='A' || form.type=='CNAME') && form.mode=='3'">
|
||||
<label class="col-sm-3 control-label no-padding-right">权重</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="weight" v-model="form.weight" placeholder="权重值(1-99)" min="1" max="99">
|
||||
</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" @click="save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 center-block" style="float: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{if request()->user['type'] eq 'user'}<a href="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{/if}{$domainName}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<form class="form-inline" id="searchToolbar" @submit.prevent>
|
||||
<div class="form-group">
|
||||
<label>搜索</label>
|
||||
<input type="text" class="form-control" name="keyword" placeholder="输入主机记录" v-model="keyword" @keyup.enter="loadParents">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" @click="loadParents"><i class="fa fa-search"></i> 搜索</button>
|
||||
<button type="button" class="btn btn-default" title="刷新解析记录列表" @click="keyword=null;loadParents()"><i class="fa fa-refresh"></i> 刷新</button>
|
||||
<button type="button" class="btn btn-success" @click="addRecord"><i class="fa fa-plus"></i> 添加记录</button>
|
||||
<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></ul>
|
||||
</div>
|
||||
<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="/log?domain={$domainName}">本站日志</a></li></ul>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table-responsive" style="margin-top:15px;">
|
||||
<table class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>主机记录</th>
|
||||
<th>线路类型</th>
|
||||
<th>记录类型</th>
|
||||
<th>模式</th>
|
||||
<th style="min-width:150px">记录值</th>
|
||||
<th>TTL</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<template v-if="loading">
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="!loading && parents.length === 0">
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted text-center">暂无记录</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="!loading" v-for="p in parents">
|
||||
|
||||
<tr :key="'p-' + p.RecordId" class="dns-parent-row" @click="toggleParent(p)">
|
||||
<td colspan="7">
|
||||
<span class="text-muted" style="display:inline-block;width:18px;">
|
||||
<i class="glyphicon"
|
||||
:class="expandedMap[p.RecordId] ? 'glyphicon-chevron-down' : 'glyphicon-chevron-right'"></i>
|
||||
</span>
|
||||
<strong>{{ p.Name }}</strong><span class="text-muted">(共 {{ p.Count }} 条记录)</span><span class="text-remark" v-if="p.Remark"><i class="glyphicon glyphicon-list-alt"></i> {{ p.Remark }}</span>
|
||||
</td>
|
||||
<td @click.stop>
|
||||
<button class="btn btn-xs btn-success" @click="addRecord(p)">添加</button>
|
||||
<button class="btn btn-xs btn-info" @click="editHostRemark(p)">备注</button>
|
||||
<button class="btn btn-xs btn-danger" @click="deleteHost(p)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="expandedMap[p.RecordId] && loadingMap[p.RecordId]" class="dns-child-empty">
|
||||
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="c in (expandedMap[p.RecordId] ? (childrenMap[p.RecordId] || []) : [])"
|
||||
:key="'c-' + c.RecordId"
|
||||
v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId]"
|
||||
class="dns-child-row">
|
||||
<td></td>
|
||||
<td>{{ c.LineName }}</td>
|
||||
<td>{{ c.Type }}</td>
|
||||
<td>{{ c.Type == 'A' || c.Type == 'CNAME' ? modeList[c.Mode] : '-' }} <span class="label label-info" v-if="(c.Type == 'A' || c.Type == 'CNAME') && c.Mode == 3">{{c.Weight}}</span></td>
|
||||
<td>{{ c.Value + (c.Type == 'MX' ? ' | ' + c.MX : '') }}<a href="javascript:void(0);" title="复制记录值" @click="copyToClipboard(c, $event)" style="padding-left:6px;"><i class="fa fa-copy"></i></a></td>
|
||||
<td>{{ c.TTL }}</td>
|
||||
<td><font color="green" v-if="c.Status=='1'"><i class="fa fa-check-circle"></i>启用</font><font color="orange" v-if="c.Status!='1'"><i class="fa fa-pause-circle"></i>暂停</font></td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-primary" @click="editRecord(c, p.RecordId)">修改</button>
|
||||
<button class="btn btn-xs btn-warning" @click="setRecordStatus(c, p.RecordId, '0')" v-if="c.Status=='1'">暂停</button>
|
||||
<button class="btn btn-xs btn-success" @click="setRecordStatus(c, p.RecordId, '1')" v-if="c.Status!='1'">启用</button>
|
||||
<button class="btn btn-xs btn-danger" @click="deleteRecord(c, p.RecordId)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId] && (childrenMap[p.RecordId] || []).length === 0"
|
||||
:key="'empty-' + p.RecordId"
|
||||
class="dns-child-empty">
|
||||
<td colspan="8" class="text-muted text-center">暂无记录</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<div class="col-sm-6 text-muted" style="padding-top:6px;">
|
||||
共 {{ total }} 条,当前第 {{ currentPage }} / {{ totalPages }} 页
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 text-right">
|
||||
<ul class="pagination pagination-sm" style="margin:0;">
|
||||
<li :class="{disabled: currentPage === 1}">
|
||||
<a href="javascript:;" @click="goPage(1)">«</a>
|
||||
</li>
|
||||
<li :class="{disabled: currentPage === 1}">
|
||||
<a href="javascript:;" @click="goPage(currentPage - 1)">上一页</a>
|
||||
</li>
|
||||
|
||||
<li v-for="n in pageList" :key="'pg-' + n" :class="{active: n === currentPage}">
|
||||
<a href="javascript:;" @click="goPage(n)">{{ n }}</a>
|
||||
</li>
|
||||
|
||||
<li :class="{disabled: currentPage === totalPages}">
|
||||
<a href="javascript:;" @click="goPage(currentPage + 1)">下一页</a>
|
||||
</li>
|
||||
<li :class="{disabled: currentPage === totalPages}">
|
||||
<a href="javascript:;" @click="goPage(totalPages)">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{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/bootstrapValidator.min.js"></script>
|
||||
<script>
|
||||
var recordLine = {$recordLine|json_encode|raw};
|
||||
var dnsconfig = {$dnsconfig|json_encode|raw};
|
||||
var defaultLine = recordLine[0].id;
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: function () {
|
||||
return {
|
||||
loading: false,
|
||||
recordLine: recordLine,
|
||||
form: {
|
||||
action: '',
|
||||
recordid: '',
|
||||
recordinfo: '',
|
||||
parentid: '',
|
||||
name: '',
|
||||
type: '',
|
||||
line: '',
|
||||
mode: '',
|
||||
value: '',
|
||||
ttl: 600,
|
||||
weight: '',
|
||||
mx: 10,
|
||||
},
|
||||
keyword: '',
|
||||
total: 0,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
parents: [],
|
||||
expandedMap: {}, // {pid: true/false}
|
||||
loadingMap: {}, // {pid: true/false}
|
||||
childrenMap: {}, // {pid: []} 缓存子列表
|
||||
modeList: [
|
||||
'默认',
|
||||
'普通',
|
||||
'轮询',
|
||||
'权重',
|
||||
'智能',
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentPage: function () {
|
||||
return Math.floor(this.offset / this.limit) + 1;
|
||||
},
|
||||
totalPages: function () {
|
||||
return Math.max(1, Math.ceil(this.total / this.limit));
|
||||
},
|
||||
pageList: function () {
|
||||
// 显示最多 5 个页码,居中
|
||||
var totalPages = this.totalPages;
|
||||
var cur = this.currentPage;
|
||||
var windowSize = 5;
|
||||
var half = Math.floor(windowSize / 2);
|
||||
|
||||
var start = Math.max(1, cur - half);
|
||||
var end = Math.min(totalPages, start + windowSize - 1);
|
||||
start = Math.max(1, end - windowSize + 1);
|
||||
|
||||
var arr = [];
|
||||
for (var i = start; i <= end; i++) arr.push(i);
|
||||
return arr;
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.loadParents();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$("#form-store").bootstrapValidator();
|
||||
},
|
||||
methods: {
|
||||
loadParents: function () {
|
||||
var vm = this;
|
||||
vm.loading = true;
|
||||
vm.expandedMap = {};
|
||||
vm.loadingMap = {};
|
||||
vm.childrenMap = {};
|
||||
$.ajax({
|
||||
url: '/record/data/{$domainId}',
|
||||
method: 'POST',
|
||||
data: { keyword: vm.keyword, offset: vm.offset, limit: vm.limit },
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
vm.loading = false;
|
||||
vm.total = res.total || 0;
|
||||
vm.parents = res.rows || [];
|
||||
}).fail(function () {
|
||||
layer.msg('加载父级列表失败');
|
||||
});
|
||||
},
|
||||
|
||||
goPage: function (page) {
|
||||
if (!page) return;
|
||||
page = Math.max(1, Math.min(this.totalPages, page));
|
||||
if (page === this.currentPage) return;
|
||||
|
||||
this.offset = (page - 1) * this.limit;
|
||||
this.loadParents();
|
||||
},
|
||||
|
||||
toggleParent: function (p) {
|
||||
var pid = p.RecordId;
|
||||
|
||||
// 收起
|
||||
if (this.expandedMap[pid]) {
|
||||
this.$set(this.expandedMap, pid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 展开
|
||||
this.$set(this.expandedMap, pid, true);
|
||||
|
||||
// 已有缓存就不再请求
|
||||
if (this.childrenMap[pid]) return;
|
||||
|
||||
this.loadChildren(pid);
|
||||
},
|
||||
|
||||
loadChildren: function (pid) {
|
||||
var vm = this;
|
||||
vm.$set(vm.loadingMap, pid, true);
|
||||
|
||||
$.ajax({
|
||||
url: '/record/data/{$domainId}',
|
||||
method: 'POST',
|
||||
data: { subdomain: pid },
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
vm.$set(vm.childrenMap, pid, (res && res.rows) ? res.rows : []);
|
||||
}).fail(function () {
|
||||
layer.msg('加载子级列表失败');
|
||||
vm.$set(vm.childrenMap, pid, []);
|
||||
}).always(function () {
|
||||
vm.$set(vm.loadingMap, pid, false);
|
||||
});
|
||||
},
|
||||
|
||||
addRecord: function (p) {
|
||||
this.form.action = 'add';
|
||||
this.form.recordid = '';
|
||||
this.form.recordinfo = '';
|
||||
this.form.parentid = p.RecordId || '';
|
||||
this.form.name = p.Name || '';
|
||||
this.form.type = 'A';
|
||||
this.form.line = defaultLine;
|
||||
this.form.mode = '1';
|
||||
this.form.value = '';
|
||||
this.form.ttl = 600;
|
||||
this.form.weight = '';
|
||||
this.form.mx = 10;
|
||||
$("#modal-store").modal('show');
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
},
|
||||
editRecord: function (c, parentid) {
|
||||
this.form.action = 'update';
|
||||
this.form.recordid = c.RecordId;
|
||||
this.form.recordinfo = JSON.stringify(c);
|
||||
this.form.parentid = parentid || '';
|
||||
this.form.name = c.Name;
|
||||
this.form.type = c.Type;
|
||||
this.form.line = c.Line;
|
||||
this.form.mode = c.Mode;
|
||||
this.form.value = c.Value;
|
||||
this.form.ttl = c.TTL;
|
||||
this.form.weight = c.Weight;
|
||||
this.form.mx = c.MX || 10;
|
||||
$("#modal-store").modal('show');
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
},
|
||||
save: function () {
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var vm = this;
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/'+vm.form.action+'/{$domainId}',
|
||||
data : vm.form,
|
||||
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');
|
||||
if(vm.form.parentid){
|
||||
vm.loadChildren(vm.form.parentid);
|
||||
}else{
|
||||
vm.loadParents();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setRecordStatus: function (c, parentid, status) {
|
||||
var vm = this;
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/status/{$domainId}',
|
||||
data : { recordid: c.RecordId, status: status, recordinfo: JSON.stringify(c) },
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg(status=='1'?'开启成功':'暂停成功', {icon: 1, time:500});
|
||||
vm.loadChildren(parentid);
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteRecord: function (c, parentid) {
|
||||
var vm = this;
|
||||
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/delete/{$domainId}',
|
||||
data : { recordid: c.RecordId, recordinfo: JSON.stringify(c) },
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg('删除成功', {icon: 1, time:800});
|
||||
vm.loadChildren(parentid);
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteHost: function (p) {
|
||||
var vm = this;
|
||||
layer.confirm('确定要删除此主机名下所有解析记录吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/delete/{$domainId}',
|
||||
data : { recordid: p.RecordId },
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg('删除成功', {icon: 1, time:800});
|
||||
vm.loadParents();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
editHostRemark: function (p) {
|
||||
var vm = this;
|
||||
layer.open({
|
||||
type: 1,
|
||||
area: ['350px'],
|
||||
closeBtn: 2,
|
||||
title: '编辑备注',
|
||||
content: '<div style="padding:15px"><div class="form-group"><input class="form-control" type="text" name="remark" value="'+(p.Remark==null?'':p.Remark)+'" autocomplete="off" placeholder="备注信息"></div></div>',
|
||||
btn: ['确认', '取消'],
|
||||
yes: function(){
|
||||
var remark = $("input[name='remark']").val();
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/remark/{$domainId}',
|
||||
data : {recordid:p.RecordId, remark:remark},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg('保存成功', {icon: 1, time:800});
|
||||
vm.loadParents();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon:2});
|
||||
}
|
||||
},
|
||||
error:function(data){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: function (c, event) {
|
||||
var text = c.Value;
|
||||
var tempInput = document.createElement('input');
|
||||
tempInput.style.position = 'absolute';
|
||||
tempInput.style.left = '-9999px';
|
||||
tempInput.value = text;
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(tempInput);
|
||||
$(event.target).toggleClass('fa-copy fa-check');
|
||||
setTimeout(function(){
|
||||
$(event.target).toggleClass('fa-check fa-copy');
|
||||
}, 1000);
|
||||
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{/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,8 +293,7 @@ $(document).ready(function(){
|
||||
title: '记录值',
|
||||
formatter: function(value, row, index) {
|
||||
var copyId = 'copy-value-' + row.RecordId;
|
||||
if(row.Type == 'MX') {
|
||||
// 只复制 mx.yandex.net,按钮在其右侧,优先级单独显示
|
||||
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>';
|
||||
@@ -349,10 +350,16 @@ $(document).ready(function(){
|
||||
if(dnsconfig.remark == 1){
|
||||
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a> ';
|
||||
}
|
||||
if(row.Name === "@") var domain = "{$domainName}";
|
||||
else var domain = row.Name + ".{$domainName}";
|
||||
domain = domain.replace(/\*/g, 'www');
|
||||
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></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}";
|
||||
domain = domain.replace(/\*/g, 'www');
|
||||
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
},
|
||||
@@ -511,9 +518,7 @@ function setStatus(recordid, status){
|
||||
}
|
||||
function delItem(recordid) {
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', recordid);
|
||||
var confirmobj = layer.confirm('确定要删除此解析记录吗?', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
@@ -531,8 +536,6 @@ function delItem(recordid) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function setRemark(recordid) {
|
||||
@@ -587,9 +590,7 @@ function operation(action){
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmobj = layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
@@ -607,8 +608,6 @@ function operation(action){
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function batch_edit(records){
|
||||
@@ -730,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}
|
||||
@@ -162,7 +162,7 @@
|
||||
<form method="post">
|
||||
<div id="error" style="display:none"></div>
|
||||
<div id="success" style="display:none"></div>
|
||||
|
||||
{if $dbconfig!='1'}
|
||||
<div class="form-group">
|
||||
<div class="form-field">
|
||||
<label>MySQL 数据库地址</label>
|
||||
@@ -194,6 +194,7 @@
|
||||
<input type="text" name="mysql_prefix" value="dnsmgr_">
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-field">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<div class="form-group" v-show="set.switchtype==0">
|
||||
<label class="col-sm-3 control-label no-padding-right" is-required>记录值</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="value" v-model="set.value" placeholder="支持填写IPv4或CNAME地址" class="form-control" required>
|
||||
<input type="text" name="value" v-model="set.value" placeholder="支持填写IP或CNAME地址" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-show="set.switchtype==0&&dnstype=='cloudflare'">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user