mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-09 23:16:27 +02:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4825f1312 | ||
|
|
2dd4978fb3 | ||
|
|
349c1d70e2 | ||
|
|
a112cf0ea2 | ||
|
|
c1600c7b17 | ||
|
|
b8f64db33c | ||
|
|
96e1c8a972 | ||
|
|
bebd655bcc | ||
|
|
36d42da491 | ||
|
|
b0d831da56 | ||
|
|
c420c81877 | ||
|
|
72492e9fd9 | ||
|
|
3075b8d8a5 | ||
|
|
92d8f49778 | ||
|
|
1084fea43b | ||
|
|
7670d5a387 | ||
|
|
a9b773868d | ||
|
|
141b2ead43 | ||
|
|
b1b0655cc0 | ||
|
|
c64a385ffc | ||
|
|
7d02f15fde | ||
|
|
918bd872d9 | ||
|
|
0bc745e164 | ||
|
|
efed00afa3 | ||
|
|
1b1605400d | ||
|
|
879e667d9a | ||
|
|
0813f2cdca | ||
|
|
780e01ce4f | ||
|
|
3ea41c1c8b | ||
|
|
e25d5d76e9 | ||
|
|
c0e72908ab | ||
|
|
867785b774 | ||
|
|
d579ca07af | ||
|
|
7161caf0a5 | ||
|
|
c91b116466 | ||
|
|
b2d27b18a3 | ||
|
|
ee45ddd7ec | ||
|
|
9b66b020c9 | ||
|
|
ec16c3fc8b | ||
|
|
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 |
82
.github/docker/Dockerfile
vendored
Normal file
82
.github/docker/Dockerfile
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
ARG ALPINE_VERSION=3.19
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
# Setup document root
|
||||
WORKDIR /app/www
|
||||
|
||||
# Install packages and remove default server definition
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
nginx \
|
||||
php82 \
|
||||
php82-ctype \
|
||||
php82-curl \
|
||||
php82-dom \
|
||||
php82-fileinfo \
|
||||
php82-fpm \
|
||||
php82-ftp \
|
||||
php82-gd \
|
||||
php82-gettext \
|
||||
php82-intl \
|
||||
php82-iconv \
|
||||
php82-mbstring \
|
||||
php82-mysqli \
|
||||
php82-opcache \
|
||||
php82-openssl \
|
||||
php82-phar \
|
||||
php82-sodium \
|
||||
php82-session \
|
||||
php82-simplexml \
|
||||
php82-tokenizer \
|
||||
php82-xml \
|
||||
php82-xmlreader \
|
||||
php82-xmlwriter \
|
||||
php82-zip \
|
||||
php82-pdo \
|
||||
php82-pdo_mysql \
|
||||
php82-pdo_sqlite \
|
||||
php82-pecl-swoole \
|
||||
php82-pecl-ssh2 \
|
||||
supervisor
|
||||
|
||||
RUN rm -rf /var/cache/apk/* /tmp/*
|
||||
|
||||
# Configure nginx - http
|
||||
COPY config/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Configure PHP-FPM
|
||||
ENV PHP_INI_DIR /etc/php82
|
||||
COPY config/fpm-pool.conf ${PHP_INI_DIR}/php-fpm.d/www.conf
|
||||
COPY config/php.ini ${PHP_INI_DIR}/conf.d/custom.ini
|
||||
|
||||
# Configure supervisord
|
||||
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Add application
|
||||
RUN mkdir -p /usr/src && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
|
||||
|
||||
# Install composer
|
||||
RUN wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
|
||||
|
||||
RUN composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader
|
||||
|
||||
RUN adduser -D -s /sbin/nologin -g www www && chown -R www.www /usr/src/www /var/lib/nginx /var/log/nginx
|
||||
|
||||
# crontab
|
||||
RUN echo "* * * * * cd /app/www && /usr/bin/php82 think certtask" | crontab -u www -
|
||||
|
||||
COPY config/run_tasks.sh /app/run_tasks.sh
|
||||
RUN chmod +x /app/run_tasks.sh
|
||||
|
||||
# copy entrypoint script
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
|
||||
# Expose the port nginx is reachable on
|
||||
EXPOSE 80
|
||||
|
||||
# Let supervisord start nginx & php-fpm
|
||||
CMD /usr/sbin/crond && /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Configure a healthcheck to validate that everything is up&running
|
||||
HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1/fpm-ping || exit 1
|
||||
21
.github/docker/config/fpm-pool.conf
vendored
Normal file
21
.github/docker/config/fpm-pool.conf
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
[global]
|
||||
error_log = /dev/stderr
|
||||
|
||||
[www]
|
||||
listen = /run/php-fpm.sock
|
||||
listen.backlog = 8192
|
||||
listen.allowed_clients = 127.0.0.1
|
||||
listen.owner = www
|
||||
listen.group = www
|
||||
listen.mode = 0666
|
||||
user = www
|
||||
group = www
|
||||
pm.status_path = /fpm-status
|
||||
pm = ondemand
|
||||
pm.max_children = 100
|
||||
pm.process_idle_timeout = 60s;
|
||||
pm.max_requests = 1000
|
||||
clear_env = no
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
||||
ping.path = /fpm-ping
|
||||
94
.github/docker/config/nginx.conf
vendored
Normal file
94
.github/docker/config/nginx.conf
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
user www;
|
||||
worker_processes auto;
|
||||
error_log stderr warn;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
# Threat files with a unknown filetype as binary
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Define custom log format to include reponse times
|
||||
log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'$request_time $upstream_response_time $pipe $upstream_cache_status';
|
||||
|
||||
access_log /dev/stdout main_timed;
|
||||
error_log /dev/stderr crit;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# Enable gzip compression by default
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# Include server configs
|
||||
server {
|
||||
listen [::]:80 default_server;
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
sendfile on;
|
||||
tcp_nodelay on;
|
||||
absolute_redirect off;
|
||||
|
||||
root /app/www/public;
|
||||
index index.php;
|
||||
|
||||
# Pass the PHP scripts to PHP-FPM listening on php-fpm.sock
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/run/php-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
#rewrite rule for pretty urls
|
||||
location / {
|
||||
if (!-e $request_filename){
|
||||
rewrite ^(.*)$ /index.php?s=$1 last; break;
|
||||
}
|
||||
}
|
||||
|
||||
# Set the cache-control headers on assets to cache for 5 days
|
||||
location ~* \.(jpg|jpeg|gif|png|ico|bmp)$ {
|
||||
access_log off;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location ~* \.(css|js)$ {
|
||||
access_log off;
|
||||
expires 12h;
|
||||
}
|
||||
|
||||
# Deny access to . files, for security
|
||||
location ~ /\. {
|
||||
log_not_found off;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Allow fpm ping and status from localhost
|
||||
location ~ ^/(fpm-status|fpm-ping)$ {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.sock;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
.github/docker/config/php.ini
vendored
Normal file
15
.github/docker/config/php.ini
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[PHP]
|
||||
short_open_tag = On
|
||||
expose_php = Off
|
||||
max_execution_time = 300
|
||||
post_max_size = 50M
|
||||
upload_max_filesize = 50M
|
||||
[Date]
|
||||
date.timezone = PRC
|
||||
[Opcache]
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.interned_strings_buffer=32
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=30
|
||||
7
.github/docker/config/run_tasks.sh
vendored
Normal file
7
.github/docker/config/run_tasks.sh
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "/app/www/.env" ]; then
|
||||
php /app/www/think dmtask
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
37
.github/docker/config/supervisord.conf
vendored
Normal file
37
.github/docker/config/supervisord.conf
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/run/supervisord.pid
|
||||
|
||||
[program:php-fpm]
|
||||
command=php-fpm82 -F
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
autorestart=false
|
||||
startretries=0
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g 'daemon off;'
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
autorestart=false
|
||||
startretries=0
|
||||
|
||||
[program:dmtask]
|
||||
command=php think dmtask
|
||||
user=www
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=5
|
||||
startretries=99999
|
||||
18
.github/docker/entrypoint.sh
vendored
Normal file
18
.github/docker/entrypoint.sh
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ ! -f /app/www/public/index.php ] || [ ! -f /app/firstrun ]; then
|
||||
echo 'Copying new files'
|
||||
\cp -a /usr/src/www /app/
|
||||
|
||||
if [ -d /app/www/runtime/cache ]; then
|
||||
rm -rf /app/www/runtime/*
|
||||
fi
|
||||
|
||||
chown -R www.www /app/www
|
||||
|
||||
touch /app/firstrun
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
56
.github/workflows/docker-build.yml
vendored
Normal file
56
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# 手动触发:构建多架构镜像(amd64 / arm64),仅推送 latest 至 Docker Hub 与华为云 SWR。
|
||||
# Dockerfile 与构建上下文位于 .github/docker/ 目录。
|
||||
#
|
||||
# 需在仓库 Settings → Secrets 中配置:
|
||||
# DOCKERHUB_USERNAME / DOCKERHUB_TOKEN(Docker Hub 访问令牌)
|
||||
# HUAWEI_SWR_USERNAME / HUAWEI_SWR_PASSWORD(华为云 SWR 登录凭证,与本地 docker login swr.cn-east-3.myhuaweicloud.com 一致)
|
||||
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Huawei SWR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: swr.cn-east-3.myhuaweicloud.com
|
||||
username: ${{ secrets.HUAWEI_SWR_USERNAME }}
|
||||
password: ${{ secrets.HUAWEI_SWR_PASSWORD }}
|
||||
|
||||
- name: Build and push (Docker Hub + Huawei SWR, latest only)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .github/docker
|
||||
file: .github/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
# 避免向仓库推送 attestations;部分镜像仓库(含部分 SWR 场景)无法解析导致 “fail to parse manifest.json”
|
||||
provenance: false
|
||||
sbom: false
|
||||
tags: |
|
||||
netcccyun/dnsmgr:latest
|
||||
swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
/vendor
|
||||
*.log
|
||||
.env
|
||||
.ace-tool/
|
||||
/.codex-tmp/dns-panel-ref/
|
||||
|
||||
@@ -302,10 +302,12 @@ function getMainDomain($host)
|
||||
$domains = config('temp.domains');
|
||||
if (!$domains) {
|
||||
$domains = Db::name('domain')->column('name');
|
||||
$domains_alias = Db::name('domain_alias')->column('name');
|
||||
$domains = array_merge($domains, $domains_alias);
|
||||
config(['domains'=>$domains], 'temp');
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
if (str_ends_with($host, $domain)) {
|
||||
if ($host === $domain || str_ends_with($host, '.' . $domain)) {
|
||||
return $domain;
|
||||
}
|
||||
}
|
||||
@@ -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 . '未在本系统添加'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1070
app/controller/Cloudflare.php
Normal file
1070
app/controller/Cloudflare.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,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 +28,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 +36,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 +95,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 +128,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' => '该域名账户下存在域名,无法删除']);
|
||||
@@ -182,12 +188,20 @@ class Domain extends BaseController
|
||||
$kw = input('post.kw', null, 'trim');
|
||||
$type = input('post.type', null, 'trim');
|
||||
$status = input('post.status', 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);
|
||||
@@ -203,11 +217,28 @@ 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();
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
|
||||
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
@@ -274,6 +305,7 @@ class Domain extends BaseController
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$id = input('post.id/d');
|
||||
Db::name('domain')->where('id', $id)->delete();
|
||||
Db::name('domain_alias')->where('did', $id)->delete();
|
||||
Db::name('dmtask')->where('did', $id)->delete();
|
||||
Db::name('optimizeip')->where('did', $id)->delete();
|
||||
Db::name('sctask')->where('did', $id)->delete();
|
||||
@@ -321,6 +353,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 +458,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 +841,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 +863,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++;
|
||||
@@ -1065,8 +1107,88 @@ class Domain extends BaseController
|
||||
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
$domainRecords = $dns->getWeightSubDomains($page, $limit, $keyword);
|
||||
if (!$domainRecords) return json(['total' => 0, 'rows' => []]);
|
||||
return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]);
|
||||
}
|
||||
|
||||
public function alias()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$drow = Db::name('domain')->where('id', $id)->find();
|
||||
if (!$drow) {
|
||||
return $this->alert('error', '域名不存在');
|
||||
}
|
||||
if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限');
|
||||
if (request()->isAjax()) {
|
||||
$act = input('param.act');
|
||||
if ($act == 'add') {
|
||||
$alias = input('post.alias', null, 'trim');
|
||||
if (empty($alias)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
if ($dns->addDomainAlias($alias)) {
|
||||
return json(['code' => 0, 'msg' => '添加域名别名成功']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '添加域名别名失败,' . $dns->getError()]);
|
||||
}
|
||||
} elseif ($act == 'delete') {
|
||||
$alias_id = input('post.alias_id/d');
|
||||
if (empty($alias_id)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
if ($dns->deleteDomainAlias($alias_id)) {
|
||||
return json(['code' => 0, 'msg' => '删除域名别名成功']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '删除域名别名失败,' . $dns->getError()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
|
||||
$domainAliasList = $dns->domainAliasList();
|
||||
if ($domainAliasList === false) $domainAliasList = [];
|
||||
|
||||
$this->updateAliasList($id, $domainAliasList);
|
||||
|
||||
View::assign('domainId', $id);
|
||||
View::assign('domainName', $drow['name']);
|
||||
View::assign('domainAliasList', $domainAliasList);
|
||||
return view();
|
||||
}
|
||||
|
||||
private function updateAliasList($id, $domainAliasList)
|
||||
{
|
||||
$domainAliases = array_column($domainAliasList, 'DomainAlias');
|
||||
$addList = [];
|
||||
$deleteList = [];
|
||||
$existList = Db::name('domain_alias')->where('did', $id)->select()->toArray();
|
||||
$existAliases = array_column($existList, 'name');
|
||||
foreach ($existList as $item) {
|
||||
if (!in_array($item['name'], $domainAliases)) {
|
||||
$deleteList[] = $item['id'];
|
||||
}
|
||||
}
|
||||
foreach ($domainAliases as $item) {
|
||||
if (!in_array($item, $existAliases)) {
|
||||
$addList[] = $item;
|
||||
}
|
||||
}
|
||||
if (!empty($deleteList)) {
|
||||
Db::name('domain_alias')->where('id', 'in', $deleteList)->delete();
|
||||
}
|
||||
if (!empty($addList)) {
|
||||
$dataList = [];
|
||||
foreach ($addList as $item) {
|
||||
$dataList[] = [
|
||||
'did' => $id,
|
||||
'name' => $item,
|
||||
];
|
||||
}
|
||||
Db::name('domain_alias')->insertAll($dataList);
|
||||
}
|
||||
}
|
||||
|
||||
public function expire_notice()
|
||||
{
|
||||
|
||||
@@ -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任务已存在']);
|
||||
|
||||
@@ -95,11 +95,12 @@ class System extends BaseController
|
||||
public function proxytest()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$proxy_server = trim($_POST['proxy_server']);
|
||||
$proxy_port = $_POST['proxy_port'];
|
||||
$proxy_user = trim($_POST['proxy_user']);
|
||||
$proxy_pwd = trim($_POST['proxy_pwd']);
|
||||
$proxy_type = $_POST['proxy_type'];
|
||||
$proxy_server = input('post.proxy_server', '', 'trim');
|
||||
$proxy_port = input('post.proxy_port/d', 0);
|
||||
$proxy_user = input('post.proxy_user', '', 'trim');
|
||||
$proxy_pwd = input('post.proxy_pwd', '', 'trim');
|
||||
$proxy_type = input('post.proxy_type', 'http', 'trim');
|
||||
|
||||
try {
|
||||
check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -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' => [
|
||||
@@ -370,9 +413,8 @@ class DeployHelper
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在网站管理->证书管理查看证书的ID,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -435,9 +477,8 @@ class DeployHelper
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在站点->证书管理查看证书的ID,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -559,7 +600,7 @@ class DeployHelper
|
||||
'icon' => 'opanel.png',
|
||||
'desc' => '更新面板证书管理内的SSL证书',
|
||||
'note' => null,
|
||||
'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书',
|
||||
'tasknote' => '',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
@@ -581,7 +622,7 @@ class DeployHelper
|
||||
'v1' => '1.x',
|
||||
'v2' => '2.x',
|
||||
],
|
||||
'value' => 'v1',
|
||||
'value' => 'v2',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
@@ -594,7 +635,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 +720,65 @@ class DeployHelper
|
||||
],
|
||||
],
|
||||
],
|
||||
'acepanel' => [
|
||||
'name' => 'AcePanel',
|
||||
'class' => 1,
|
||||
'icon' => 'acepanel.svg',
|
||||
'desc' => '支持 AcePanel 3.0+ 版本使用',
|
||||
'note' => '支持 AcePanel 3.0+ 版本使用',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'AcePanel 地址',
|
||||
'note' => '填写规则如:https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'id' => [
|
||||
'name' => '访问令牌ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '1',
|
||||
'note' => 'AcePanel 设置->用户->访问令牌',
|
||||
'required' => true,
|
||||
],
|
||||
'token' => [
|
||||
'name' => '访问令牌',
|
||||
'type' => 'input',
|
||||
'note' => 'AcePanel 设置->用户->访问令牌',
|
||||
'placeholder' => '32位字符串',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'type' => [
|
||||
'name' => '部署类型',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => 'AcePanel 网站的证书',
|
||||
'1' => 'AcePanel 本身的证书',
|
||||
],
|
||||
'value' => '0',
|
||||
'required' => true,
|
||||
],
|
||||
'sites' => [
|
||||
'name' => '网站名称列表',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站名称,每行一个',
|
||||
'note' => '填写创建网站时设置的网站唯一名称',
|
||||
'show' => 'type==0',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'ratpanel' => [
|
||||
'name' => '耗子面板',
|
||||
'class' => 1,
|
||||
@@ -754,6 +879,53 @@ class DeployHelper
|
||||
],
|
||||
],
|
||||
],
|
||||
'amh' => [
|
||||
'name' => 'AMH面板',
|
||||
'class' => 1,
|
||||
'icon' => 'amh.ico',
|
||||
'desc' => '',
|
||||
'note' => null,
|
||||
'tasknote' => '',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'AMH面板地址',
|
||||
'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'apikey' => [
|
||||
'name' => 'API接口密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '安装amapi软件后查看,是密钥不是私钥',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'env_name' => [
|
||||
'name' => '环境名称',
|
||||
'type' => 'input',
|
||||
'placeholder' => '如:lnmp01',
|
||||
'required' => true,
|
||||
],
|
||||
'vhost_name' => [
|
||||
'name' => '网站名称列表',
|
||||
'type' => 'textarea',
|
||||
'placeholder' => '填写要部署证书的网站标识域名,每行一个',
|
||||
'note' => '网站标识域名一列的值,并非绑定域名',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'synology' => [
|
||||
'name' => '群晖面板',
|
||||
'class' => 1,
|
||||
@@ -857,7 +1029,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,6 +1195,7 @@ 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'=>'waf2', 'label'=>'Web应用防火墙2.0'],
|
||||
@@ -1035,6 +1208,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 +1218,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 +1250,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==\'ddoscoo\'||product==\'esa\'||product==\'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'regionid' => [
|
||||
@@ -1124,6 +1306,21 @@ 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,
|
||||
],
|
||||
'deploy_type' => [
|
||||
'name' => '部署证书类型',
|
||||
'type' => 'select',
|
||||
@@ -1132,21 +1329,21 @@ ctrl+x 保存退出',
|
||||
['value'=>'1', 'label'=>'扩展证书'],
|
||||
],
|
||||
'value' => '0',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'',
|
||||
'required' => true,
|
||||
],
|
||||
'clb_domain' => [
|
||||
'name' => '扩展域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product==\'clb\'&&deploy_type==1',
|
||||
'show' => 'product==\'clb\'&&deploy_type==1||product==\'ga\'&&deploy_type==1',
|
||||
'required' => true,
|
||||
],
|
||||
'domain' => [
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'',
|
||||
'placeholder' => '多个域名可用,隔开',
|
||||
'show' => 'product!=\'esa\'&&product!=\'esa_saas\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1199,6 +1396,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 +1497,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 +1548,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 +1667,7 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'CDN'],
|
||||
['value'=>'oss', 'label'=>'OSS'],
|
||||
['value'=>'pili', 'label'=>'视频直播'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1459,6 +1683,7 @@ ctrl+x 保存退出',
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可使用,分隔',
|
||||
'show' => 'product!=\'upload\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
@@ -1569,6 +1794,7 @@ ctrl+x 保存退出',
|
||||
['value'=>'cdn', 'label'=>'CDN'],
|
||||
['value'=>'blb', 'label'=>'普通型BLB'],
|
||||
['value'=>'appblb', 'label'=>'应用型BLB'],
|
||||
['value'=>'upload', 'label'=>'上传到证书管理'],
|
||||
],
|
||||
'value' => 'cdn',
|
||||
'required' => true,
|
||||
@@ -1702,7 +1928,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 +1942,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 +2159,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 +2257,8 @@ ctrl+x 保存退出',
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'placeholder' => '留空则为添加证书',
|
||||
'note' => '在SSL证书->我的证书页面查看,注意域名是否与证书匹配',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -2248,6 +2504,12 @@ ctrl+x 保存退出',
|
||||
'required' => true,
|
||||
'show' => 'auth==1',
|
||||
],
|
||||
'passphrase' => [
|
||||
'name' => '私钥密码',
|
||||
'type' => 'input',
|
||||
'placeholder' => '若私钥有设置密码,请填写此项',
|
||||
'show' => 'auth==1',
|
||||
],
|
||||
'windows' => [
|
||||
'name' => '是否Windows',
|
||||
'type' => 'radio',
|
||||
@@ -2413,6 +2675,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,96 @@ class DnsHelper
|
||||
'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,
|
||||
],
|
||||
];
|
||||
|
||||
public static $line_name = [
|
||||
@@ -173,9 +619,14 @@ 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'],
|
||||
];
|
||||
|
||||
public static function getList()
|
||||
@@ -195,11 +646,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 +663,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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,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 +136,98 @@ class aliyun implements DeployInterface
|
||||
|
||||
private function deploy_cdn($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('CDN绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('CDN绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cdn.aliyuncs.com', '2018-05-10', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetCdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetCdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_dcdn($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('DCDN绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('DCDN绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15', $this->proxy);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetDcdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('DCDN域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_esa_saas($cas_id, $config)
|
||||
{
|
||||
$sitename = $config['esa_sitename'];
|
||||
$saas_sitename = $config['esa_saas_sitename'];
|
||||
if (empty($sitename)) throw new Exception('ESA站点名称不能为空');
|
||||
if (empty($saas_sitename)) throw new Exception('ESA SAAS域名不能为空');
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$endpoint = 'esa.ap-southeast-1.aliyuncs.com';
|
||||
} else {
|
||||
$endpoint = 'esa.cn-hangzhou.aliyuncs.com';
|
||||
}
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2024-09-10');
|
||||
$param = [
|
||||
'Action' => 'ListSites',
|
||||
'SiteName' => $sitename,
|
||||
'SiteSearchType' => 'exact',
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('DCDN域名 ' . $domain . ' 部署证书成功!');
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA站点列表失败:' . $e->getMessage());
|
||||
}
|
||||
if ($data['TotalCount'] == 0) throw new Exception('ESA站点 ' . $sitename . ' 不存在');
|
||||
$this->log('成功查询到' . $data['TotalCount'] . '个ESA站点');
|
||||
$site_id = $data['Sites'][0]['SiteId'];
|
||||
// 查询对应的saas域名
|
||||
$param = [
|
||||
'Action' => 'ListCustomHostnames',
|
||||
'SiteName' => $saas_sitename,
|
||||
'SiteId' => $site_id,
|
||||
'SiteSearchType' => 'exact',
|
||||
];
|
||||
try {
|
||||
$saas_data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA saas域名失败:' . $e->getMessage());
|
||||
}
|
||||
if ($saas_data['TotalCount'] == 0) throw new Exception('ESA saas站点 ' . $saas_sitename . ' 不存在');
|
||||
$saas_hostname_id = $saas_data['Hostnames'][0]['HostnameId'];
|
||||
|
||||
$param = [
|
||||
'Action' => 'UpdateCustomHostname',
|
||||
'HostnameId' => $saas_hostname_id,
|
||||
'SslFlag' => 'on',
|
||||
'CertType' => 'cas',
|
||||
'CasId' => $cas_id,
|
||||
'CasRegion' => $config['region'],
|
||||
];
|
||||
$this->log('ESA SAAS站点部署参数 ' . json_encode($param));
|
||||
try {
|
||||
$saas_deploy_result = $client->request($param);
|
||||
$this->log('ESA SAAS站点部署结果 ' . json_encode($saas_deploy_result));
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('部署失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('ESA SAAS站点 ' . $saas_sitename . ' 证书添加成功!');
|
||||
}
|
||||
|
||||
private function deploy_esa($cas_id, $cert_name, $config)
|
||||
@@ -200,11 +267,11 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
$this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书');
|
||||
|
||||
$exist_cert_id = null;
|
||||
$exist_cert_name = null;
|
||||
$exist_cert_casid = null;
|
||||
$exist_cert = null;
|
||||
$oldest_cert = null;
|
||||
if ($data['TotalCount'] > 0) {
|
||||
foreach ($data['Result'] as $cert) {
|
||||
if ($cert['Type'] == 'free') continue;
|
||||
$domains = explode(',', $cert['SAN']);
|
||||
$flag = true;
|
||||
foreach ($domains as $domain) {
|
||||
@@ -214,11 +281,40 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
if ($flag) {
|
||||
$exist_cert_id = $cert['Id'];
|
||||
$exist_cert_name = $cert['Name'];
|
||||
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
|
||||
$exist_cert = $cert;
|
||||
break;
|
||||
}
|
||||
if (!$oldest_cert) {
|
||||
$oldest_cert = $cert;
|
||||
} elseif (strtotime($cert['CreateTime']) < strtotime($oldest_cert['CreateTime'])) {
|
||||
$oldest_cert = $cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$exist_cert) { //新增证书时,若配额已满,则删除最旧的证书
|
||||
$param = [
|
||||
'Action' => 'ListInstanceQuotasWithUsage',
|
||||
'SiteId' => $site_id,
|
||||
'QuotaNames' => 'customHttpCert',
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询ESA站点证书配额失败:' . $e->getMessage());
|
||||
}
|
||||
if (!empty($data['Quotas']) && intval($data['Quotas'][0]['Usage']) >= intval($data['Quotas'][0]['QuotaValue']) && $oldest_cert) {
|
||||
$param = [
|
||||
'Action' => 'DeleteCertificate',
|
||||
'SiteId' => $site_id,
|
||||
'Id' => $oldest_cert['Id'],
|
||||
];
|
||||
try {
|
||||
$client->request($param, 'GET');
|
||||
$this->log('ESA站点 ' . $sitename . ' 删除证书 ' . $oldest_cert['Name'] . ' 成功');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('ESA站点 ' . $sitename . ' 删除证书' . $oldest_cert['Name'] . '失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,10 +327,10 @@ class aliyun implements DeployInterface
|
||||
'Region' => $config['region'],
|
||||
];
|
||||
|
||||
if ($exist_cert_id) {
|
||||
$param['Id'] = $exist_cert_id;
|
||||
if ($exist_cert) {
|
||||
$param['Id'] = $exist_cert['Id'];
|
||||
|
||||
if ($exist_cert_casid == $cas_id) {
|
||||
if (isset($exist_cert['CasId']) && $exist_cert['CasId'] == $cas_id) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
@@ -242,8 +338,8 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client->request($param);
|
||||
|
||||
if ($exist_cert_name) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功');
|
||||
if ($exist_cert) {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert['Name'] . ' 更新成功');
|
||||
} else {
|
||||
$this->log('ESA站点 ' . $sitename . ' 证书添加成功!');
|
||||
}
|
||||
@@ -255,14 +351,16 @@ class aliyun implements DeployInterface
|
||||
if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空');
|
||||
if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空');
|
||||
$client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']);
|
||||
$client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou');
|
||||
$this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
if (empty($domain)) continue;
|
||||
$client->addBucketCnameCert($config['oss_bucket'], $domain, $cert_id . '-cn-hangzhou');
|
||||
$this->log('OSS域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_waf($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('WAF绑定域名不能为空');
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$cert_id .= '-ap-southeast-1';
|
||||
@@ -287,49 +385,63 @@ class aliyun implements DeployInterface
|
||||
$instance_id = $data['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
$param = [
|
||||
'Action' => 'DescribeDomainDetail',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询CNAME接入详情失败:' . $e->getMessage());
|
||||
}
|
||||
if (!isset($data['Listen'])) {
|
||||
throw new Exception('没有找到' . $domain . '监听器');
|
||||
}
|
||||
|
||||
if (isset($data['Listen']['CertId'])) {
|
||||
$old_cert_id = $data['Listen']['CertId'];
|
||||
if (!empty($old_cert_id) && $old_cert_id == $cert_id) {
|
||||
$this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
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 . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
$data['Listen']['CertId'] = $cert_id;
|
||||
if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443];
|
||||
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
|
||||
$param = [
|
||||
'Action' => 'ModifyDomain',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'Listen' => json_encode($data['Listen']),
|
||||
'Redirect' => json_encode($data['Redirect']),
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$data = $client->request($param);
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_waf2($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('WAF绑定域名不能为空');
|
||||
|
||||
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
@@ -348,23 +460,24 @@ class aliyun implements DeployInterface
|
||||
$instance_id = $data['InstanceInfo']['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
$param = [
|
||||
'Action' => 'CreateCertificateByCertificateId',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'CreateCertificateByCertificateId',
|
||||
'InstanceId' => $instance_id,
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('WAF域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_api($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$groupid = $config['api_groupid'];
|
||||
if (empty($groupid)) throw new Exception('API分组ID不能为空');
|
||||
if (empty($domain)) throw new Exception('API分组绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('API分组绑定域名不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
if (!$certInfo) throw new Exception('证书解析失败');
|
||||
@@ -374,76 +487,80 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2016-07-14', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'SetDomainCertificate',
|
||||
'GroupId' => $groupid,
|
||||
'DomainName' => $domain,
|
||||
'CertificateName' => $cert_name,
|
||||
'CertificateBody' => $fullchain,
|
||||
'CertificatePrivateKey' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetDomainCertificate',
|
||||
'GroupId' => $groupid,
|
||||
'DomainName' => $domain,
|
||||
'CertificateName' => $cert_name,
|
||||
'CertificateBody' => $fullchain,
|
||||
'CertificatePrivateKey' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('API网关域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('API网关域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_ddoscoo($cert_id, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('绑定域名不能为空');
|
||||
|
||||
$endpoint = 'ddoscoo.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-01-01', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'AssociateWebCert',
|
||||
'Domain' => $domain,
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'AssociateWebCert',
|
||||
'Domain' => $domain,
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
|
||||
$this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!');
|
||||
$this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_live($cert_id, $cert_name, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('视频直播绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('视频直播绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'live.aliyuncs.com', '2016-11-01', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetLiveDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('设置视频直播域名 ' . $domain . ' 证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetLiveDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
'CertId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('设置视频直播域名 ' . $domain . ' 证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_vod($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
if (empty($domain)) throw new Exception('视频点播绑定域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('视频点播绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'vod.cn-shanghai.aliyuncs.com', '2017-03-21', $this->proxy);
|
||||
$param = [
|
||||
'Action' => 'SetVodDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'SSLProtocol' => 'on',
|
||||
'SSLPub' => $fullchain,
|
||||
'SSLPri' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('视频点播域名 ' . $domain . ' 部署证书成功!');
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetVodDomainCertificate',
|
||||
'DomainName' => $domain,
|
||||
'SSLProtocol' => 'on',
|
||||
'SSLPub' => $fullchain,
|
||||
'SSLPri' => $privatekey,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('视频点播域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_fc($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$fc_cname = $config['fc_cname'];
|
||||
if (empty($domain)) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
@@ -452,41 +569,42 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2023-03-30', $this->proxy);
|
||||
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'authConfig' => $data['authConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'authConfig' => $data['authConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_fc2($fullchain, $privatekey, $config)
|
||||
{
|
||||
$domain = $config['domain'];
|
||||
$fc_cname = $config['fc_cname'];
|
||||
if (empty($domain)) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($config['domain'])) throw new Exception('函数计算域名不能为空');
|
||||
if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空');
|
||||
|
||||
$certInfo = openssl_x509_parse($fullchain, true);
|
||||
@@ -495,33 +613,35 @@ class aliyun implements DeployInterface
|
||||
|
||||
$client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2021-04-06', $this->proxy);
|
||||
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
try {
|
||||
$data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取绑定域名信息失败:' . $e->getMessage());
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
$this->log('获取函数计算绑定域名信息成功');
|
||||
|
||||
if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) {
|
||||
$this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
|
||||
$data['certConfig']['certName'] = $cert_name;
|
||||
$data['certConfig']['certificate'] = $fullchain;
|
||||
$data['certConfig']['privateKey'] = $privatekey;
|
||||
|
||||
$param = [
|
||||
'protocol' => $data['protocol'],
|
||||
'routeConfig' => $data['routeConfig'],
|
||||
'certConfig' => $data['certConfig'],
|
||||
'tlsConfig' => $data['tlsConfig'],
|
||||
'wafConfig' => $data['wafConfig'],
|
||||
];
|
||||
$client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param);
|
||||
|
||||
$this->log('函数计算域名 ' . $domain . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
private function deploy_clb($cert_id, $cert_name, $config)
|
||||
@@ -721,6 +841,84 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_ga($cert_id, $config)
|
||||
{
|
||||
if (empty($config['ga_id'])) throw new Exception('全球加速实例ID不能为空');
|
||||
if (empty($config['ga_listener_id'])) throw new Exception('全球加速监听ID不能为空');
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'ga.cn-hangzhou.aliyuncs.com', '2019-11-20', $this->proxy);
|
||||
$cert_id = $cert_id . '-cn-hangzhou';
|
||||
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
|
||||
|
||||
if ($deploy_type == 1) {
|
||||
if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空');
|
||||
$param = [
|
||||
'Action' => 'ListListenerCertificates',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('扩展域名列表查询失败:' . $e->getMessage());
|
||||
}
|
||||
$need_add = [];
|
||||
foreach (explode(',', $config['clb_domain']) as $domain) {
|
||||
$domainExists = false;
|
||||
$exist_cert_id = null;
|
||||
foreach ($data['Certificates'] as $cert) {
|
||||
if (isset($cert['Domain']) && $domain == $cert['Domain']) {
|
||||
$domainExists = true;
|
||||
$exist_cert_id = $cert['CertificateId'];
|
||||
}
|
||||
}
|
||||
if ($domainExists) {
|
||||
if ($exist_cert_id == $cert_id) {
|
||||
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 证书已配置');
|
||||
continue;
|
||||
}
|
||||
$param = [
|
||||
'Action' => 'UpdateAdditionalCertificateWithListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
'Domain' => $domain,
|
||||
'CertificateId' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 替换证书成功!');
|
||||
} else {
|
||||
$need_add[] = $domain;
|
||||
}
|
||||
}
|
||||
if (count($need_add) > 0) {
|
||||
$param = [
|
||||
'Action' => 'AssociateAdditionalCertificatesWithListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
];
|
||||
foreach ($need_add as $index => $domain) {
|
||||
$param['Certificates.' . ($index + 1) . '.Id'] = $cert_id;
|
||||
$param['Certificates.' . ($index + 1) . '.Domain'] = $domain;
|
||||
}
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听扩展域名 ' . implode(',', $need_add) . ' 绑定证书成功!');
|
||||
}
|
||||
} else {
|
||||
$param = [
|
||||
'Action' => 'UpdateListener',
|
||||
'RegionId' => 'cn-hangzhou',
|
||||
'AcceleratorId' => $config['ga_id'],
|
||||
'ListenerId' => $config['ga_listener_id'],
|
||||
'Certificates.1.Id' => $cert_id,
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('全球加速实例监听默认证书更新成功!');
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
|
||||
108
app/lib/deploy/amh.php
Normal file
108
app/lib/deploy/amh.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class amh implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $apikey;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/');
|
||||
$this->apikey = $config['apikey'];
|
||||
$this->proxy = $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥');
|
||||
$this->login();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function login()
|
||||
{
|
||||
$path = '/?c=amapi&a=login';
|
||||
$post_data = 'amapi_expires=' . time() + 120;
|
||||
$post_data .= '&amapi_sign=' . hash_hmac('sha256', $post_data, $this->apikey);
|
||||
$response = $this->request($path, $post_data);
|
||||
if ($response['code'] == 302 && strpos($response['redirect_url'], 'amh_token=') !== false) {
|
||||
if(preg_match('/amh_token=([A-Za-z0-9]+)/', $response['redirect_url'], $matches)) {
|
||||
return $matches[1];
|
||||
}else{
|
||||
throw new Exception('面板返回数据异常');
|
||||
}
|
||||
} elseif ($response['code'] == 200 && preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
|
||||
throw new Exception(strip_tags($matches[1]));
|
||||
} else {
|
||||
throw new Exception('面板地址无法连接');
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
if (empty($config['env_name'])) throw new Exception('环境名称不能为空');
|
||||
if (empty($config['vhost_name'])) throw new Exception('网站标识域名不能为空');
|
||||
|
||||
$amh_token = $this->login();
|
||||
|
||||
foreach (explode("\n", $config['vhost_name']) as $vhost_name) {
|
||||
$vhost_name = trim($vhost_name);
|
||||
if (empty($vhost_name)) continue;
|
||||
|
||||
$path = '/?c=amssl&a=admin_amssl&envs_name=' . $config['env_name'] . '&vhost_name=' . $vhost_name . '&ModuleSort=app';
|
||||
$params = [
|
||||
'submit_key_crt' => 'y',
|
||||
'key_input1' => 'key_input1',
|
||||
'key_content1' => $privatekey,
|
||||
'crt_input1' => 'crt_input1',
|
||||
'crt_content1' => $fullchain,
|
||||
'amh_token' => $amh_token,
|
||||
];
|
||||
$response = $this->request($path, $params);
|
||||
if (strpos($response['body'], '<p id="success"') !== false) {
|
||||
$this->log("网站 {$vhost_name} 证书部署成功");
|
||||
} elseif (preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
|
||||
$errmsg = strip_tags($matches[1]);
|
||||
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
|
||||
throw new Exception($errmsg);
|
||||
} elseif (preg_match('/<p id="error".*?>(.*?)<br \/>/s', $response['body'], $matches)) {
|
||||
$errmsg = $matches[1];
|
||||
if (strpos($errmsg, '<br />') !== false) {
|
||||
$errmsg = explode('<br />', $errmsg)[0];
|
||||
}
|
||||
$errmsg = strip_tags($errmsg);
|
||||
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
|
||||
throw new Exception($errmsg);
|
||||
} else {
|
||||
throw new Exception("网站 {$vhost_name} 证书部署失败:未知错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function request($path, $post_data = null)
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
$cookie = 'PHPSESSID=' . hash_hmac('md5', 'php_sessid=' . $this->apikey, $this->apikey);
|
||||
$response = http_request($url, $post_data, null, $cookie, null, $this->proxy);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
|
||||
604
app/service/CloudflareEnhanceService.php
Normal file
604
app/service/CloudflareEnhanceService.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CloudflareEnhanceService
|
||||
{
|
||||
private string $email = '';
|
||||
private string $apiKey = '';
|
||||
private int $auth = 0;
|
||||
private bool $proxy = false;
|
||||
private string $accountId = '';
|
||||
private string $baseUrl = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->email = trim((string)($config['email'] ?? ''));
|
||||
$this->apiKey = preg_replace('/\s+/', '', trim((string)($config['apikey'] ?? '')));
|
||||
$this->auth = isset($config['auth']) ? intval($config['auth']) : (preg_match('/^[0-9a-f]+$/i', $this->apiKey) ? 0 : 1);
|
||||
$this->proxy = isset($config['proxy']) && strval($config['proxy']) === '1';
|
||||
$this->accountId = trim((string)($config['account_id'] ?? ''));
|
||||
}
|
||||
|
||||
public function isApiTokenAuth(): bool
|
||||
{
|
||||
return $this->auth === 1;
|
||||
}
|
||||
|
||||
public function getConfiguredAccountId(): string
|
||||
{
|
||||
return $this->accountId;
|
||||
}
|
||||
|
||||
public function getAccounts(): array
|
||||
{
|
||||
try {
|
||||
return $this->paginate('/accounts', [], 50);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取账户列表', $e, 'Account:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getDefaultAccountId(): string
|
||||
{
|
||||
try {
|
||||
$accounts = $this->getAccounts();
|
||||
if (!empty($accounts[0]['id'])) {
|
||||
return trim((string)$accounts[0]['id']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones', ['page' => 1, 'per_page' => 1]);
|
||||
$first = $payload['result'][0] ?? [];
|
||||
$accountId = trim((string)($first['account']['id'] ?? ''));
|
||||
if ($accountId !== '') {
|
||||
return $accountId;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getZone(string $zoneId): array
|
||||
{
|
||||
try {
|
||||
return $this->requestResult('GET', '/zones/' . $zoneId);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取域名详情', $e, 'Zone:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function listCustomHostnames(string $zoneId): array
|
||||
{
|
||||
try {
|
||||
return $this->paginate('/zones/' . $zoneId . '/custom_hostnames', [], 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取自定义主机名列表', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomHostname(string $zoneId, string $hostnameId): array
|
||||
{
|
||||
try {
|
||||
return $this->requestResult('GET', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取自定义主机名详情', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array
|
||||
{
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$payload = [
|
||||
'hostname' => $hostname,
|
||||
'ssl' => [
|
||||
'method' => 'http',
|
||||
'type' => 'dv',
|
||||
],
|
||||
];
|
||||
$origin = trim((string)$customOriginServer);
|
||||
if ($origin !== '') {
|
||||
$payload['custom_origin_server'] = $this->normalizeHostname($origin);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/zones/' . $zoneId . '/custom_hostnames', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCustomHostname(string $zoneId, string $hostnameId, array $payload): array
|
||||
{
|
||||
if (isset($payload['custom_origin_server']) && $payload['custom_origin_server'] !== null) {
|
||||
$payload['custom_origin_server'] = $this->normalizeHostname($payload['custom_origin_server']);
|
||||
}
|
||||
if (isset($payload['hostname']) && $payload['hostname'] !== null) {
|
||||
$payload['hostname'] = $this->normalizeHostname($payload['hostname']);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('PATCH', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId), [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCustomHostname(string $zoneId, string $hostnameId): bool
|
||||
{
|
||||
try {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/custom_hostnames/' . $hostnameId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function getFallbackOrigin(string $zoneId): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], null, true);
|
||||
if ($result === null) {
|
||||
return '';
|
||||
}
|
||||
return trim((string)($result['origin'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() === 404) {
|
||||
return '';
|
||||
}
|
||||
$this->throwActionError('获取 Fallback Origin', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateFallbackOrigin(string $zoneId, string $origin): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('PUT', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], [
|
||||
'origin' => $this->normalizeHostname($origin),
|
||||
]);
|
||||
return trim((string)($result['origin'] ?? $origin));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新 Fallback Origin', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFallbackOrigin(string $zoneId): bool
|
||||
{
|
||||
try {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/custom_hostnames/fallback_origin', [], null, true);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() === 404) {
|
||||
return true;
|
||||
}
|
||||
$this->throwActionError('删除 Fallback Origin', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listTunnels(string $accountId): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/cfd_tunnel', ['is_deleted' => 'false'], 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel 列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createTunnel(string $accountId, string $name): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/cfd_tunnel', [], [
|
||||
'name' => trim($name),
|
||||
'tunnel_secret' => base64_encode(random_bytes(32)),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建 Tunnel', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTunnel(string $accountId, string $tunnelId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 Tunnel', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function getTunnelToken(string $accountId, string $tunnelId): string
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/token');
|
||||
if (is_string($result)) {
|
||||
return $result;
|
||||
}
|
||||
return trim((string)($result['token'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel Token', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function getTunnelConfig(string $accountId, string $tunnelId): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/configurations', [], null, true);
|
||||
return is_array($result) ? $result : [];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 Tunnel 配置', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateTunnelConfig(string $accountId, string $tunnelId, array $config): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
return $this->requestResult('PUT', '/accounts/' . $accountId . '/cfd_tunnel/' . $tunnelId . '/configurations', [], [
|
||||
'config' => $config,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新 Tunnel 配置', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listCidrRoutes(string $accountId, ?string $tunnelId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$query = ['is_deleted' => 'false'];
|
||||
if (!empty($tunnelId)) {
|
||||
$query['tunnel_id'] = $tunnelId;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/teamnet/routes', $query, 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 CIDR 路由列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createCidrRoute(string $accountId, string $tunnelId, string $network, ?string $comment = null, ?string $virtualNetworkId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$payload = [
|
||||
'network' => trim($network),
|
||||
'tunnel_id' => trim($tunnelId),
|
||||
];
|
||||
if (!empty($comment)) {
|
||||
$payload['comment'] = trim($comment);
|
||||
}
|
||||
if (!empty($virtualNetworkId)) {
|
||||
$payload['virtual_network_id'] = trim($virtualNetworkId);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/teamnet/routes', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建 CIDR 路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCidrRoute(string $accountId, string $routeId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/teamnet/routes/' . $routeId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 CIDR 路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function listHostnameRoutes(string $accountId, ?string $tunnelId = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$query = ['is_deleted' => 'false'];
|
||||
if (!empty($tunnelId)) {
|
||||
$query['tunnel_id'] = $tunnelId;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->paginate('/accounts/' . $accountId . '/zerotrust/routes/hostname', $query, 100);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取主机名路由列表', $e, 'Cloudflare Tunnel:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createHostnameRoute(string $accountId, string $tunnelId, string $hostname, ?string $comment = null): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
$payload = [
|
||||
'hostname' => $this->normalizeHostname($hostname),
|
||||
'tunnel_id' => trim($tunnelId),
|
||||
];
|
||||
if (!empty($comment)) {
|
||||
$payload['comment'] = trim($comment);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('POST', '/accounts/' . $accountId . '/zerotrust/routes/hostname', [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('创建主机名路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteHostnameRoute(string $accountId, string $routeId): bool
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
try {
|
||||
$this->requestResult('DELETE', '/accounts/' . $accountId . '/zerotrust/routes/hostname/' . $routeId);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除主机名路由', $e, 'Cloudflare Tunnel:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function upsertTunnelCnameRecord(string $zoneId, string $hostname, string $tunnelId): array
|
||||
{
|
||||
$zoneId = trim($zoneId);
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$target = trim($tunnelId) . '.cfargotunnel.com';
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'type' => 'CNAME',
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$records = $payload['result'] ?? [];
|
||||
|
||||
$allByNamePayload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$allByName = $allByNamePayload['result'] ?? [];
|
||||
$otherTypes = [];
|
||||
foreach ($allByName as $row) {
|
||||
$type = strtoupper((string)($row['type'] ?? ''));
|
||||
$name = $this->normalizeHostname($row['name'] ?? '');
|
||||
if ($name === $hostname && $type !== 'CNAME') {
|
||||
$otherTypes[] = $type;
|
||||
}
|
||||
}
|
||||
if (!empty($otherTypes)) {
|
||||
$otherTypes = array_unique(array_filter($otherTypes));
|
||||
throw new Exception('主机名已存在非 CNAME 记录(' . implode(', ', $otherTypes) . '),无法同步 Tunnel CNAME', 400);
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
$name = $this->normalizeHostname($record['name'] ?? '');
|
||||
if ($name !== $hostname) {
|
||||
continue;
|
||||
}
|
||||
$content = $this->normalizeHostname($record['content'] ?? '');
|
||||
$proxied = !empty($record['proxied']);
|
||||
if ($content === $this->normalizeHostname($target) && $proxied) {
|
||||
return ['action' => 'unchanged'];
|
||||
}
|
||||
|
||||
$this->requestResult('PUT', '/zones/' . $zoneId . '/dns_records/' . $record['id'], [], [
|
||||
'type' => 'CNAME',
|
||||
'name' => $hostname,
|
||||
'content' => $target,
|
||||
'proxied' => true,
|
||||
'ttl' => 1,
|
||||
]);
|
||||
return ['action' => 'updated'];
|
||||
}
|
||||
|
||||
$this->requestResult('POST', '/zones/' . $zoneId . '/dns_records', [], [
|
||||
'type' => 'CNAME',
|
||||
'name' => $hostname,
|
||||
'content' => $target,
|
||||
'proxied' => true,
|
||||
'ttl' => 1,
|
||||
]);
|
||||
return ['action' => 'created'];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('同步 Tunnel CNAME 记录', $e, 'Zone:DNS:Edit');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTunnelCnameRecordIfMatch(string $zoneId, string $hostname, string $tunnelId): array
|
||||
{
|
||||
$zoneId = trim($zoneId);
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$target = $this->normalizeHostname(trim($tunnelId) . '.cfargotunnel.com');
|
||||
|
||||
try {
|
||||
$payload = $this->requestRaw('GET', '/zones/' . $zoneId . '/dns_records', [
|
||||
'name' => $hostname,
|
||||
'type' => 'CNAME',
|
||||
'page' => 1,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
$records = $payload['result'] ?? [];
|
||||
foreach ($records as $record) {
|
||||
$name = $this->normalizeHostname($record['name'] ?? '');
|
||||
$content = $this->normalizeHostname($record['content'] ?? '');
|
||||
if ($name === $hostname && $content === $target) {
|
||||
$this->requestResult('DELETE', '/zones/' . $zoneId . '/dns_records/' . $record['id']);
|
||||
return ['deleted' => true];
|
||||
}
|
||||
}
|
||||
return ['deleted' => false];
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('删除 Tunnel CNAME 记录', $e, 'Zone:DNS:Edit');
|
||||
}
|
||||
}
|
||||
|
||||
private function paginate(string $path, array $query = [], int $perPage = 100): array
|
||||
{
|
||||
$all = [];
|
||||
$page = 1;
|
||||
$maxPage = 200;
|
||||
while ($page <= $maxPage) {
|
||||
$payload = $this->requestRaw('GET', $path, array_merge($query, [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
]));
|
||||
$batch = $payload['result'] ?? [];
|
||||
if (!is_array($batch)) {
|
||||
$batch = [];
|
||||
}
|
||||
foreach ($batch as $item) {
|
||||
$all[] = $item;
|
||||
}
|
||||
|
||||
$totalPages = intval($payload['result_info']['total_pages'] ?? 0);
|
||||
if ($totalPages > 0) {
|
||||
if ($page >= $totalPages) {
|
||||
break;
|
||||
}
|
||||
} elseif (count($batch) < $perPage || empty($batch)) {
|
||||
break;
|
||||
}
|
||||
$page++;
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function requestResult(string $method, string $path, array $query = [], ?array $body = null, bool $allowNotFound = false)
|
||||
{
|
||||
$payload = $this->requestRaw($method, $path, $query, $body, $allowNotFound);
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
return $payload['result'] ?? [];
|
||||
}
|
||||
|
||||
private function requestRaw(string $method, string $path, array $query = [], ?array $body = null, bool $allowNotFound = false): ?array
|
||||
{
|
||||
$headers = $this->buildHeaders($body !== null);
|
||||
$url = $this->baseUrl . $path;
|
||||
if (!empty($query)) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$response = http_request(
|
||||
$url,
|
||||
$body,
|
||||
null,
|
||||
null,
|
||||
$headers,
|
||||
$this->proxy,
|
||||
strtoupper($method),
|
||||
20
|
||||
);
|
||||
|
||||
$status = intval($response['code'] ?? 0);
|
||||
if ($allowNotFound && $status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = json_decode($response['body'] ?? '', true);
|
||||
if (!is_array($payload)) {
|
||||
throw new Exception('Cloudflare 返回数据解析失败', $status > 0 ? $status : 502);
|
||||
}
|
||||
|
||||
if (($payload['success'] ?? false) !== true) {
|
||||
if ($allowNotFound && $status === 404) {
|
||||
return null;
|
||||
}
|
||||
$message = $this->extractErrorMessage($payload);
|
||||
throw new Exception($message !== '' ? $message : 'Cloudflare API 请求失败', $status > 0 ? $status : 400);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function buildHeaders(bool $json = false): array
|
||||
{
|
||||
if ($this->apiKey === '') {
|
||||
throw new Exception('Cloudflare API 凭证为空', 400);
|
||||
}
|
||||
|
||||
if ($this->auth === 1) {
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $this->apiKey,
|
||||
];
|
||||
} else {
|
||||
if ($this->email === '') {
|
||||
throw new Exception('当前 Cloudflare 账户缺少邮箱地址,旧版 API Key 认证需要填写邮箱', 400);
|
||||
}
|
||||
$headers = [
|
||||
'X-Auth-Email' => $this->email,
|
||||
'X-Auth-Key' => $this->apiKey,
|
||||
];
|
||||
}
|
||||
|
||||
if ($json) {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function assertTunnelSupported(): void
|
||||
{
|
||||
if (!$this->isApiTokenAuth()) {
|
||||
throw new Exception('Cloudflare Tunnels 仅支持 API 令牌认证,请将当前账户的认证方式切换为 API令牌', 400);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeHostname($hostname): string
|
||||
{
|
||||
$hostname = trim((string)$hostname);
|
||||
if ($hostname === '') {
|
||||
return '';
|
||||
}
|
||||
$hostname = rtrim($hostname, '.');
|
||||
$hostname = convertDomainToAscii($hostname);
|
||||
return strtolower($hostname);
|
||||
}
|
||||
|
||||
private function extractErrorMessage(array $payload): string
|
||||
{
|
||||
if (!empty($payload['errors'][0]['message'])) {
|
||||
return trim((string)$payload['errors'][0]['message']);
|
||||
}
|
||||
if (!empty($payload['messages'][0]['message'])) {
|
||||
return trim((string)$payload['messages'][0]['message']);
|
||||
}
|
||||
if (!empty($payload['result']['message'])) {
|
||||
return trim((string)$payload['result']['message']);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function throwActionError(string $action, Exception $e, string $permissionHint = ''): void
|
||||
{
|
||||
$status = intval($e->getCode());
|
||||
$message = trim($e->getMessage());
|
||||
|
||||
if ($status === 401) {
|
||||
$message = 'Cloudflare 凭证无效或已过期,无法' . $action;
|
||||
} elseif ($status === 403) {
|
||||
$message = 'Cloudflare 权限不足,无法' . $action;
|
||||
if ($permissionHint !== '') {
|
||||
$message .= '。请确认 Token 具备 ' . $permissionHint . ' 权限';
|
||||
}
|
||||
} elseif ($status === 404 && $message === '') {
|
||||
$message = $action . '失败:资源不存在';
|
||||
} elseif ($status === 429) {
|
||||
$message = 'Cloudflare API 请求过于频繁,暂时无法' . $action . ',请稍后重试';
|
||||
} elseif ($status >= 500) {
|
||||
$message = 'Cloudflare 服务暂时不可用,无法' . $action . ',请稍后重试';
|
||||
} elseif ($message === '') {
|
||||
$message = $action . '失败';
|
||||
}
|
||||
|
||||
throw new Exception($message, $status > 0 ? $status : 400);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class ExpireNoticeService
|
||||
|
||||
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', '1048');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
|
||||
@@ -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`)
|
||||
@@ -253,4 +251,14 @@ CREATE TABLE `dnsmgr_sctask` (
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_domain_alias`;
|
||||
CREATE TABLE `dnsmgr_domain_alias` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`did` int(11) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`),
|
||||
KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -185,4 +185,17 @@ CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` (
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`)
|
||||
) 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;
|
||||
@@ -42,12 +42,14 @@ class DnsQueryUtils
|
||||
$id = array_rand(self::$doh_servers);
|
||||
$url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
|
||||
$data = get_curl($url);
|
||||
if (!$data) return false;
|
||||
$arr = json_decode($data, true);
|
||||
if (!$arr) {
|
||||
unset(self::$doh_servers[$id]);
|
||||
$id = array_rand(self::$doh_servers);
|
||||
$url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
|
||||
$data = get_curl($url);
|
||||
if (!$data) return false;
|
||||
$arr = json_decode($data, true);
|
||||
if (!$arr) return false;
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@ class MsgNotice
|
||||
$url = 'https://wxpusher.zjiecode.com/api/send/message';
|
||||
$post = ['appToken' => $wechat_apptoken, 'content' => $content, 'summary' => $title, 'contentType' => 3, 'uids' => [$wechat_appuid]];
|
||||
$result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']);
|
||||
if (!$result) return '请求失败';
|
||||
$arr = json_decode($result, true);
|
||||
if (isset($arr['success']) && $arr['success'] == true) {
|
||||
return true;
|
||||
@@ -246,6 +247,7 @@ class MsgNotice
|
||||
$url = $tgbot_url.'/bot'.$tgbot_token.'/sendMessage';
|
||||
$post = ['chat_id' => $tgbot_chatid, 'text' => $content, 'parse_mode' => 'HTML'];
|
||||
$result = self::telegram_curl($url, http_build_query($post));
|
||||
if (!$result) return '请求失败';
|
||||
$arr = json_decode($result, true);
|
||||
if (isset($arr['ok']) && $arr['ok'] == true) {
|
||||
return true;
|
||||
@@ -257,6 +259,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 +270,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 +287,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;
|
||||
|
||||
@@ -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){
|
||||
|
||||
690
app/view/cloudflare/hostnames.html
Normal file
690
app/view/cloudflare/hostnames.html
Normal file
@@ -0,0 +1,690 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}Cloudflare增强 - {$domainName}{/block}
|
||||
{block name="main"}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float:none;">
|
||||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
<div class="clearfix">
|
||||
<div class="pull-right" style="margin-top:-6px;max-width:100%;">
|
||||
<a href="/record/{$domainId}" class="btn btn-sm btn-default" style="vertical-align:middle;"><i class="fa fa-reply fa-fw"></i> 返回解析</a>
|
||||
</div>
|
||||
<h3 class="panel-title" style="padding-top:4px;">Cloudflare增强 - {$domainName}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>说明:</strong> 这里管理 Cloudflare 自定义主机名、证书状态、证书校验与 Fallback Origin。
|
||||
</div>
|
||||
|
||||
<div class="well well-sm">
|
||||
<div class="form-inline">
|
||||
<div class="form-group" style="width:70%;max-width:720px;">
|
||||
<label>Fallback Origin</label>
|
||||
<input type="text" id="fallbackOrigin" class="form-control" style="width:80%;" placeholder="例如 origin.example.com">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveFallbackOrigin()">保存</button>
|
||||
<button type="button" class="btn btn-default" onclick="loadFallbackOrigin()">刷新</button>
|
||||
<button type="button" class="btn btn-danger" onclick="clearFallbackOrigin()">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom:5px;">
|
||||
<div class="pull-left">
|
||||
<a href="javascript:refreshHostnameList()" class="btn btn-default" title="刷新自定义主机名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openAddDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 添加自定义主机名</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-store" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="storeTitle">添加自定义主机名</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<input type="hidden" name="hostname_id" value="">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">主机名</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="例如 app.example.com 或 *.example.com" required>
|
||||
<p class="help-block" id="hostnameHint">创建后主机名不能直接改名,如需改名请删除后重建。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义源站</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||||
<p class="help-block">留空表示清空当前自定义源站,回退到 Fallback Origin 或默认源站逻辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitHostname()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-verification" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="verificationTitle">证书校验</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="verificationContent"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="refreshHostnameValidation()">刷新校验</button>
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js?v=1005"></script>
|
||||
<script>
|
||||
var currentVerificationHostnameId = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#form-store").bootstrapValidator();
|
||||
loadFallbackOrigin();
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/hostnames/data/{$domainId}',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: hostnameResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: '主机名'},
|
||||
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
||||
{field: 'ssl_validation_status', title: '证书校验', formatter: formatStatus},
|
||||
{field: 'verification_status', title: '所有权校验', formatter: formatStatus},
|
||||
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return ''
|
||||
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
|
||||
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">校验</a> '
|
||||
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function hostnameResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshHostnameList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function formatStatus(value){
|
||||
var v = String(value || '').toLowerCase();
|
||||
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
if(v && v !== '-'){
|
||||
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function getHostnameRow(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
if(!row){
|
||||
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
||||
return null;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function resetHostnameForm(){
|
||||
$("#form-store")[0].reset();
|
||||
$("#form-store input[name=hostname_id]").val('');
|
||||
$("#form-store input[name=hostname]").prop('readonly', false);
|
||||
$("#form-store").data("bootstrapValidator").resetForm(true);
|
||||
}
|
||||
|
||||
function openAddDialog(){
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('添加自定义主机名');
|
||||
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function openEditDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('编辑自定义主机名');
|
||||
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
||||
$("#form-store input[name=hostname_id]").val(row.id);
|
||||
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
||||
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function submitHostname(){
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
||||
var url = hostnameId ? '/cloudflare/hostnames/update/{$domainId}' : '/cloudflare/hostnames/add/{$domainId}';
|
||||
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: $("#form-store").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-store").modal('hide');
|
||||
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openVerificationDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
currentVerificationHostnameId = id;
|
||||
renderVerificationDialog(row);
|
||||
$("#modal-verification").modal('show');
|
||||
}
|
||||
|
||||
function refreshHostnameValidation(){
|
||||
if(!currentVerificationHostnameId){
|
||||
layer.msg('请先选择自定义主机名');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/refresh/{$domainId}',
|
||||
data: {hostname_id: currentVerificationHostnameId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
renderVerificationDialog(res.data);
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderVerificationDialog(row){
|
||||
$("#verificationTitle").text('证书校验 - ' + row.hostname);
|
||||
var html = '';
|
||||
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新校验”会重新向 Cloudflare 发起一次校验。</div>';
|
||||
html += '<div class="row">';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书校验', formatStatusText(row.ssl_validation_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('所有权校验', formatStatusText(row.verification_status))+'</div>';
|
||||
html += '</div>';
|
||||
|
||||
var ownership = row.ownership_verification || {};
|
||||
if(ownership.name || ownership.value){
|
||||
html += renderSection('所有权 TXT 校验',
|
||||
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
||||
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
||||
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
||||
+ renderQuickAddTxtButton(ownership.name || '', ownership.value || '', '快速添加所有权 TXT')
|
||||
);
|
||||
}
|
||||
|
||||
var ownershipHttp = row.ownership_verification_http || {};
|
||||
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
||||
html += renderSection('所有权 HTTP 校验',
|
||||
renderCopyTextarea('HTTP URL', ownershipHttp.http_url || '', true, 2)
|
||||
+ renderCopyTextarea('HTTP Body', ownershipHttp.http_body || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||||
if(records.length > 0){
|
||||
var recordsHtml = '';
|
||||
for(var i = 0; i < records.length; i++){
|
||||
var item = records[i] || {};
|
||||
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
||||
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
||||
recordsHtml += '<div class="panel-heading"><strong>证书校验记录 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
||||
recordsHtml += '<div class="panel-body">';
|
||||
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
||||
recordsHtml += renderQuickAddTxtButton(item.txt_name || '', item.txt_value || '', '快速添加 TXT');
|
||||
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP URL', item.http_url || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP Body', item.http_body || '', true, 3);
|
||||
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
||||
recordsHtml += '</div></div>';
|
||||
}
|
||||
html += renderSection('证书校验记录', recordsHtml);
|
||||
}else{
|
||||
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书校验记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新校验”或稍后刷新列表。</div>';
|
||||
}
|
||||
|
||||
if(row.validation_errors){
|
||||
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
||||
}
|
||||
|
||||
$("#verificationContent").html(html);
|
||||
}
|
||||
|
||||
function renderSummaryCard(title, value){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
||||
}
|
||||
|
||||
function renderSection(title, body){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function renderCopyInput(label, value, copyable){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
if(copyable){
|
||||
html += '<div class="input-group">';
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
||||
html += '</div>';
|
||||
}else{
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCopyTextarea(label, value, copyable, rows){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
||||
if(copyable){
|
||||
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderQuickAddTxtButton(name, value, label){
|
||||
var txtName = String(name || '').trim();
|
||||
var txtValue = String(value || '').trim();
|
||||
if(!txtName || !txtValue){
|
||||
return '';
|
||||
}
|
||||
return '<div class="text-right" style="margin-top:8px;margin-bottom:12px;"><button type="button" class="btn btn-success btn-xs" data-name="' + encodeURIComponent(txtName) + '" data-value="' + encodeURIComponent(txtValue) + '" onclick="quickAddTxtRecord(this)">' + htmlEscape(label || '快速添加 TXT') + '</button></div>';
|
||||
}
|
||||
|
||||
function formatStatusText(value){
|
||||
var text = value || '-';
|
||||
if(text === '-'){
|
||||
return '<span class="text-muted">-</span>';
|
||||
}
|
||||
return formatStatus(text);
|
||||
}
|
||||
|
||||
function copyEncodedValue(btn){
|
||||
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
||||
}
|
||||
|
||||
function copyText(text){
|
||||
var value = String(text || '');
|
||||
if(!value){
|
||||
layer.msg('没有可复制的内容');
|
||||
return;
|
||||
}
|
||||
if(navigator.clipboard && window.isSecureContext){
|
||||
navigator.clipboard.writeText(value).then(function(){
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}).catch(function(){
|
||||
fallbackCopyText(value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopyText(value);
|
||||
}
|
||||
|
||||
function fallbackCopyText(text){
|
||||
var $temp = $('<textarea readonly></textarea>');
|
||||
$('body').append($temp);
|
||||
$temp.val(text).select();
|
||||
try{
|
||||
document.execCommand('copy');
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}catch(e){
|
||||
layer.alert('复制失败,请手动复制', {icon: 2});
|
||||
}
|
||||
$temp.remove();
|
||||
}
|
||||
|
||||
function quickAddTxtRecord(btn){
|
||||
var fullName = decodeURIComponent($(btn).attr('data-name') || '');
|
||||
var value = decodeURIComponent($(btn).attr('data-value') || '');
|
||||
resolveTxtRecordTargets(fullName, function(targets){
|
||||
if(!targets.length){
|
||||
layer.alert('系统中未找到与该 TXT 主机名对应的托管域名,请手动到解析页添加', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if(targets.length === 1){
|
||||
confirmQuickAddTxtRecord(fullName, value, targets[0]);
|
||||
return;
|
||||
}
|
||||
openTxtTargetPicker(fullName, value, targets);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTxtRecordTargets(fullName, callback){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/txttargets/{$domainId}',
|
||||
data: {hostname: fullName},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
var targets = res.data && $.isArray(res.data.candidates) ? res.data.candidates : [];
|
||||
callback(targets);
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openTxtTargetPicker(fullName, value, targets){
|
||||
var html = '<div style="padding:16px 18px 6px;">';
|
||||
html += '<div class="alert alert-warning" style="margin-bottom:12px;">检测到多个可用解析域名,请确认要写入哪个服务商。</div>';
|
||||
html += '<div class="form-group"><label>TXT 主机名</label><div><code>' + htmlEscape(fullName) + '</code></div></div>';
|
||||
html += '<div class="form-group"><label>TXT 值</label><textarea class="form-control" rows="3" readonly>' + htmlEscape(value) + '</textarea></div>';
|
||||
html += '<form id="txtTargetPickerForm">';
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var target = targets[i] || {};
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
html += '<div class="radio" style="margin:0 0 12px;border:1px solid #e5e5e5;border-radius:4px;padding:10px 12px;">';
|
||||
html += '<label style="display:block;padding-left:22px;">';
|
||||
html += '<input type="radio" name="txtTarget" value="' + htmlEscape(String(target.domain_id || '')) + '"' + (i === 0 ? ' checked' : '') + '>';
|
||||
html += '<strong>' + htmlEscape(target.domain_name || '-') + '</strong>';
|
||||
if(target.is_current_domain){
|
||||
html += ' <span class="label label-primary">当前页</span>';
|
||||
}
|
||||
html += '<div class="help-block" style="margin:8px 0 0;">';
|
||||
html += '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>';
|
||||
html += '服务商:' + htmlEscape(providerName) + '<br>';
|
||||
html += '账户:' + htmlEscape(accountName);
|
||||
html += '</div>';
|
||||
html += '</label></div>';
|
||||
}
|
||||
html += '</form></div>';
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择解析服务商',
|
||||
area: ['640px', 'auto'],
|
||||
shadeClose: false,
|
||||
content: html,
|
||||
btn: ['添加 TXT', '取消'],
|
||||
yes: function(index){
|
||||
var selectedId = $('#txtTargetPickerForm input[name=txtTarget]:checked').val();
|
||||
var target = findTxtTargetByDomainId(targets, selectedId);
|
||||
if(!target){
|
||||
layer.msg('请选择要写入的解析域名', {icon: 2});
|
||||
return;
|
||||
}
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmQuickAddTxtRecord(fullName, value, target){
|
||||
layer.confirm(buildQuickAddConfirmHtml(fullName, target), {title: '提示', icon: 0}, function(index){
|
||||
layer.close(index);
|
||||
submitQuickAddTxtRecord(value, target);
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuickAddConfirmHtml(fullName, target){
|
||||
var providerName = target.account_type_name || target.account_type || '-';
|
||||
var accountName = target.account_display_name || ('账户#' + (target.account_id || ''));
|
||||
return '确定要快速添加 TXT 记录吗?<br><br>'
|
||||
+ 'TXT 主机名:<code>' + htmlEscape(fullName) + '</code><br>'
|
||||
+ '解析域名:<code>' + htmlEscape(target.domain_name || '-') + '</code><br>'
|
||||
+ '主机记录:<code>' + htmlEscape(target.record_name || '@') + '</code><br>'
|
||||
+ '服务商:' + htmlEscape(providerName) + '<br>'
|
||||
+ '账户:' + htmlEscape(accountName);
|
||||
}
|
||||
|
||||
function submitQuickAddTxtRecord(value, target){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/record/add/' + target.domain_id,
|
||||
data: {
|
||||
name: target.record_name,
|
||||
type: 'TXT',
|
||||
value: value,
|
||||
line: '0',
|
||||
ttl: 600,
|
||||
mx: 1,
|
||||
weight: 0,
|
||||
remark: 'Cloudflare证书校验'
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-verification").modal('show');
|
||||
layer.msg('TXT 记录已添加到 ' + (target.domain_name || '-'), {icon: 1, time: 1400});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function findTxtTargetByDomainId(targets, domainId){
|
||||
var selected = String(domainId || '');
|
||||
for(var i = 0; i < targets.length; i++){
|
||||
var item = targets[i] || {};
|
||||
if(String(item.domain_id || '') === selected){
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function deleteHostname(id, hostname){
|
||||
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/delete/{$domainId}',
|
||||
data: {hostname_id: id, hostname: hostname},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
refreshHostnameList();
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFallbackOrigin(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/get/{$domainId}',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val((res.data && res.data.origin) ? res.data.origin : '');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveFallbackOrigin(){
|
||||
var origin = $.trim($("#fallbackOrigin").val());
|
||||
if(!origin){
|
||||
layer.msg('请输入 Fallback Origin');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/set/{$domainId}',
|
||||
data: {origin: origin},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val(res.data.origin || origin);
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFallbackOrigin(){
|
||||
layer.confirm('确定要清空 Fallback Origin 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/delete/{$domainId}',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#fallbackOrigin").val('');
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
607
app/view/cloudflare/tunnels.html
Normal file
607
app/view/cloudflare/tunnels.html
Normal file
@@ -0,0 +1,607 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}Cloudflare Tunnels - {$accountName}{/block}
|
||||
{block name="main"}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float:none;">
|
||||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a href="/account" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回账户</a>
|
||||
Cloudflare Tunnels - {$accountName}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>Account ID:</strong>{$cfAccountId}
|
||||
<br>
|
||||
这里管理 Tunnel 列表、公网主机名、CIDR 路由和主机名路由。公网主机名会自动同步为对应域名下的 CNAME。
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom:15px;">
|
||||
<div class="pull-left">
|
||||
<a href="javascript:refreshTunnelList()" class="btn btn-default" title="刷新 Tunnel 列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openTunnelDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 创建 Tunnel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-tunnel" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title">创建 Tunnel</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-tunnel">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">名称</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="name" placeholder="例如 edge-prod" required>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitTunnel()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-token" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title">Tunnel Token</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Tunnel</label>
|
||||
<input type="text" class="form-control" id="tokenTunnelName" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Token</label>
|
||||
<textarea id="tokenValue" class="form-control" rows="4" readonly></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>启动命令</label>
|
||||
<textarea id="tokenCommand" class="form-control" rows="3" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" onclick="copyTokenCommand()">复制启动命令</button>
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-public" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="publicTitle">公网主机名</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-public">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="hostname,例如 app.example.com" style="width:240px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="service" placeholder="service,例如 http://127.0.0.1:8080" style="width:260px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="path" placeholder="可留空,例如 /api/*" style="width:180px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="savePublicHostname()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="publicTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-cidr" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="cidrTitle">CIDR 路由</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-cidr">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="network" placeholder="例如 10.10.0.0/16" style="width:220px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="comment" placeholder="备注,可留空" style="width:240px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveCidrRoute()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="cidrTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-hostname-route" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="hostnameRouteTitle">主机名路由</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline" id="form-hostname-route">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="例如 internal.example.com" style="width:260px;" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="comment" placeholder="备注,可留空" style="width:240px;">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="saveHostnameRoute()">保存</button>
|
||||
</form>
|
||||
<hr>
|
||||
<table id="hostnameRouteTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var selectedTunnelId = '';
|
||||
var selectedTunnelName = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#form-tunnel").bootstrapValidator();
|
||||
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/tunnels/data/{$accountId}',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'name', title: '名称'},
|
||||
{field: 'id', title: 'Tunnel ID'},
|
||||
{field: 'status', title: '状态', formatter: tunnelStatusFormatter},
|
||||
{field: 'connection_count', title: '连接数'},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return ''
|
||||
+ '<a href="javascript:showToken(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-info btn-xs">Token</a> '
|
||||
+ '<a href="javascript:openPublicHostnames(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-primary btn-xs">公网主机名</a> '
|
||||
+ '<a href="javascript:openCidrRoutes(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-warning btn-xs">CIDR</a> '
|
||||
+ '<a href="javascript:openHostnameRoutes(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-success btn-xs">主机名路由</a> '
|
||||
+ '<a href="javascript:deleteTunnel(\''+row.id+'\', \''+htmlEscape(row.name)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#publicTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'hostname',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: 'Hostname'},
|
||||
{field: 'path', title: 'Path', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'service', title: 'Service'},
|
||||
{field: 'zone_name', title: '匹配域名', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deletePublicHostname(\''+escapeJs(row.hostname)+'\', \''+escapeJs(row.path || '')+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#cidrTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'network', title: 'CIDR'},
|
||||
{field: 'comment', title: '备注', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deleteCidrRoute(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#hostnameRouteTable").bootstrapTable({
|
||||
method: 'post',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: tableResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: 'Hostname'},
|
||||
{field: 'comment', title: '备注', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'created_at', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deleteHostnameRoute(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function tableResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '请求失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshTunnelList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function tunnelStatusFormatter(value){
|
||||
var v = (value || '').toLowerCase();
|
||||
if(v === 'healthy' || v === 'active'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'inactive' || v === 'down' || v === 'degraded'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
return value ? '<span class="label label-default">'+htmlEscape(value)+'</span>' : '-';
|
||||
}
|
||||
|
||||
function openTunnelDialog(){
|
||||
$("#form-tunnel")[0].reset();
|
||||
$("#form-tunnel").data("bootstrapValidator").resetForm();
|
||||
$("#modal-tunnel").modal('show');
|
||||
}
|
||||
|
||||
function submitTunnel(){
|
||||
$("#form-tunnel").data("bootstrapValidator").validate();
|
||||
if(!$("#form-tunnel").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/add/{$accountId}',
|
||||
data: $("#form-tunnel").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-tunnel").modal('hide');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTunnel(tunnelId, tunnelName){
|
||||
layer.confirm('确定要删除 Tunnel '+tunnelName+' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/delete/{$accountId}',
|
||||
data: {tunnel_id: tunnelId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showToken(tunnelId, tunnelName){
|
||||
$("#tokenTunnelName").val(tunnelName + ' [' + tunnelId + ']');
|
||||
$("#tokenValue").val('');
|
||||
$("#tokenCommand").val('');
|
||||
$("#modal-token").modal('show');
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/token/{$accountId}',
|
||||
data: {tunnel_id: tunnelId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
var token = (res.data && res.data.token) ? res.data.token : '';
|
||||
$("#tokenValue").val(token);
|
||||
$("#tokenCommand").val('cloudflared tunnel run --token ' + token);
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyTokenCommand(){
|
||||
copyPlainText($("#tokenCommand").val());
|
||||
}
|
||||
|
||||
function openPublicHostnames(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#publicTitle").text('公网主机名 - ' + tunnelName);
|
||||
$("#form-public")[0].reset();
|
||||
$("#modal-public").modal('show');
|
||||
$("#publicTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/publichostnames/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function savePublicHostname(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-public").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/publichostnames/save/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#publicTable").bootstrapTable('refresh');
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletePublicHostname(hostname, path){
|
||||
layer.confirm('确定要删除公网主机名 '+hostname+' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/publichostnames/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, hostname: hostname, path: path},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-public").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#publicTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openCidrRoutes(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#cidrTitle").text('CIDR 路由 - ' + tunnelName);
|
||||
$("#form-cidr")[0].reset();
|
||||
$("#modal-cidr").modal('show');
|
||||
$("#cidrTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/cidr/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function saveCidrRoute(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-cidr").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/cidr/add/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#cidrTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCidrRoute(routeId){
|
||||
layer.confirm('确定要删除该 CIDR 路由吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/cidr/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, route_id: routeId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-cidr").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#cidrTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openHostnameRoutes(tunnelId, tunnelName){
|
||||
selectedTunnelId = tunnelId;
|
||||
selectedTunnelName = tunnelName;
|
||||
$("#hostnameRouteTitle").text('主机名路由 - ' + tunnelName);
|
||||
$("#form-hostname-route")[0].reset();
|
||||
$("#modal-hostname-route").modal('show');
|
||||
$("#hostnameRouteTable").bootstrapTable('refreshOptions', {
|
||||
url: '/cloudflare/tunnels/hostnameroutes/data/{$accountId}',
|
||||
queryParams: function(){ return {tunnel_id: selectedTunnelId}; }
|
||||
});
|
||||
}
|
||||
|
||||
function saveHostnameRoute(){
|
||||
if(!selectedTunnelId){
|
||||
layer.msg('请先选择 Tunnel');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
var data = $("#form-hostname-route").serializeArray();
|
||||
data.push({name: 'tunnel_id', value: selectedTunnelId});
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/hostnameroutes/add/{$accountId}',
|
||||
data: $.param(data),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#hostnameRouteTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteHostnameRoute(routeId){
|
||||
layer.confirm('确定要删除该主机名路由吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/tunnels/hostnameroutes/delete/{$accountId}',
|
||||
data: {tunnel_id: selectedTunnelId, route_id: routeId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#modal-hostname-route").modal('show');
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
$("#hostnameRouteTable").bootstrapTable('refresh');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPlainText(text){
|
||||
var temp = document.createElement('textarea');
|
||||
temp.style.position = 'absolute';
|
||||
temp.style.left = '-9999px';
|
||||
temp.value = text || '';
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(temp);
|
||||
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
|
||||
}
|
||||
|
||||
function escapeJs(str){
|
||||
return String(str || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
function htmlEscape(str){
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -110,7 +110,7 @@
|
||||
<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="treeview {:checkIfActive('overview,task,taskinfo,taskform')}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -127,6 +127,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="域名或备注">
|
||||
@@ -139,12 +141,15 @@
|
||||
<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('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('updateexpire')">刷新到期时间</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li></ul>
|
||||
</div>
|
||||
<a href="/domain/expirenotice" class="btn btn-default">到期提醒设置</a>{/if}
|
||||
</form>
|
||||
@@ -167,7 +172,7 @@
|
||||
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var userLevel = "{:request()->user['level']}";
|
||||
var userLevel = "{$user['level']|default=''}";
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
const defaultPageSize = getCookie('domain_pagesize') ? getCookie('domain_pagesize') : 15;
|
||||
@@ -193,7 +198,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+')');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -423,9 +428,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 +445,6 @@ function delItem(id) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function getDomainList(){
|
||||
@@ -512,9 +513,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 +531,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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
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,12 @@ $(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>';
|
||||
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 +514,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 +532,6 @@ function delItem(recordid) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function setRemark(recordid) {
|
||||
@@ -587,9 +586,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 +604,6 @@ function operation(action){
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
function batch_edit(records){
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -121,6 +121,10 @@
|
||||
<label class="col-sm-3 control-label">Webhook地址</label>
|
||||
<div class="col-sm-9"><input type="text" name="webhook_url" value="{:config_get('webhook_url')}" class="form-control"/></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">@用户手机号</label>
|
||||
<div class="col-sm-9"><input type="text" name="webhook_user" value="{:config_get('webhook_user')}" class="form-control" placeholder="非必填,可填写用户的手机号,@全体填写all"/></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"/>
|
||||
@@ -130,7 +134,9 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
仅支持填写企业微信、钉钉、飞书群机器人的Webhook地址
|
||||
仅支持填写企业微信、钉钉、飞书群机器人的Webhook地址<br/>
|
||||
认证方式可以选自定义关键词“DNS”或IP白名单。<br/>
|
||||
@用户不支持企业微信,飞书用户手机号需要填写<a href="https://open.feishu.cn/document/home/user-identity-introduction/open-id" target="_blank" rel="noreferrer">用户ID</a>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,9 +279,7 @@ function setStatus(id,status) {
|
||||
});
|
||||
}
|
||||
function delItem(id) {
|
||||
var confirmobj = layer.confirm('确定要删除此用户吗?', {
|
||||
btn: ['确定','取消']
|
||||
}, function(){
|
||||
layer.confirm('确定要删除此用户吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
@@ -298,8 +296,6 @@ function delItem(id) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, function(){
|
||||
layer.close(confirmobj);
|
||||
});
|
||||
}
|
||||
var CreatePassword = function (len)
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"cccyun/php-whois": "^1.0",
|
||||
"cccyun/think-captcha": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"phpmailer/phpmailer": "^6.10",
|
||||
"phpmailer/phpmailer": "^7.0",
|
||||
"symfony/polyfill-intl-idn": "^1.32",
|
||||
"symfony/polyfill-mbstring": "^1.32",
|
||||
"symfony/polyfill-php81": "^1.32",
|
||||
|
||||
190
composer.lock
generated
190
composer.lock
generated
@@ -4,20 +4,20 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f7c4abfaf4cb80cd99107e9e1763e75c",
|
||||
"content-hash": "34b2ff614d9cf3cc515823086a4f091b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "cccyun/php-whois",
|
||||
"version": "1.2",
|
||||
"version": "1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/netcccyun/php-whois.git",
|
||||
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1"
|
||||
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/c631f1c5e26e7150501a14cd25a2380f8a077ca1",
|
||||
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1",
|
||||
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/f02627ba0bef005aa9e336d63541f9fd288675b5",
|
||||
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -62,9 +62,9 @@
|
||||
"црщшы"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/netcccyun/php-whois/tree/1.2"
|
||||
"source": "https://github.com/netcccyun/php-whois/tree/1.3"
|
||||
},
|
||||
"time": "2025-06-25T06:54:23+00:00"
|
||||
"time": "2026-02-12T05:56:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cccyun/think-captcha",
|
||||
@@ -329,16 +329,16 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "2.8.0",
|
||||
"version": "2.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "21dc724a0583619cd1652f673303492272778051"
|
||||
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
|
||||
"reference": "21dc724a0583619cd1652f673303492272778051",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884",
|
||||
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -354,6 +354,7 @@
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"http-interop/http-factory-tests": "0.9.0",
|
||||
"jshttp/mime-db": "1.54.0.1",
|
||||
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -425,7 +426,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/psr7/issues",
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.8.0"
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.9.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -441,20 +442,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-23T21:21:41+00:00"
|
||||
"time": "2026-03-10T16:41:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.11.1",
|
||||
"version": "v7.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
|
||||
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
|
||||
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -468,13 +469,13 @@
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"squizlabs/php_codesniffer": "^3.7.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
@@ -515,7 +516,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -523,7 +524,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-30T11:54:53+00:00"
|
||||
"time": "2026-01-09T18:02:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
@@ -687,16 +688,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.1",
|
||||
"version": "2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
|
||||
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -705,7 +706,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1.x-dev"
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -720,7 +721,7 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
@@ -734,9 +735,9 @@
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-message/tree/1.1"
|
||||
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||
},
|
||||
"time": "2023-04-04T09:50:52+00:00"
|
||||
"time": "2023-04-04T09:54:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
@@ -952,16 +953,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1011,7 +1012,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1031,11 +1032,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
@@ -1098,7 +1099,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1122,7 +1123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -1183,7 +1184,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1207,16 +1208,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1268,7 +1269,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1288,11 +1289,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-12-23T08:48:59+00:00"
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -1348,7 +1349,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1372,16 +1373,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.33.0",
|
||||
"version": "v1.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692"
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"reference": "5d2ed36f7734637dacc025f179698031951b1692",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1428,7 +1429,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1448,32 +1449,32 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.3.3",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
|
||||
"reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
|
||||
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883",
|
||||
"reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0"
|
||||
"symfony/console": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
@@ -1504,7 +1505,7 @@
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.3"
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1524,20 +1525,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-27T11:34:33+00:00"
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/framework",
|
||||
"version": "v8.1.3",
|
||||
"version": "v8.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/framework.git",
|
||||
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f"
|
||||
"reference": "8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/top-think/framework/zipball/e4207e98b66f92d26097ed6efd535930cba90e8f",
|
||||
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f",
|
||||
"url": "https://api.github.com/repos/top-think/framework/zipball/8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01",
|
||||
"reference": "8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1545,7 +1546,7 @@
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.0.0",
|
||||
"psr/http-message": "^1.0",
|
||||
"psr/http-message": "^1.0|^2.0",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"psr/simple-cache": "^1.0|^2.0|^3.0",
|
||||
"topthink/think-container": "^3.0",
|
||||
@@ -1554,6 +1555,7 @@
|
||||
"topthink/think-validate": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.92",
|
||||
"guzzlehttp/psr7": "^2.1.0",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"mockery/mockery": "^1.2",
|
||||
@@ -1589,9 +1591,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/top-think/framework/issues",
|
||||
"source": "https://github.com/top-think/framework/tree/v8.1.3"
|
||||
"source": "https://github.com/top-think/framework/tree/v8.1.4"
|
||||
},
|
||||
"time": "2025-07-14T03:48:44+00:00"
|
||||
"time": "2026-01-15T02:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-container",
|
||||
@@ -1641,16 +1643,16 @@
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-helper",
|
||||
"version": "v3.1.11",
|
||||
"version": "v3.1.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-helper.git",
|
||||
"reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c"
|
||||
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
|
||||
"reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
|
||||
"url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb",
|
||||
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1681,22 +1683,22 @@
|
||||
"description": "The ThinkPHP6 Helper Package",
|
||||
"support": {
|
||||
"issues": "https://github.com/top-think/think-helper/issues",
|
||||
"source": "https://github.com/top-think/think-helper/tree/v3.1.11"
|
||||
"source": "https://github.com/top-think/think-helper/tree/v3.1.12"
|
||||
},
|
||||
"time": "2025-04-07T06:55:59+00:00"
|
||||
"time": "2025-12-26T09:58:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-orm",
|
||||
"version": "v4.0.50",
|
||||
"version": "v4.0.51",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-orm.git",
|
||||
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc"
|
||||
"reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/top-think/think-orm/zipball/ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
|
||||
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
|
||||
"url": "https://api.github.com/repos/top-think/think-orm/zipball/46abe2f824eb3bcb117d4c0ce93b203b592b79f7",
|
||||
"reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1741,9 +1743,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/top-think/think-orm/issues",
|
||||
"source": "https://github.com/top-think/think-orm/tree/v4.0.50"
|
||||
"source": "https://github.com/top-think/think-orm/tree/v4.0.51"
|
||||
},
|
||||
"time": "2025-08-26T05:32:22+00:00"
|
||||
"time": "2025-12-18T13:11:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-template",
|
||||
@@ -1907,16 +1909,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
|
||||
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1928,10 +1930,10 @@
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/process": "^6.4|^7.0",
|
||||
"symfony/uid": "^6.4|^7.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/uid": "^6.4|^7.0|^8.0",
|
||||
"twig/twig": "^3.12"
|
||||
},
|
||||
"bin": [
|
||||
@@ -1970,7 +1972,7 @@
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1990,7 +1992,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-11T10:12:26+00:00"
|
||||
"time": "2026-03-30T13:44:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-trace",
|
||||
@@ -2046,7 +2048,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -2060,6 +2062,6 @@
|
||||
"ext-sockets": "*",
|
||||
"ext-ssh2": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ return [
|
||||
'show_error_msg' => true,
|
||||
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
|
||||
|
||||
'version' => '1042',
|
||||
'version' => '1049',
|
||||
|
||||
'dbversion' => '1040'
|
||||
'dbversion' => '1048'
|
||||
];
|
||||
|
||||
6
public/static/images/acepanel.svg
Normal file
6
public/static/images/acepanel.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="812" height="812">
|
||||
<rect x="0" y="0" width="812" height="812" rx="80" ry="80" fill="#333333"/>
|
||||
<path d="M0,0 L136,0 L138,23 L141,47 L146,91 L152,146 L155,169 L157,188 L161,222 L163,243 L167,275 L169,294 L173,329 L176,355 L178,372 L178,377 L360,377 L354,399 L342,439 L333,470 L324,501 L321,509 L302,509 L307,504 L313,497 L313,494 L296,481 L293,481 L286,489 L277,494 L270,496 L261,496 L253,493 L248,487 L246,481 L246,474 L247,473 L276,470 L296,464 L307,458 L314,451 L318,443 L320,430 L318,420 L313,412 L306,406 L294,401 L290,400 L267,400 L254,404 L243,410 L233,419 L223,434 L217,450 L215,464 L215,478 L217,490 L222,502 L226,507 L226,509 L187,509 L193,501 L199,492 L199,488 L179,479 L176,478 L169,488 L164,493 L157,496 L150,496 L142,493 L138,488 L136,480 L138,464 L142,445 L146,435 L151,429 L157,426 L162,425 L169,425 L176,428 L181,436 L182,439 L187,438 L208,430 L207,422 L201,412 L193,406 L182,401 L175,400 L155,400 L143,404 L132,410 L122,419 L115,430 L110,440 L107,450 L105,468 L106,488 L111,500 L116,509 L86,509 L84,496 L81,460 L78,432 L74,385 L-27,385 L-22,373 L-7,342 L1,324 L9,308 L14,297 L67,297 L66,293 L62,236 L60,213 L57,169 L52,104 L52,95 L45,95 L43,101 L27,136 L11,171 L-7,210 L-26,251 L-42,285 L-50,304 L-60,326 L-68,342 L-76,359 L-84,375 L-96,400 L-114,437 L-125,460 L-137,485 L-148,508 L-149,509 L-261,509 L-259,503 L-245,475 L-235,456 L-212,411 L-202,392 L-189,366 L-179,347 L-159,308 L-145,281 L-137,266 L-129,250 L-105,203 L-91,176 L-82,159 L-74,143 L-51,98 L-34,65 L-19,36 L-1,1 Z " fill="#FEFEFE" transform="translate(372,151)"/>
|
||||
<path d="M0,0 L11,0 L17,3 L19,6 L19,15 L14,21 L4,25 L-14,28 L-20,28 L-18,17 L-13,9 L-5,2 Z " fill="#FBFBFB" transform="translate(642,575)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
BIN
public/static/images/amh.ico
Normal file
BIN
public/static/images/amh.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user