34 Commits

Author SHA1 Message Date
net909
4bf87156e3 version 2025-10-10 20:47:20 +08:00
net909
475c14804a 更新composer 2025-10-10 20:11:35 +08:00
net909
531ad68847 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-10-10 20:10:54 +08:00
net909
f86c68fc6a 新增K8S部署 2025-10-10 20:10:12 +08:00
dependabot[bot]
460067a5e7 Bump phpmailer/phpmailer from 6.10.0 to 6.11.1 (#324)
Bumps [phpmailer/phpmailer](https://github.com/PHPMailer/PHPMailer) from 6.10.0 to 6.11.1.
- [Release notes](https://github.com/PHPMailer/PHPMailer/releases)
- [Changelog](https://github.com/PHPMailer/PHPMailer/blob/master/changelog.md)
- [Commits](https://github.com/PHPMailer/PHPMailer/compare/v6.10.0...v6.11.1)

---
updated-dependencies:
- dependency-name: phpmailer/phpmailer
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:39:20 +08:00
net909
6ffa9e003a 新增天翼云部署 2025-09-29 10:45:38 +08:00
net909
c5ed1c6990 增加fnOS部署,堡塔云WAF支持部署本身证书 2025-09-17 20:46:11 +08:00
net909
2b51a2d015 新增南墙WAF、小皮面板部署 2025-09-03 19:48:18 +08:00
net909
2e00773c0a readme 2025-08-31 17:04:30 +08:00
net909
4bee80e06e 新增百度云BLB部署 2025-08-27 19:13:55 +08:00
net909
7cb745acdf 优化部署阿里云SLB 2025-08-26 22:32:30 +08:00
net909
79437aba60 新增支持宝塔Win极速版,lecdn支持令牌认证 2025-08-25 17:18:35 +08:00
net909
fd21a55d01 新增定时切换解析功能 2025-08-22 21:25:42 +08:00
net909
9f529e2528 新增uniCloud部署,cdnfly部署支持账号密码登录 2025-08-21 19:47:37 +08:00
net909
9a70fd7116 修复泛域名部署支持 2025-08-12 10:11:08 +08:00
dependabot[bot]
3dd55cf007 Bump topthink/think-orm from 4.0.48 to 4.0.49 (#293)
Bumps [topthink/think-orm](https://github.com/top-think/think-orm) from 4.0.48 to 4.0.49.
- [Release notes](https://github.com/top-think/think-orm/releases)
- [Commits](https://github.com/top-think/think-orm/compare/v4.0.48...v4.0.49)

---
updated-dependencies:
- dependency-name: topthink/think-orm
  dependency-version: 4.0.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 10:09:29 +08:00
I
6ff8cb9f45 Update README.md (#294)
将 Docker 本地持久化数据路径修改为与 docker-compose.yml 文件同目录,并移除 YAML 文件中的 version 标识(新版 Docker 已不再使用)。
2025-08-12 10:09:04 +08:00
net909
98f185ee8e 修复腾讯云吊销证书 2025-08-08 10:13:36 +08:00
dependabot[bot]
028688df20 Bump cccyun/php-whois from 1.1 to 1.2 (#288) 2025-08-08 09:56:38 +08:00
dependabot[bot]
e8c68375a9 Bump topthink/think-orm from 4.0.47 to 4.0.48 (#284) 2025-08-03 00:06:45 +08:00
dependabot[bot]
6958530337 Bump symfony/var-dumper from 7.3.1 to 7.3.2 (#283) 2025-08-03 00:06:36 +08:00
net909
0863d02cc9 fix 2025-07-28 20:37:10 +08:00
net909
9032ea0405 整合计划任务,新增访问URL方式的计划任务 2025-07-28 20:30:09 +08:00
net909
e3749ecb6c Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-07-18 14:47:30 +08:00
net909
e1e90c3c71 修复阿里云ESA部署失败 2025-07-18 14:47:12 +08:00
dependabot[bot]
a171a5b9b0 Bump topthink/think-trace from 1.6 to 2.0 (#276)
---
updated-dependencies:
- dependency-name: topthink/think-trace
  dependency-version: '2.0'
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 17:08:08 +08:00
dependabot[bot]
f608b2fceb Bump topthink/framework from 8.1.2 to 8.1.3 (#277)
---
updated-dependencies:
- dependency-name: topthink/framework
  dependency-version: 8.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 17:08:01 +08:00
Hanada
0837ac9be1 Merge pull request #274 from HanadaLee/multipartpostfix
重构POST内容判断逻辑,去除X-Content-Type标头
2025-07-12 09:44:14 +08:00
net909
c31e0eaf41 修复部署失败 2025-07-11 09:56:08 +08:00
net909
987deda95d 修复报错 2025-07-10 16:41:11 +08:00
消失的彩虹海
654151ce5b Merge pull request #270 from HanadaLee/ssldeployfix
修复部署证书到阿里云和群晖失败的问题
2025-07-08 23:25:03 +08:00
Hanada
2fedee1e93 修复使用guzzle库后部署证书到群晖失败的问题 2025-07-08 23:12:40 +08:00
Hanada
ba97ac3685 修复部署阿里云时证书序列号可能存在前置0的问题 2025-07-08 21:42:40 +08:00
net909
f2f1a0d01e 修复upyun部署 2025-07-07 21:47:15 +08:00
71 changed files with 3582 additions and 456 deletions

View File

@@ -17,6 +17,7 @@
- 多用户管理,可为每个用户可分配不同的域名解析权限;
- 提供API接口可获取域名单独的登录链接方便各种IDC系统对接
- 容灾切换功能支持ping、tcp、http(s)检测协议并自动暂停/修改域名解析,并支持发送通知;
- 定时切换功能,设置在指定时间/周期,自动修改/开启/暂停/删除域名解析;
- CF优选IP功能支持获取最新的Cloudflare优选IP并自动更新到解析记录
- SSL证书申请与自动部署功能支持从Let's Encrypt等渠道申请SSL证书并自动部署到各种面板、云服务商、服务器等
- 支持邮件、微信公众号、Telegram、钉钉、飞书、企业微信等多种通知渠道。
@@ -98,7 +99,6 @@ docker pull swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest
### docker-compose 部署
```
version: '3'
services:
dnsmgr-web:
container_name: dnsmgr-web
@@ -107,7 +107,7 @@ services:
ports:
- 8081:80
volumes:
- /volume1/docker/dnsmgr/web:/app/www
- ./web:/app/www
image: netcccyun/dnsmgr
depends_on:
- dnsmgr-mysql

View File

@@ -12,7 +12,10 @@ use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
use app\service\ScheduleService;
class Certtask extends Command
{
@@ -20,7 +23,7 @@ class Certtask extends Command
{
// 指令配置
$this->setName('certtask')
->setDescription('证书申请与部署任务');
->setDescription('SSL证书续签与部署、域名到期提醒、定时切换解析、CF优选IP更新');
}
protected function execute(Input $input, Output $output)
@@ -28,6 +31,12 @@ class Certtask extends Command
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');
Config::set($res, 'sys');
(new CertTaskService())->execute();
(new ScheduleService())->execute();
$res = (new OptimizeService())->execute();
if (!$res) {
(new CertTaskService())->execute();
(new ExpireNoticeService())->task();
}
echo 'done'.PHP_EOL;
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\OptimizeService;
class Opiptask extends Command
{
protected function configure()
{
// 指令配置
$this->setName('opiptask')
->setDescription('CF优选IP任务');
}
protected function execute(Input $input, Output $output)
{
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');
Config::set($res, 'sys');
(new OptimizeService())->execute();
}
}

View File

@@ -457,6 +457,10 @@ function http_request($url, $data = null, $referer = null, $cookie = null, $head
if ($options['headers']['Content-Type'] == 'application/x-www-form-urlencoded') {
// 表单
$options['form_params'] = $data;
} else if ($options['headers']['Content-Type'] == 'multipart/form-data') {
// 表单文件
$options['multipart'] = $data;
unset($options['headers']['Content-Type']); // 由GuzzleHttp重新生成Content-Type头部
} else if ($options['headers']['Content-Type'] == 'application/json') {
// json
$options['json'] = $data;

View File

@@ -65,102 +65,125 @@ class Dmonitor extends BaseController
$list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray();
foreach ($list as &$row) {
$row['checktimestr'] = date('Y-m-d H:i:s', $row['checktime']);
$row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']);
$row['checktimestr'] = $row['checktime'] > 0 ? date('Y-m-d H:i:s', $row['checktime']) : '未运行';
}
return json(['total' => $total, 'rows' => $list]);
}
public function task_op()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if ($action == 'add') {
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'proxy' => input('post.proxy/d'),
'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0,
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
'addtime' => time(),
'active' => 1
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) {
return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) {
return json(['code' => -1, 'msg' => '主备地址不能相同']);
}
if (Db::name('dmtask')->where('recordid', $task['recordid'])->find()) {
return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']);
}
Db::name('dmtask')->insert($task);
return json(['code' => 0, 'msg' => '添加成功']);
} elseif ($action == 'edit') {
$id = input('post.id/d');
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'proxy' => input('post.proxy/d'),
'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0,
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) {
return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) {
return json(['code' => -1, 'msg' => '主备地址不能相同']);
}
if (Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']);
}
Db::name('dmtask')->where('id', $id)->update($task);
return json(['code' => 0, 'msg' => '修改成功']);
} elseif ($action == 'setactive') {
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('dmtask')->where('id', $id)->update(['active' => $active]);
return json(['code' => 0, 'msg' => '设置成功']);
} elseif ($action == 'del') {
$id = input('post.id/d');
Db::name('dmtask')->where('id', $id)->delete();
Db::name('dmlog')->where('taskid', $id)->delete();
return json(['code' => 0, 'msg' => '删除成功']);
} elseif ($action == 'operation') {
$ids = input('post.ids');
$success = 0;
foreach ($ids as $id) {
if (input('post.act') == 'delete') {
Db::name('dmtask')->where('id', $id)->delete();
Db::name('dmlog')->where('taskid', $id)->delete();
$success++;
} elseif (input('post.act') == 'retry') {
Db::name('dmtask')->where('id', $id)->update(['checknexttime' => time()]);
$success++;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$isauto = input('post.act') == 'open' ? 1 : 0;
Db::name('dmtask')->where('id', $id)->update(['active' => $isauto]);
$success++;
}
}
return json(['code' => 0, 'msg' => '成功操作' . $success . '个容灾切换策略']);
} else {
return json(['code' => -1, 'msg' => '参数错误']);
}
}
public function taskform()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if ($this->request->isPost()) {
if ($action == 'add') {
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'proxy' => input('post.proxy/d'),
'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0,
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
'addtime' => time(),
'active' => 1
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) {
return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) {
return json(['code' => -1, 'msg' => '主备地址不能相同']);
}
if (Db::name('dmtask')->where('recordid', $task['recordid'])->find()) {
return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']);
}
Db::name('dmtask')->insert($task);
return json(['code' => 0, 'msg' => '添加成功']);
} elseif ($action == 'edit') {
$id = input('post.id/d');
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'proxy' => input('post.proxy/d'),
'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0,
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) {
return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) {
return json(['code' => -1, 'msg' => '主备地址不能相同']);
}
if (Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']);
}
Db::name('dmtask')->where('id', $id)->update($task);
return json(['code' => 0, 'msg' => '修改成功']);
} elseif ($action == 'setactive') {
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('dmtask')->where('id', $id)->update(['active' => $active]);
return json(['code' => 0, 'msg' => '设置成功']);
} elseif ($action == 'del') {
$id = input('post.id/d');
Db::name('dmtask')->where('id', $id)->delete();
Db::name('dmlog')->where('taskid', $id)->delete();
return json(['code' => 0, 'msg' => '删除成功']);
} else {
return json(['code' => -1, 'msg' => '参数错误']);
}
}
$task = null;
if ($action == 'edit') {
$id = input('get.id/d');

View File

@@ -276,6 +276,7 @@ class Domain extends BaseController
Db::name('domain')->where('id', $id)->delete();
Db::name('dmtask')->where('did', $id)->delete();
Db::name('optimizeip')->where('did', $id)->delete();
Db::name('sctask')->where('did', $id)->delete();
return json(['code' => 0]);
} elseif ($act == 'batchadd') {
if (!checkPermission(2)) return $this->alert('error', '无权限');
@@ -318,6 +319,7 @@ class Domain extends BaseController
Db::name('domain')->where('id', 'in', $ids)->delete();
Db::name('dmtask')->where('did', 'in', $ids)->delete();
Db::name('optimizeip')->where('did', 'in', $ids)->delete();
Db::name('sctask')->where('did', 'in', $ids)->delete();
return json(['code' => 0, 'msg' => '成功删除' . count($ids) . '个域名!']);
}
return json(['code' => -3]);

View File

@@ -20,6 +20,9 @@ class Optimizeip extends BaseController
if (empty($key)) {
continue;
}
if ($key == 'optimize_ip_min' && intval($value) < 10) {
return json(['code' => -1, 'msg' => '自动更新时间间隔不能小于10分钟']);
}
config_set($key, $value);
Cache::delete('configs');
}

165
app/controller/Schedule.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\service\ScheduleService;
class Schedule extends BaseController
{
public function stask()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
return View::fetch();
}
public function stask_data()
{
if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]);
$type = input('post.type/d', 1);
$kw = input('post.kw', null, 'trim');
$stype = input('post.stype', null);
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('sctask')->alias('A')->join('domain B', 'A.did = B.id');
if (!empty($kw)) {
if ($type == 1) {
$select->whereLike('rr|B.name', '%' . $kw . '%');
} elseif ($type == 2) {
$select->where('recordid', $kw);
} elseif ($type == 3) {
$select->where('value', $kw);
} elseif ($type == 4) {
$select->whereLike('remark', '%' . $kw . '%');
}
}
if (!isNullOrEmpty($stype)) {
$select->where('type', $stype);
}
$total = $select->count();
$list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray();
foreach ($list as &$row) {
$row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']);
$row['updatetimestr'] = $row['updatetime'] > 0 ? date('Y-m-d H:i:s', $row['updatetime']) : '未运行';
$row['nexttimestr'] = $row['nexttime'] > 0 ? date('Y-m-d H:i:s', $row['nexttime']) : '无';
}
return json(['total' => $total, 'rows' => $list]);
}
public function stask_op()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if ($action == 'add') {
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'cycle' => input('post.cycle/d'),
'switchtype' => input('post.switchtype/d'),
'switchdate' => input('post.switchdate', null, 'trim'),
'switchtime' => input('post.switchtime', null, 'trim'),
'value' => input('post.value', null, 'trim'),
'line' => input('post.line', null, 'trim'),
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
'addtime' => time(),
'active' => 1
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->find()) {
return json(['code' => -1, 'msg' => '当前定时切换策略已存在']);
}
$id = Db::name('sctask')->insertGetId($task);
$row = Db::name('sctask')->where('id', $id)->find();
(new ScheduleService())->update_nexttime($row);
return json(['code' => 0, 'msg' => '添加成功']);
} elseif ($action == 'edit') {
$id = input('post.id/d');
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'cycle' => input('post.cycle/d'),
'switchtype' => input('post.switchtype/d'),
'switchdate' => input('post.switchdate', null, 'trim'),
'switchtime' => input('post.switchtime', null, 'trim'),
'value' => input('post.value', null, 'trim'),
'line' => input('post.line', null, 'trim'),
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
];
if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '当前定时切换策略已存在']);
}
Db::name('sctask')->where('id', $id)->update($task);
$row = Db::name('sctask')->where('id', $id)->find();
(new ScheduleService())->update_nexttime($row);
return json(['code' => 0, 'msg' => '修改成功']);
} elseif ($action == 'setactive') {
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('sctask')->where('id', $id)->update(['active' => $active]);
return json(['code' => 0, 'msg' => '设置成功']);
} elseif ($action == 'del') {
$id = input('post.id/d');
Db::name('sctask')->where('id', $id)->delete();
return json(['code' => 0, 'msg' => '删除成功']);
} elseif ($action == 'operation') {
$ids = input('post.ids');
$success = 0;
foreach ($ids as $id) {
if (input('post.act') == 'delete') {
Db::name('sctask')->where('id', $id)->delete();
$success++;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$isauto = input('post.act') == 'open' ? 1 : 0;
Db::name('sctask')->where('id', $id)->update(['active' => $isauto]);
$success++;
}
}
return json(['code' => 0, 'msg' => '成功操作' . $success . '个定时切换策略']);
} else {
return json(['code' => -1, 'msg' => '参数错误']);
}
}
public function staskform()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
$task = null;
if ($action == 'edit') {
$id = input('get.id/d');
$task = Db::name('sctask')->where('id', $id)->find();
if (empty($task)) return $this->alert('error', '切换策略不存在');
}
$domains = [];
$domainList = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.id,A.name,B.type')->select();
foreach ($domainList as $row) {
$domains[] = ['id'=>$row['id'], 'name'=>$row['name'], 'type'=>$row['type']];
}
View::assign('domains', $domains);
View::assign('info', $task);
View::assign('action', $action);
return View::fetch();
}
}

View File

@@ -7,6 +7,10 @@ use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
use app\service\ScheduleService;
class System extends BaseController
{
@@ -107,4 +111,39 @@ class System extends BaseController
}
return json(['code' => 0]);
}
public function cronset()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
if (config_get('cron_key') === null) {
config_set('cron_key', random(10));
Cache::delete('configs');
}
View::assign('is_user_www', isset($_SERVER['USER']) && $_SERVER['USER'] == 'www');
View::assign('siteurl', request()->root(true));
return View::fetch();
}
public function cron()
{
if (function_exists("set_time_limit")) {
@set_time_limit(0);
}
if (function_exists("ignore_user_abort")) {
@ignore_user_abort(true);
}
if (isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'Baiduspider')) exit;
$key = input('get.key', '');
$cron_key = config_get('cron_key');
if (config_get('cron_type', '0') != '1' || empty($cron_key)) exit('未开启当前方式');
if ($key != $cron_key) exit('访问密钥错误');
(new ScheduleService())->execute();
$res = (new OptimizeService())->execute();
if (!$res) {
(new CertTaskService())->execute();
(new ExpireNoticeService())->task();
}
echo 'success!';
}
}

View File

@@ -171,7 +171,7 @@ class User extends BaseController
$select->where('domain', $this->request->user['name']);
} elseif ($this->request->user['level'] == 1) {
$select->where('uid', $this->request->user['id']);
} elseif (!empty($uid)) {
} elseif (!isNullOrEmpty($uid)) {
$select->where('uid', $uid);
}
if (!empty($kw)) {

View File

@@ -11,7 +11,7 @@ class DeployHelper
'name' => '宝塔面板',
'class' => 1,
'icon' => 'bt.png',
'desc' => '支持部署到宝塔面板搭建的站点、Docker、邮局与面板本身',
'desc' => '支持部署到宝塔面板&aaPanel搭建的站点、Docker、邮局与面板本身',
'note' => null,
'inputs' => [
'url' => [
@@ -27,6 +27,15 @@ class DeployHelper
'placeholder' => '宝塔面板设置->面板设置->API接口',
'required' => true,
],
'version' => [
'name' => '面板版本',
'type' => 'radio',
'options' => [
'0' => 'Linux面板+Win经典版',
'1' => 'Win极速版',
],
'value' => '0'
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
@@ -54,10 +63,20 @@ class DeployHelper
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'note' => 'PHP项目和反代项目填写创建时绑定的第一个域名Java/Node/Go等其他项目填写项目名称邮局填写域名',
'note' => 'PHP项目和反代项目填写创建时绑定的第一个域名Java/Node/Go等其他项目填写项目名称邮局和IIS站点填写绑定的域名',
'show' => 'type==0||type==2||type==3',
'required' => true,
],
'is_iis' => [
'name' => '是否IIS站点',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'show' => 'type==0',
'value' => '0'
],
],
],
'kangle' => [
@@ -139,12 +158,12 @@ class DeployHelper
'class' => 1,
'icon' => 'host.png',
'desc' => '支持虚拟主机与CDN站点',
'note' => '以上登录地址需填写Easypanel管理员面板地址,非用户面板。',
'note' => '以上登录信息为Easypanel管理员面板,非用户面板。',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'Easypanel管理员面板地址',
'placeholder' => 'Easypanel面板地址',
'note' => '填写规则如http://192.168.1.100:3312 ,不要带其他后缀',
'required' => true,
],
@@ -266,11 +285,22 @@ class DeployHelper
],
],
'taskinputs' => [
'type' => [
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => '网站的证书',
'1' => '面板本身的证书',
],
'value' => '0',
'required' => true,
],
'sites' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'required' => true,
'show' => 'type==0',
],
],
],
@@ -288,17 +318,43 @@ class DeployHelper
'note' => '填写示例http://demo.cdnfly.cn',
'required' => true,
],
'auth' => [
'name' => '认证方式',
'type' => 'radio',
'options' => [
'0' => '接口密钥',
'1' => '模拟登录',
],
'value' => '0',
'required' => true,
],
'api_key' => [
'name' => 'api_key',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==0',
],
'api_secret' => [
'name' => 'api_secret',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==0',
],
'username' => [
'name' => '登录账号',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==1',
],
'password' => [
'name' => '登录密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==1',
],
'proxy' => [
'name' => '使用代理服务器',
@@ -334,17 +390,36 @@ class DeployHelper
'note' => '填写示例http://demo.xxxx.cn',
'required' => true,
],
'auth' => [
'name' => '认证方式',
'type' => 'radio',
'options' => [
'0' => '账号密码(旧版)',
'1' => 'API访问令牌',
],
'value' => '0',
'required' => true,
],
'api_key' => [
'name' => 'API访问令牌',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==1',
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==0',
],
'password' => [
'name' => '密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'auth==0',
],
'proxy' => [
'name' => '使用代理服务器',
@@ -425,6 +500,59 @@ class DeployHelper
],
'taskinputs' => [],
],
'uusec' => [
'name' => '南墙WAF',
'class' => 1,
'icon' => 'waf.png',
'desc' => '',
'note' => null,
'inputs' => [
'url' => [
'name' => '控制台地址',
'type' => 'input',
'placeholder' => '南墙WAF控制台地址',
'note' => '填写规则如http://192.168.1.100:4443 ,不要带其他后缀',
'required' => true,
],
'username' => [
'name' => '用户名',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'password' => [
'name' => '密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '',
'note' => '在证书管理查看证书的ID注意域名是否与证书匹配',
'required' => true,
],
'name' => [
'name' => '证书名称',
'type' => 'input',
'placeholder' => '',
'note' => '在证书管理查看证书的名称',
'required' => true,
],
],
],
'opanel' => [
'name' => '1Panel',
'class' => 1,
@@ -585,6 +713,47 @@ class DeployHelper
],
],
],
'xp' => [
'name' => '小皮面板',
'class' => 1,
'icon' => 'xp.png',
'desc' => '',
'note' => null,
'tasknote' => '',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => '小皮面板地址',
'note' => '填写规则如http://192.168.1.100:8888 ,不要带其他后缀',
'required' => true,
],
'apikey' => [
'name' => '接口密钥',
'type' => 'input',
'placeholder' => '设置->OpenAPI接口',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'sites' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'note' => '网站名称,即为网站创建时绑定的第一个域名',
'required' => true,
],
],
],
'synology' => [
'name' => '群晖面板',
'class' => 1,
@@ -679,6 +848,47 @@ class DeployHelper
],
'taskinputs' => [],
],
'fnos' => [
'name' => '飞牛OS',
'class' => 1,
'icon' => 'fnos.png',
'desc' => '更新飞牛OS的证书',
'note' => '请先配置sudo免密<br/>
sudo visudo<br/>
#在文件最后一行增加以下内容需要将username替换成自己的用户名<br/>
username ALL=(ALL) NOPASSWD: NOPASSWD: ALL<br/>
ctrl+x 保存退出',
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
'host' => [
'name' => '主机地址',
'type' => 'input',
'placeholder' => '填写IP地址或域名需开启SSH功能',
'required' => true,
],
'port' => [
'name' => 'SSH端口',
'type' => 'input',
'placeholder' => '',
'value' => '22',
'required' => true,
],
'username' => [
'name' => '用户名',
'type' => 'input',
'placeholder' => '登录用户名',
'value' => '',
'required' => true,
],
'password' => [
'name' => '密码',
'type' => 'input',
'placeholder' => '登录密码',
'required' => true,
],
],
'taskinputs' => [],
],
'proxmox' => [
'name' => 'Proxmox VE',
'class' => 1,
@@ -725,6 +935,56 @@ class DeployHelper
],
],
],
'k8s' => [
'name' => 'K8S',
'class' => 1,
'icon' => 'server.png',
'desc' => '部署到K8S集群的Secret和Ingress',
'note' => '支持部署到K8S集群的Secret和Ingress',
'tasknote' => '',
'inputs' => [
'name' => [
'name' => '名称',
'type' => 'input',
'placeholder' => '仅用于区分',
'required' => true,
],
'kubeconfig' => [
'name' => 'kubeconfig',
'type' => 'textarea',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'namespace' => [
'name' => '命名空间',
'type' => 'input',
'value' => 'default',
'required' => true,
],
'secret_name' => [
'name' => 'Secret名称',
'type' => 'input',
'placeholder' => '如果Secret不存在则自动创建',
'required' => true,
],
'ingresses' => [
'name' => 'Ingress名称',
'type' => 'input',
'placeholder' => '多个用英文逗号分隔可留空留空则只更新Secret',
],
],
],
'aliyun' => [
'name' => '阿里云',
'class' => 2,
@@ -864,6 +1124,24 @@ class DeployHelper
'note' => '进入NLB实例详情->监听列表复制监听ID只支持TCPSSL监听协议',
'required' => true,
],
'deploy_type' => [
'name' => '部署证书类型',
'type' => 'select',
'options' => [
['value'=>'0', 'label'=>'默认证书'],
['value'=>'1', 'label'=>'扩展证书'],
],
'value' => '0',
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'',
'required' => true,
],
'clb_domain' => [
'name' => '扩展域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'clb\'&&deploy_type==1',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
@@ -1258,8 +1536,8 @@ class DeployHelper
'name' => '百度云',
'class' => 2,
'icon' => 'baidu.ico',
'desc' => '支持部署到百度云CDN',
'note' => '支持部署到百度云CDN',
'desc' => '支持部署到百度云CDN、BLB',
'note' => '支持部署到百度云CDN、BLB',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
@@ -1284,10 +1562,102 @@ class DeployHelper
],
],
'taskinputs' => [
'product' => [
'name' => '要部署的产品',
'type' => 'select',
'options' => [
['value'=>'cdn', 'label'=>'CDN'],
['value'=>'blb', 'label'=>'普通型BLB'],
['value'=>'appblb', 'label'=>'应用型BLB'],
],
'value' => 'cdn',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'cdn\'',
'required' => true,
],
'region' => [
'name' => '所属地域',
'type' => 'select',
'options' => [
['value'=>'bj', 'label'=>'北京'],
['value'=>'gz', 'label'=>'广州'],
['value'=>'su', 'label'=>'苏州'],
['value'=>'hkg', 'label'=>'香港'],
['value'=>'fwh', 'label'=>'武汉'],
['value'=>'bd', 'label'=>'保定'],
['value'=>'fsh', 'label'=>'上海'],
['value'=>'sin', 'label'=>'新加坡'],
],
'value' => 'bj',
'show' => 'product==\'blb\'||product==\'appblb\'',
'required' => true,
],
'blb_id' => [
'name' => '负载均衡实例ID',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'blb\'||product==\'appblb\'',
'required' => true,
],
'blb_port' => [
'name' => 'HTTPS监听端口',
'type' => 'input',
'placeholder' => '',
'value' => '443',
'show' => 'product==\'blb\'||product==\'appblb\'',
'required' => true,
],
],
],
'ksyun' => [
'name' => '金山云',
'class' => 2,
'icon' => 'ksyun.ico',
'desc' => '支持部署到金山云CDN',
'note' => '支持部署到金山云CDN',
'inputs' => [
'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'
],
],
'taskinputs' => [
'product' => [
'name' => '要部署的产品',
'type' => 'select',
'options' => [
['value'=>'cdn', 'label'=>'CDN'],
],
'value' => 'cdn',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'cdn\'',
'required' => true,
],
],
@@ -1296,8 +1666,8 @@ class DeployHelper
'name' => '火山引擎',
'class' => 2,
'icon' => 'huoshan.ico',
'desc' => '支持部署到火山引擎CDN',
'note' => '支持部署到火山引擎CDN',
'desc' => '支持部署到火山引擎CDN、CLB、TOS、直播、veImageX',
'note' => '支持部署到火山引擎CDN、CLB、TOS、直播、veImageX',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
@@ -1636,6 +2006,61 @@ class DeployHelper
],
],
],
'unicloud' => [
'name' => 'uniCloud',
'class' => 2,
'icon' => 'unicloud.png',
'desc' => '部署到uniCloud服务空间',
'note' => null,
'inputs' => [
'username' => [
'name' => '账号',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'password' => [
'name' => '密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'spaceId' => [
'name' => '服务空间ID',
'type' => 'input',
'placeholder' => 'spaceId',
'required' => true,
],
'provider' => [
'name' => '空间提供商',
'type' => 'select',
'options' => [
['value'=>'aliyun', 'label'=>'阿里云'],
['value'=>'tencent', 'label'=>'腾讯云'],
['value'=>'alipay', 'label'=>'支付宝云'],
],
'value' => 'aliyun',
'required' => true,
],
'domains' => [
'name' => '空间域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'required' => true,
],
],
],
'aws' => [
'name' => 'AWS',
'class' => 2,

View File

@@ -159,9 +159,9 @@ class tencent implements CertInterface
if (!empty($data['RevokeDomainValidateAuths'])) {
$dnsList = [];
foreach ($data['RevokeDomainValidateAuths'] as $opts) {
$mainDomain = getMainDomain($opts['DomainValidateAuthDomain']);
$name = str_replace('.' . $mainDomain, '', $opts['DomainValidateAuthKey']);
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'CNAME', 'value' => $opts['DomainValidateAuthValue']];
$mainDomain = getMainDomain($opts['DomainValidateAuthKey']);
$name = substr($opts['DomainValidateAuthKey'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['DomainValidateAuthValue']];
}
\app\utils\CertDnsUtils::addDns($dnsList, function ($txt) {
$this->log($txt);

209
app/lib/client/Ksyun.php Normal file
View File

@@ -0,0 +1,209 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 金山云
*/
class Ksyun
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $service;
private $region;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $service, $region, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->SecretAccessKey = $SecretAccessKey;
$this->endpoint = $endpoint;
$this->service = $service;
$this->region = $region;
$this->proxy = $proxy;
}
/**
* @param string $method 请求方法
* @param string $action 方法名称
* @param array $params 请求参数
* @return array
* @throws Exception
*/
public function request($method, $action, $version, $path = '/', $params = [])
{
if (!empty($params)) {
$params = array_filter($params, function ($a) {
return $a !== null;
});
}
$body = '';
$query = [];
if ($method == 'GET') {
$query = $params;
} else {
$body = !empty($params) ? json_encode($params) : '';
}
$time = time();
$headers = [
'Host' => $this->endpoint,
'X-Amz-Date' => gmdate("Ymd\THis\Z", $time),
'X-Version' => $version,
'X-Action' => $action,
];
$authorization = $this->generateSign($method, $path, $query, $headers, $body, $time);
$headers['Authorization'] = $authorization;
$headers['Accept'] = 'application/json';
if ($body) {
$headers['Content-Type'] = 'application/json';
}
$url = 'https://' . $this->endpoint . $path;
if (!empty($query)) {
$url .= '?' . http_build_query($query);
}
$header = [];
foreach ($headers as $key => $value) {
$header[] = $key . ': ' . $value;
}
return $this->curl($method, $url, $body, $header);
}
private function generateSign($method, $path, $query, $headers, $body, $time)
{
$algorithm = "AWS4-HMAC-SHA256";
// step 1: build canonical request string
$httpRequestMethod = $method;
$canonicalUri = $this->getCanonicalURI($path);
$canonicalQueryString = $this->getCanonicalQueryString($query);
[$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers);
$hashedRequestPayload = hash("sha256", $body);
$canonicalRequest = $httpRequestMethod . "\n"
. $canonicalUri . "\n"
. $canonicalQueryString . "\n"
. $canonicalHeaders . "\n"
. $signedHeaders . "\n"
. $hashedRequestPayload;
// step 2: build string to sign
$date = gmdate("Ymd\THis\Z", $time);
$shortDate = substr($date, 0, 8);
$credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request';
$hashedCanonicalRequest = hash("sha256", $canonicalRequest);
$stringToSign = $algorithm . "\n"
. $date . "\n"
. $credentialScope . "\n"
. $hashedCanonicalRequest;
// step 3: sign string
$kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true);
$kRegion = hash_hmac("sha256", $this->region, $kDate, true);
$kService = hash_hmac("sha256", $this->service, $kRegion, true);
$kSigning = hash_hmac("sha256", "aws4_request", $kService, true);
$signature = hash_hmac("sha256", $stringToSign, $kSigning);
// step 4: build authorization
$credential = $this->AccessKeyId . '/' . $credentialScope;
$authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature;
return $authorization;
}
private function escape($str)
{
$search = ['+', '*', '%7E'];
$replace = ['%20', '%2A', '~'];
return str_replace($search, $replace, urlencode($str));
}
private function getCanonicalURI($path)
{
if (empty($path)) return '/';
$pattens = explode('/', $path);
$pattens = array_map(function ($item) {
return $this->escape($item);
}, $pattens);
$canonicalURI = implode('/', $pattens);
return $canonicalURI;
}
private function getCanonicalQueryString($parameters)
{
if (empty($parameters)) return '';
ksort($parameters);
$canonicalQueryString = '';
foreach ($parameters as $key => $value) {
if (!is_array($value)) {
$canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value);
} else {
sort($value);
foreach ($value as $v) {
$canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($v);
}
}
}
return substr($canonicalQueryString, 1);
}
private function getCanonicalHeaders($oldheaders)
{
$headers = array();
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];
}
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);
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);
$arr = json_decode($response, true);
if ($httpCode == 200) {
return $arr;
} else {
if (isset($arr['Error']['Message'])) {
throw new Exception($arr['Error']['Message']);
} else {
throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')');
}
}
}
}

View File

@@ -101,7 +101,7 @@ class aliyun implements DeployInterface
$cert_id = null;
if ($data['TotalCount'] > 0 && !empty($data['CertificateOrderList'])) {
foreach ($data['CertificateOrderList'] as $cert) {
if (strtolower($cert['SerialNo']) == $serial_no) {
if (strtolower($cert['SerialNo']) == $serial_no || strpos(strtolower($cert['SerialNo']), $serial_no) !== false) {
$cert_id = $cert['CertificateId'];
$cert_name = $cert['Name'];
break;
@@ -216,7 +216,7 @@ class aliyun implements DeployInterface
if ($flag) {
$exist_cert_id = $cert['Id'];
$exist_cert_name = $cert['Name'];
$exist_cert_casid = $cert['CasId'];
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
break;
}
}
@@ -568,36 +568,65 @@ class aliyun implements DeployInterface
$this->log('找到已添加的服务器证书 ServerCertificateId=' . $ServerCertificateId);
}
$param = [
'Action' => 'DescribeLoadBalancerHTTPSListenerAttribute',
'RegionId' => $config['regionid'],
'LoadBalancerId' => $config['clb_id'],
'ListenerPort' => $config['clb_port'],
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('HTTPS监听配置查询失败' . $e->getMessage());
}
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
if ($deploy_type == 1) {
if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空');
$domains = explode(',', $config['clb_domain']);
$param = [
'Action' => 'DescribeDomainExtensions',
'RegionId' => $config['regionid'],
'LoadBalancerId' => $config['clb_id'],
'ListenerPort' => $config['clb_port'],
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('扩展域名列表查询失败:' . $e->getMessage());
}
foreach ($data['DomainExtensions']['DomainExtension'] as $item) {
if (in_array($item['Domain'], $domains)) {
if ($ServerCertificateId == $item['ServerCertificateId']) {
$this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书已配置');
} else {
$param = [
'Action' => 'SetDomainExtensionAttribute',
'RegionId' => $config['regionid'],
'DomainExtensionId' => $item['DomainExtensionId'],
'ServerCertificateId' => $ServerCertificateId,
];
$client->request($param);
$this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书更新成功');
}
}
}
} else {
$param = [
'Action' => 'DescribeLoadBalancerHTTPSListenerAttribute',
'RegionId' => $config['regionid'],
'LoadBalancerId' => $config['clb_id'],
'ListenerPort' => $config['clb_port'],
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('HTTPS监听配置查询失败' . $e->getMessage());
}
if ($data['ServerCertificateId'] == $ServerCertificateId) {
$this->log('负载均衡HTTPS监听已配置该证书无需重复操作');
return;
}
if ($data['ServerCertificateId'] == $ServerCertificateId) {
$this->log('负载均衡HTTPS监听已配置该证书无需重复操作');
return;
}
$param = [
'Action' => 'SetLoadBalancerHTTPSListenerAttribute',
'RegionId' => $config['regionid'],
'LoadBalancerId' => $config['clb_id'],
'ListenerPort' => $config['clb_port'],
];
$keys = ['Bandwidth', 'XForwardedFor', 'Scheduler', 'StickySession', 'StickySessionType', 'CookieTimeout', 'Cookie', 'HealthCheck', 'HealthCheckMethod', 'HealthCheckDomain', 'HealthCheckURI', 'HealthyThreshold', 'UnhealthyThreshold', 'HealthCheckTimeout', 'HealthCheckInterval', 'HealthCheckConnectPort', 'HealthCheckHttpCode', 'ServerCertificateId', 'CACertificateId', 'VServerGroup', 'VServerGroupId', 'XForwardedFor_SLBIP', 'XForwardedFor_SLBID', 'XForwardedFor_proto', 'Gzip', 'AclId', 'AclType', 'AclStatus', 'IdleTimeout', 'RequestTimeout', 'EnableHttp2', 'TLSCipherPolicy', 'Description', 'XForwardedFor_SLBPORT', 'XForwardedFor_ClientSrcPort'];
foreach ($keys as $key) {
if (isset($data[$key])) $param[$key] = $data[$key];
$param = [
'Action' => 'SetLoadBalancerHTTPSListenerAttribute',
'RegionId' => $config['regionid'],
'LoadBalancerId' => $config['clb_id'],
'ListenerPort' => $config['clb_port'],
'ServerCertificateId' => $ServerCertificateId,
];
$client->request($param);
$this->log('负载均衡HTTPS监听证书配置成功');
}
$param['ServerCertificateId'] = $ServerCertificateId;
$client->request($param);
$this->log('负载均衡HTTPS监听证书配置成功');
}
private function deploy_alb($cert_id, $config)
@@ -606,33 +635,44 @@ class aliyun implements DeployInterface
$endpoint = 'alb.' . $config['regionid'] . '.aliyuncs.com';
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-06-16', $this->proxy);
$cert_id = $cert_id . '-cn-hangzhou';
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
$param = [
'Action' => 'ListListenerCertificates',
'MaxResults' => 100,
'ListenerId' => $config['alb_listener_id'],
'CertificateType' => 'Server',
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('获取监听证书列表失败:' . $e->getMessage());
}
foreach ($data['Certificates'] as $cert) {
if (strpos($cert['CertificateId'], '-')) $cert['CertificateId'] = substr($cert['CertificateId'], 0, strpos($cert['CertificateId'], '-'));
if ($cert['CertificateId'] == $cert_id) {
$this->log('负载均衡监听证书已添加,无需重复操作');
return;
if ($deploy_type == 1) {
$param = [
'Action' => 'ListListenerCertificates',
'MaxResults' => 100,
'ListenerId' => $config['alb_listener_id'],
'CertificateType' => 'Server',
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('获取监听证书列表失败:' . $e->getMessage());
}
foreach ($data['Certificates'] as $cert) {
if ($cert['CertificateId'] == $cert_id) {
$this->log('负载均衡监听扩展证书已添加,无需重复操作');
return;
}
}
}
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'ListenerId' => $config['alb_listener_id'],
'Certificates.1.CertificateId' => $cert_id . '-cn-hangzhou',
];
$client->request($param);
$this->log('应用型负载均衡监听证书添加成功!');
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'ListenerId' => $config['alb_listener_id'],
'Certificates.1.CertificateId' => $cert_id,
];
$client->request($param);
$this->log('应用型负载均衡监听扩展证书添加成功!');
} else {
$param = [
'Action' => 'UpdateListenerAttribute',
'ListenerId' => $config['alb_listener_id'],
'Certificates.1.CertificateId' => $cert_id,
];
$client->request($param);
$this->log('应用型负载均衡监听默认证书更新成功!');
}
}
private function deploy_nlb($cert_id, $config)
@@ -641,33 +681,44 @@ class aliyun implements DeployInterface
$endpoint = 'nlb.' . $config['regionid'] . '.aliyuncs.com';
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2022-04-30', $this->proxy);
$cert_id = $cert_id . '-cn-hangzhou';
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
$param = [
'Action' => 'ListListenerCertificates',
'MaxResults' => 50,
'ListenerId' => $config['nlb_listener_id'],
'CertificateType' => 'Server',
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('获取监听证书列表失败:' . $e->getMessage());
}
foreach ($data['Certificates'] as $cert) {
if (strpos($cert['CertificateId'], '-')) $cert['CertificateId'] = substr($cert['CertificateId'], 0, strpos($cert['CertificateId'], '-'));
if ($cert['CertificateId'] == $cert_id) {
$this->log('负载均衡监听证书已添加,无需重复操作');
return;
if ($deploy_type == 1) {
$param = [
'Action' => 'ListListenerCertificates',
'MaxResults' => 50,
'ListenerId' => $config['nlb_listener_id'],
'CertificateType' => 'Server',
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('获取监听证书列表失败:' . $e->getMessage());
}
foreach ($data['Certificates'] as $cert) {
if ($cert['CertificateId'] == $cert_id) {
$this->log('负载均衡监听扩展证书已添加,无需重复操作');
return;
}
}
}
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'ListenerId' => $config['nlb_listener_id'],
'AdditionalCertificateIds.1' => $cert_id . '-cn-hangzhou',
];
$client->request($param);
$this->log('网络型负载均衡监听证书添加成功!');
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'ListenerId' => $config['nlb_listener_id'],
'AdditionalCertificateIds.1' => $cert_id,
];
$client->request($param);
$this->log('网络型负载均衡监听扩展证书添加成功!');
} else {
$param = [
'Action' => 'UpdateListenerAttribute',
'ListenerId' => $config['nlb_listener_id'],
'CertificateIds.1' => $cert_id,
];
$client->request($param);
$this->log('网络型负载均衡监听默认证书更新成功!');
}
}
public function setLogger($func)

View File

@@ -29,6 +29,23 @@ class baidu implements DeployInterface
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (!isset($config['product']) || $config['product'] == 'cdn') {
$this->deploy_cdn($fullchain, $privatekey, $config, $info);
} else {
$cert_id = $this->get_cert_id($fullchain, $privatekey);
$info['cert_id'] = $cert_id;
if ($config['product'] == 'blb') {
$this->deploy_blb($cert_id, $config);
} elseif ($config['product'] == 'appblb') {
$this->deploy_appblb($cert_id, $config);
} else {
throw new Exception('不支持的产品类型');
}
}
}
public function deploy_cdn($fullchain, $privatekey, $config, &$info)
{
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
@@ -36,16 +53,6 @@ class baidu implements DeployInterface
$config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com', $this->proxy);
try {
$data = $client->request('GET', '/v2/' . $config['domain'] . '/certificates');
if (isset($data['certName']) && $data['certName'] == $config['cert_name']) {
$this->log('CDN域名 ' . $config['domain'] . ' 证书已存在,无需重复部署');
return;
}
} catch (Exception $e) {
$this->log($e->getMessage());
}
$param = [
'httpsEnable' => 'ON',
'certificate' => [
@@ -54,9 +61,89 @@ class baidu implements DeployInterface
'certPrivateData' => $privatekey,
],
];
$data = $client->request('PUT', '/v2/' . $config['domain'] . '/certificates', null, $param);
$info['cert_id'] = $data['certId'];
$this->log('CDN域名 ' . $config['domain'] . ' 证书部署成功!');
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
try {
$data = $client->request('GET', '/v2/' . $domain . '/certificates');
if (isset($data['certName']) && $data['certName'] == $config['cert_name']) {
$this->log('CDN域名 ' . $domain . ' 证书已存在,无需重复部署');
return;
}
} catch (Exception $e) {
$this->log($e->getMessage());
}
$data = $client->request('PUT', '/v2/' . $domain . '/certificates', null, $param);
$info['cert_id'] = $data['certId'];
$this->log('CDN域名 ' . $domain . ' 证书部署成功!');
}
}
public function deploy_blb($cert_id, $config)
{
if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空');
if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空');
$client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy);
$query = [
'listenerPort' => $config['blb_port'],
];
$param = [
'certIds' => [$cert_id],
];
$client->request('PUT', '/v1/blb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param);
$this->log('普通型BLB ' . $config['blb_id'] . ' 部署证书成功!');
}
public function deploy_appblb($cert_id, $config)
{
if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空');
if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空');
$client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy);
$query = [
'listenerPort' => $config['blb_port'],
];
$param = [
'certIds' => [$cert_id],
];
$client->request('PUT', '/v1/appblb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param);
$this->log('应用型BLB ' . $config['blb_id'] . ' 部署证书成功!');
}
private function get_cert_id($fullchain, $privatekey)
{
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'certificate.baidubce.com', $this->proxy);
$query = [
'certName' => $cert_name,
];
try {
$data = $client->request('GET', '/v1/certificate', $query);
} catch (Exception $e) {
throw new Exception('查找证书失败:' . $e->getMessage());
}
foreach ($data['certs'] as $row) {
if ($row['certName'] == $cert_name) {
$this->log('证书已存在 CertId=' . $row['certId']);
return $row['certId'];
}
}
$param = [
'certName' => $cert_name,
'certServerData' => $fullchain,
'certPrivateData' => $privatekey,
];
try {
$data = $client->request('POST', '/v1/certificate', null, $param);
} catch (Exception $e) {
throw new Exception('上传证书失败:' . $e->getMessage());
}
$cert_id = $data['certId'];
$this->log('上传证书成功 CertId=' . $cert_id);
return $cert_id;
}
public function setLogger($func)

View File

@@ -3,6 +3,7 @@
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\CertHelper;
use Exception;
class btpanel implements DeployInterface
@@ -10,12 +11,14 @@ 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;
}
@@ -23,13 +26,24 @@ class btpanel implements DeployInterface
{
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
$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;
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'] : '面板地址无法连接');
}
} 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 {
throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
}
}
}
@@ -40,6 +54,40 @@ class btpanel implements DeployInterface
$this->log("面板证书部署成功");
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;
@@ -64,6 +112,15 @@ 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);
@@ -82,30 +139,113 @@ class btpanel implements DeployInterface
private function deployPanel($fullchain, $privatekey)
{
$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']);
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 : '返回数据解析失败');
}
} 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 {
throw new Exception($response ? $response : '返回数据解析失败');
}
}
}
private function deploySite($siteName, $fullchain, $privatekey)
{
$path = '/site?action=SetSSL';
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';
$data = [
'type' => '0',
'siteName' => $siteName,
'key' => $privatekey,
'csr' => $fullchain,
'domain' => $domain,
'path' => $pfx_path,
'password' => $password,
];
$response = $this->request($path, $data);
$result = json_decode($response, true);
@@ -169,17 +309,27 @@ class btpanel implements DeployInterface
}
}
private function request($path, $params)
private function request($path, $params, $file = false)
{
$url = $this->url . $path;
$now_time = time();
$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, null, $this->proxy);
$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'];
}
}

View File

@@ -35,6 +35,12 @@ class btwaf implements DeployInterface
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;
@@ -105,6 +111,24 @@ class btwaf implements DeployInterface
}
}
private function deployPanel($fullchain, $privatekey)
{
$path = '/api/config/set_cert';
$data = [
'certContent' => $fullchain,
'keyContent' => $privatekey,
];
$response = $this->request($path, $data);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 0) {
return true;
} elseif (isset($result['res'])) {
throw new Exception($result['res']);
} else {
throw new Exception($response ? $response : '返回数据解析失败');
}
}
public function setLogger($func)
{
$this->logger = $func;

View File

@@ -11,6 +11,9 @@ class cdnfly implements DeployInterface
private $url;
private $api_key;
private $api_secret;
private $auth = 0;
private $username;
private $password;
private $proxy;
public function __construct($config)
@@ -18,13 +21,23 @@ class cdnfly implements DeployInterface
$this->url = rtrim($config['url'], '/');
$this->api_key = $config['api_key'];
$this->api_secret = $config['api_secret'];
$this->auth = isset($config['auth']) ? $config['auth'] : 0;
if ($this->auth == 1) {
$this->username = $config['username'];
$this->password = $config['password'];
}
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空');
$this->request('/v1/user');
if ($this->auth == 1) {
if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填参数不能为空');
$this->login();
} else {
if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空');
$this->request('/v1/user');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
@@ -37,10 +50,46 @@ class cdnfly implements DeployInterface
'cert' => $fullchain,
'key' => $privatekey,
];
$this->request('/v1/certs/' . $id, $params, 'PUT');
if ($this->auth == 1) {
$access_token = $this->login();
$url = $this->url . '/v1/certs/' . $id;
$body = json_encode($params);
$headers = [
'Access-Token' => $access_token,
];
$response = http_request($url, $body, null, null, $headers, $this->proxy, 'PUT');
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 0) {
} elseif (isset($result['msg'])) {
throw new Exception('证书ID:' . $id . '更新失败,' . $result['msg']);
} else {
throw new Exception('证书ID:' . $id . '更新失败,返回数据解析失败');
}
} else {
$this->request('/v1/certs/' . $id, $params, 'PUT');
}
$this->log("证书ID:{$id}更新成功!");
}
public function login()
{
$url = $this->url . '/v1/login';
$params = [
'account' => $this->username,
'password' => $this->password,
];
$body = json_encode($params);
$response = http_request($url, $body, null, null, null, $this->proxy);
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 0) {
return $result['data']['access_token'];
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception('登录失败,返回数据解析失败');
}
}
private function request($path, $params = null, $method = null)
{
$url = $this->url . $path;

View File

@@ -38,6 +38,7 @@ class doge implements DeployInterface
$cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name);
foreach (explode(',', $domains) as $domain) {
if (empty($domain)) continue;
$param = [
'id' => $cert_id,
'domain' => $domain,

132
app/lib/deploy/fnos.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class fnos implements DeployInterface
{
private $logger;
private $config;
public function __construct($config)
{
$this->config = $config;
}
public function check()
{
$this->connect();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$connection = $this->connect();
$cert_all = $this->exec($connection, '获取证书列表', 'cat /usr/trim/etc/network_cert_all.conf');
$list = json_decode($cert_all, true);
if (!$list) throw new Exception('获取证书列表失败');
$success = 0;
foreach ($list as $row) {
if (empty($row['san'])) continue;
$cert_domains = $row['san'];
$flag = false;
foreach ($cert_domains as $domain) {
if (in_array($domain, $domains)) {
$flag = true;
break;
}
}
if ($flag) {
$certPath = $row['certificate'];
$keyPath = $row['privateKey'];
$certDir = dirname($certPath);
$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->log('证书 '.$row['domain'].' 更新成功');
$success++;
}
}
if ($success == 0) {
throw new Exception('没有要更新的证书');
} else {
$this->exec($connection, '重启webdav', 'sudo systemctl restart webdav.service');
$this->exec($connection, '重启smbftpd', 'sudo systemctl restart smbftpd.service');
$this->exec($connection, '重启trim_nginx', 'sudo systemctl restart trim_nginx.service');
}
}
private function exec($connection, $name, $cmd)
{
$stream = ssh2_exec($connection, $cmd);
$errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
if (!$stream || !$errorStream) {
throw new Exception($name.'执行命令失败');
}
stream_set_blocking($stream, true);
stream_set_blocking($errorStream, true);
$output = stream_get_contents($stream);
$errorOutput = stream_get_contents($errorStream);
fclose($stream);
fclose($errorStream);
if (trim($errorOutput)) {
if (strpos($errorOutput, 'a password is required') !== false) {
throw new Exception('权限不足,请先配置 sudo 免密');
}
throw new Exception($name.'失败:' . trim($errorOutput));
} else {
if (strlen($output) > 200) {
return $output;
}
$this->log($name.'成功 ' . trim($output));
return $output;
}
}
private function connect()
{
if (!function_exists('ssh2_connect')) {
throw new Exception('ssh2扩展未安装');
}
if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) {
throw new Exception('必填参数不能为空');
}
if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) {
throw new Exception('主机地址不合法');
}
if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) {
throw new Exception('SSH端口不合法');
}
$connection = ssh2_connect($this->config['host'], intval($this->config['port']));
if (!$connection) {
throw new Exception('SSH连接失败');
}
if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {
throw new Exception('用户名或密码错误');
}
return $connection;
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

View File

@@ -59,6 +59,7 @@ class huawei implements DeployInterface
],
];
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
$client->request('PUT', '/v1.1/cdn/configuration/domains/' . $domain . '/configs', null, $param);
$this->log('CDN域名 ' . $domain . ' 部署证书成功!');
}

View File

@@ -89,6 +89,7 @@ class huoshan implements DeployInterface
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $config['bucket_domain'], 'tos', '2021-04-01', 'cn-beijing', $this->proxy);
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
$param = [
'CustomDomainRule' => [
'Domain' => $domain,
@@ -122,6 +123,7 @@ class huoshan implements DeployInterface
$this->log('上传证书成功 ChainID=' . $result['ChainID']);
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
$param = [
'ChainID' => $result['ChainID'],
'Domain' => $domain,
@@ -138,6 +140,7 @@ class huoshan implements DeployInterface
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'imagex.volcengineapi.com', 'imagex', '2018-08-01', 'cn-north-1', $this->proxy);
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
$param = [
[
'domain' => $domain,

202
app/lib/deploy/k8s.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Symfony\Component\Yaml\Yaml;
use Exception;
class k8s implements DeployInterface
{
private $logger;
private $kubeconfig;
private $server;
private $bearerToken;
private $tls = [];
private $proxy;
public function __construct($config)
{
$this->kubeconfig = $config['kubeconfig'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->kubeconfig)) throw new Exception('Kubeconfig不能为空');
$this->verify();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$namespace = $config['namespace'];
$secretName = $config['secret_name'];
if (empty($namespace)) throw new Exception('命名空间不能为空');
if (empty($secretName)) throw new Exception('Secret名称不能为空');
$this->parse();
$secretPayload = [
'apiVersion' => 'v1',
'kind' => 'Secret',
'metadata' => ['name' => $secretName, 'namespace' => $namespace],
'type' => 'kubernetes.io/tls',
'data' => [
'tls.crt' => base64_encode($config['fullchain']),
'tls.key' => base64_encode($config['privatekey']),
],
];
$secretUrl = '/api/v1/namespaces/' . $namespace . '/secrets/' . $secretName;
list($sCode, $sBody, $sErr) = $this->k8s_request('GET', $secretUrl);
if ($sCode === 404) {
$createUrl = '/api/v1/namespaces/' . $namespace . '/secrets';
$this->log('Secret:' . $secretName . ' 不存在,正在创建...');
list($cCode, $cBody, $cErr) = $this->k8s_request('POST', $createUrl, json_encode($secretPayload));
if ($cCode < 200 || $cCode >= 300) throw new Exception("创建Secret失败 (HTTP $cCode): $cBody | $cErr");
$this->log('Secret:' . $namespace . ' 创建成功');
} elseif ($sCode >= 200 && $sCode < 300) {
$this->log('Secret:' . $secretName . ' 已存在,正在更新...');
$patch = ['data' => $secretPayload['data'], 'type' => 'kubernetes.io/tls'];
list($pCode, $pBody, $pErr) = $this->k8s_request('PATCH', $secretUrl, json_encode($patch));
if ($pCode < 200 || $pCode >= 300) throw new Exception("更新Secret失败 (HTTP $pCode): $pBody | $pErr");
$this->log('Secret:' . $secretName . ' 更新成功');
} else {
throw new Exception("获取Secret失败 (HTTP $sCode): $sBody | $sErr");
}
// Bind Secret to specified Ingresses (merge spec.tls & hosts) ----
if (!empty($config['ingresses'])) {
$ingressUrl = '/apis/networking.k8s.io/v1/namespaces/' . $namespace . '/ingresses';
foreach (explode(',', $config['ingresses']) as $ingName) {
list($gCode, $gBody, $gErr) = $this->k8s_request('GET', $ingressUrl . '/' . $ingName);
if ($gCode < 200 || $gCode >= 300) throw new Exception("获取Ingress '$ingName' 失败 (HTTP $gCode): $gBody | $gErr");
$ing = json_decode($gBody, true);
if (!$ing) throw new Exception("解析Ingress '$ingName' JSON失败: $gBody");
// collect hosts from spec.rules
$hosts = [];
foreach (($ing['spec']['rules'] ?? []) as $rule) {
if (!empty($rule['host'])) $hosts[] = $rule['host'];
}
$hosts = array_values(array_unique($hosts));
// merge/ensure spec.tls entry
$tls = $ing['spec']['tls'] ?? [];
$found = false;
foreach ($tls as &$entry) {
if (($entry['secretName'] ?? '') === $secretName) {
$found = true;
$existingHosts = $entry['hosts'] ?? [];
$entry['hosts'] = array_values(array_unique(array_merge($existingHosts, $hosts)));
}
}
unset($entry);
if (!$found) {
$tls[] = ['secretName' => $secretName, 'hosts' => $hosts];
}
$patch = ['spec' => ['tls' => $tls]];
list($iCode, $iBody, $iErr) = $this->k8s_request('PATCH', $ingressUrl . '/' . $ingName, json_encode($patch, JSON_UNESCAPED_SLASHES));
if ($iCode < 200 || $iCode >= 300) throw new Exception("更新Ingress '$ingName' 失败 (HTTP $iCode): $iBody | $iErr");
$this->log("Ingress '$ingName' 更新TLS成功");
}
}
}
private function parse()
{
$kcfg = Yaml::parse($this->kubeconfig);
if (!$kcfg) throw new Exception('Kubeconfig格式错误');
$curr = $kcfg['current-context'] ?? null;
if (!$curr) throw new Exception('Kubeconfig缺少current-context');
$contexts = $this->index_by_name($kcfg['contexts'] ?? []);
$clusters = $this->index_by_name($kcfg['clusters'] ?? []);
$users = $this->index_by_name($kcfg['users'] ?? []);
$ctx = $contexts[$curr] ?? null;
if (!$ctx) throw new Exception("Kubeconfig中找不到current-context: $curr");
$clusterName = $ctx['context']['cluster'] ?? null;
$userName = $ctx['context']['user'] ?? null;
if (!$clusterName || !$userName) throw new Exception("Kubeconfig中context缺少cluster或user: $curr");
$cluster = $clusters[$clusterName] ?? null;
$user = $users[$userName] ?? null;
if (!$cluster) throw new Exception("Kubeconfig中找不到cluster: $clusterName");
if (!$user) throw new Exception("Kubeconfig中找不到user: $userName");
$this->server = $cluster['cluster']['server'] ?? null;
if (!$this->server) throw new Exception("Kubeconfig中找不到cluster.server");
$this->server = rtrim($this->server, '/');
$this->bearerToken = $user['user']['token'] ?? ($user['user']['auth-provider']['config']['access-token'] ?? null);
$clientCertFile = $clientKeyFile = null;
if (!empty($user['user']['client-certificate-data']) && !empty($user['user']['client-key-data'])) {
$clientCertFile = tempnam(sys_get_temp_dir(), 'kcc_');
$clientKeyFile = tempnam(sys_get_temp_dir(), 'kck_');
file_put_contents($clientCertFile, base64_decode($user['user']['client-certificate-data']));
file_put_contents($clientKeyFile, base64_decode($user['user']['client-key-data']));
} elseif (!empty($user['user']['client-certificate']) && !empty($user['user']['client-key'])) {
$clientCertFile = $user['user']['client-certificate'];
$clientKeyFile = $user['user']['client-key'];
}
$this->tls = ['cert' => $clientCertFile, 'key' => $clientKeyFile];
}
private function verify()
{
$this->parse();
list($vCode, $vBody, $vErr) = $this->k8s_request('GET', '/version');
if ($vErr) throw new Exception("连接Kubernetes API服务器失败: $vErr");
if ($vCode != 200) throw new Exception("连接Kubernetes API服务器失败: HTTP $vCode $vBody");
}
private function k8s_request($method, $path, $body = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->server . $path);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$headers = ['Accept: application/json'];
if ($this->bearerToken) $headers[] = 'Authorization: Bearer ' . $this->bearerToken;
if ($body !== null) $headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
if (!empty($this->tls['cert']) && !empty($this->tls['key'])) {
curl_setopt($ch, CURLOPT_SSLCERT, $this->tls['cert']);
curl_setopt($ch, CURLOPT_SSLKEY, $this->tls['key']);
}
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
return [$code, $resp, $err];
}
private function index_by_name($arr)
{
$out = [];
foreach ($arr as $item) {
if (isset($item['name'])) $out[$item['name']] = $item;
}
return $out;
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

79
app/lib/deploy/ksyun.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Ksyun as KsyunClient;
use Exception;
class ksyun implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy);
$client->request('GET', 'GetCertificates', '2016-09-01', '/2016-09-01/cert/GetCertificates');
return true;
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$this->deploy_cdn($fullchain, $privatekey, $config, $info);
}
public function deploy_cdn($fullchain, $privatekey, $config, &$info)
{
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$domains = explode(',', $config['domain']);
$client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy);
$param = [
'PageSize' => 100,
'PageNumber' => 1,
];
$domain_ids = [];
$result = $client->request('GET', 'GetCdnDomains', '2019-06-01', '/2019-06-01/domain/GetCdnDomains', $param);
foreach ($result['Domains'] as $row) {
if (in_array($row['DomainName'], $domains)) {
$domain_ids[] = $row['DomainId'];
}
}
if (count($domain_ids) == 0) throw new Exception('未找到对应的CDN域名');
$param = [
'Enable' => 'on',
'DomainIds' => implode(',', $domain_ids),
'CertificateName' => $config['cert_name'],
'ServerCertificate' => $fullchain,
'PrivateKey' => $privatekey,
];
$result = $client->request('POST', 'ConfigCertificate', '2016-09-01', '/2016-09-01/cert/ConfigCertificate', $param);
$this->log('CDN证书部署成功证书ID' . $result['CertificateId']);
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

View File

@@ -11,6 +11,8 @@ class lecdn implements DeployInterface
private $url;
private $email;
private $password;
private $auth;
private $apiKey;
private $proxy;
private $accessToken;
@@ -19,13 +21,22 @@ class lecdn implements DeployInterface
$this->url = rtrim($config['url'], '/');
$this->email = $config['email'];
$this->password = $config['password'];
$this->auth = isset($config['auth']) ? intval($config['auth']) : 0;
if ($this->auth == 1) {
$this->apiKey = $config['api_key'];
}
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空');
$this->login();
if ($this->auth == 1) {
if (empty($this->url) || empty($this->apiKey)) throw new Exception('API访问令牌不能为空');
$this->request('/prod-api/system/info');
} else {
if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空');
$this->login();
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
@@ -33,7 +44,9 @@ class lecdn implements DeployInterface
$id = $config['id'];
if (empty($id)) throw new Exception('证书ID不能为空');
$this->login();
if ($this->auth == 0) {
$this->login();
}
try {
$data = $this->request('/prod-api/certificate/' . $id);
@@ -77,6 +90,8 @@ class lecdn implements DeployInterface
$body = null;
if ($this->accessToken) {
$headers['Authorization'] = 'Bearer ' . $this->accessToken;
} elseif ($this->auth == 1 && $this->apiKey) {
$headers['Authorization'] = $this->apiKey;
}
if ($params) {
$headers['Content-Type'] = 'application/json;charset=UTF-8';

View File

@@ -48,7 +48,7 @@ class opanel implements DeployInterface
if (!empty($row['domains'])) $cert_domains += explode(',', $row['domains']);
$flag = false;
foreach ($cert_domains as $domain) {
if (in_array($domain, $domains)) {
if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) {
$flag = true;
break;
}

View File

@@ -39,6 +39,7 @@ class qiniu implements DeployInterface
$cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name);
foreach (explode(',', $domains) as $domain) {
if (empty($domain)) continue;
if ($config['product'] == 'cdn') {
$this->deploy_cdn($domain, $cert_id);
} elseif ($config['product'] == 'oss') {

View File

@@ -43,7 +43,7 @@ class safeline implements DeployInterface
if (empty($row['domains'])) continue;
$flag = false;
foreach ($row['domains'] as $domain) {
if (in_array($domain, $domains)) {
if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) {
$flag = true;
break;
}

View File

@@ -109,19 +109,30 @@ class synology implements DeployInterface
'_sid' => $this->token['sid'],
'SynoToken' => $this->token['synotoken'],
];
$privatekey_file = tempnam(sys_get_temp_dir(), 'privatekey');
file_put_contents($privatekey_file, $privatekey);
$fullchain_file = tempnam(sys_get_temp_dir(), 'fullchain');
file_put_contents($fullchain_file, $fullchain);
$post = [
'key' => new \CURLFile($privatekey_file),
'cert' => new \CURLFile($fullchain_file),
'id' => $id,
'desc' => $config['desc'],
$headers = [
'Content-Type' => 'multipart/form-data'
];
$response = http_request($url . '?' . http_build_query($params), $post, null, null, null, $this->proxy, null, 15);
unlink($privatekey_file);
unlink($fullchain_file);
$post = [
[
'name' => 'key',
'filename' => 'key.pem',
'contents' => $privatekey
],
[
'name' => 'cert',
'filename' => 'cert.pem',
'contents' => $fullchain
],
[
'name' => 'id',
'contents' => $id
],
[
'name' => 'desc',
'contents' => $config['desc']
]
];
$response = http_request($url . '?' . http_build_query($params), $post, null, null, $headers, $this->proxy, null, 15);
$result = json_decode($response['body'], true);
if ($id) {
if (isset($result['success']) && $result['success']) {

View File

@@ -37,34 +37,20 @@ class ucloud implements DeployInterface
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$param = [
'CertName' => $cert_name,
'UserCert' => $fullchain,
'PrivateKey' => $privatekey,
];
try {
$data = $this->client->request('GetCertificateV2', []);
$data = $this->client->request('AddCertificate', $param);
$this->log('添加证书成功,名称:' . $cert_name);
} catch (Exception $e) {
throw new Exception('获取证书列表失败 ' . $e->getMessage());
}
$exist = false;
foreach ($data['CertList'] as $cert) {
if (trim($cert['UserCert']) == trim($fullchain)) {
$cert_name = $cert['CertName'];
$exist = true;
}
}
if (!$exist) {
$param = [
'CertName' => $cert_name,
'UserCert' => $fullchain,
'PrivateKey' => $privatekey,
];
try {
$data = $this->client->request('AddCertificate', $param);
} catch (Exception $e) {
if (strpos($e->getMessage(), 'cert already exist') !== false) {
$this->log('证书已存在,名称:' . $cert_name);
} else {
throw new Exception('添加证书失败 ' . $e->getMessage());
}
$this->log('添加证书成功,名称:' . $cert_name);
} else {
$this->log('获取到已添加的证书,名称:' . $cert_name);
}
try {

212
app/lib/deploy/unicloud.php Normal file
View File

@@ -0,0 +1,212 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class unicloud implements DeployInterface
{
private $logger;
private $username;
private $password;
private $deviceId;
private $proxy;
private $token;
public function __construct($config)
{
$this->username = $config['username'];
$this->password = $config['password'];
$this->proxy = $config['proxy'] == 1;
$this->deviceId = getMillisecond() . random(7, 1);
}
public function check()
{
if (empty($this->username) || empty($this->password)) throw new Exception('账号或密码不能为空');
$this->login();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (empty($config['domains'])) throw new Exception('绑定的域名不能为空');
$this->getToken();
$url = 'https://unicloud-api.dcloud.net.cn/unicloud/api/host/create-domain-with-cert';
foreach (explode(',', $config['domains']) as $domain) {
if (empty($domain)) continue;
$params = [
'appid' => '',
'provider' => $config['provider'],
'spaceId' => $config['spaceId'],
'domain' => $domain,
'cert' => rawurlencode($fullchain),
'key' => rawurlencode($privatekey),
];
$post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$headers = [
'Token' => $this->token,
];
$response = http_request($url, $post, null, null, $headers, $this->proxy);
$result = json_decode($response['body'], true);
if (isset($result['ret']) && $result['ret'] == 0) {
$this->log('域名:' . $domain . ' 证书更新成功!');
} elseif(isset($result['desc'])) {
throw new Exception('域名:' . $domain . ' 证书更新失败:' . $result['desc']);
} else {
throw new Exception('域名:' . $domain . ' 证书更新失败:' . $response['body']);
}
}
}
private function login()
{
$url = 'https://account.dcloud.net.cn/client';
$clientInfo = $this->getClientInfo('__UNI__unicloud_console', '账号中心');
$bizParams = [
'functionTarget' => 'uni-id-co',
'functionArgs' => [
'method' => 'login',
'params' => [[
'password' => $this->password,
'captcha' => '',
'resetAppId' => '__UNI__unicloud_console',
'resetUniPlatform' => 'web',
'isReturnToken' => false,
'email' => $this->username,
]],
'clientInfo' => $clientInfo,
],
];
$params = [
'method' => 'serverless.function.runtime.invoke',
'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'spaceId' => 'uni-id-server',
'timestamp' => getMillisecond(),
];
$sign = $this->sign($params, 'ba461799-fde8-429f-8cc4-4b6d306e2339');
$post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$headers = [
'Origin' => 'https://account.dcloud.net.cn',
'Referer' => 'https://account.dcloud.net.cn/',
'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'X-Serverless-Sign' => $sign,
];
$response = http_request($url, $post, null, null, $headers, $this->proxy);
$result = json_decode($response['body'], true);
if (isset($result['success']) && $result['success'] == true) {
if (isset($result['data']['errCode']) && $result['data']['errCode'] == 0) {
return $result['data']['newToken']['token'];
} else {
throw new Exception('登录失败:' . $result['data']['errMsg']);
}
} else {
throw new Exception('登录失败:' . $response['body']);
}
}
private function getToken()
{
$uniIdToken = $this->login();
$url = 'https://unicloud.dcloud.net.cn/client';
$clientInfo = $this->getClientInfo('__UNI__unicloud_console', 'uniCloud控制台');
$bizParams = [
'functionTarget' => 'uni-cloud-kernel',
'functionArgs' => [
'action' => 'user/getUserToken',
'data' => [
'isLogin' => true
],
'clientInfo' => $clientInfo,
'uniIdToken' => $uniIdToken,
],
];
$params = [
'method' => 'serverless.function.runtime.invoke',
'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'spaceId' => 'dc-6nfabcn6ada8d3dd',
'timestamp' => getMillisecond(),
];
$sign = $this->sign($params, '4c1f7fbf-c732-42b0-ab10-4634a8bbe834');
$post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$headers = [
'Origin' => 'https://account.dcloud.net.cn',
'Referer' => 'https://account.dcloud.net.cn/',
'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'X-Client-Token' => $uniIdToken,
'X-Serverless-Sign' => $sign,
];
$response = http_request($url, $post, null, null, $headers, $this->proxy);
$result = json_decode($response['body'], true);
if (isset($result['success']) && $result['success'] == true) {
if (isset($result['data']['code']) && $result['data']['code'] == 0) {
if (isset($result['data']['data']['ret']) && $result['data']['data']['ret'] == 0) {
$this->token = $result['data']['data']['data']['token'];
return $result['data']['data']['data']['token'];
} else {
throw new Exception('获取token失败:' . $result['data']['data']['desc']);
}
} else {
throw new Exception('获取token失败:' . $response['body']);
}
} else {
throw new Exception('获取token失败:' . $response['body']);
}
}
private function getClientInfo($appId, $appName, $appVersion = '1.0.0', $appVersionCode = '100')
{
$clientInfo = [
'PLATFORM' => 'web',
'OS' => 'windows',
'APPID' => $appId,
'DEVICEID' => $this->deviceId,
'scene' => 1001,
'appId' => $appId,
'appLanguage' => 'zh-Hans',
'appName' => $appName,
'appVersion' => $appVersion,
'appVersionCode' => $appVersionCode,
'browserName' => 'chrome',
'browserVersion' => '122.0.6261.95',
'deviceId' => $this->deviceId,
'deviceModel' => 'PC',
'deviceType' => 'pc',
'hostName' => 'chrome',
'hostVersion' => '122.0.6261.95',
'osName' => 'windows',
'osVersion' => '10 x64',
'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'uniCompilerVersion' => '4.45',
'uniPlatform' => 'web',
'uniRuntimeVersion' => '4.45',
'locale' => 'zh-Hans',
'LOCALE' => 'zh-Hans',
];
return $clientInfo;
}
private function sign($data, $key)
{
ksort($data);
$signstr = '';
foreach ($data as $k => $v) {
$signstr .= $k . '=' . $v . '&';
}
$signstr = rtrim($signstr, '&');
return hash_hmac('md5', $signstr, $key);
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

View File

@@ -101,8 +101,8 @@ class upyun implements DeployInterface
$result = json_decode($response['body'], true);
if (isset($result['data']['result']) && $result['data']['result'] == true) {
$cookie = '';
if (isset($response['headers']['Set-Cookie'])) {
foreach ($response['headers']['Set-Cookie'] as $val) {
if (isset($response['headers']['set-cookie'])) {
foreach ($response['headers']['set-cookie'] as $val) {
$arr = explode('=', $val);
if ($arr[1] == '' || $arr[1] == 'deleted') continue;
$cookie .= $val . '; ';

103
app/lib/deploy/uusec.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class uusec implements DeployInterface
{
private $logger;
private $url;
private $username;
private $password;
private $proxy;
private $accessToken;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->username = $config['username'];
$this->password = $config['password'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->password) || empty($this->password)) throw new Exception('用户名和密码不能为空');
$this->login();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$id = $config['id'];
if (empty($id)) throw new Exception('证书ID不能为空');
$this->login();
$params = [
'id' => intval($id),
'type' => 1,
'name' => $config['name'],
'crt' => $fullchain,
'key' => $privatekey,
];
$result = $this->request('/api/v1/certs', $params, 'PUT');
if (is_string($result) && $result == 'OK') {
$this->log('证书ID:' . $id . '更新成功!');
} else {
throw new Exception('证书ID:' . $id . '更新失败,' . (isset($result['err']) ? $result['err'] : '未知错误'));
}
}
private function login()
{
$path = '/api/v1/users/login';
$params = [
'usr' => $this->username,
'pwd' => $this->password,
'otp' => '',
];
$result = $this->request($path, $params);
if (isset($result['token'])) {
$this->accessToken = $result['token'];
} else {
throw new Exception('登录失败,' . (isset($result['err']) ? $result['err'] : '未知错误'));
}
}
private function request($path, $params = null, $method = null)
{
$url = $this->url . $path;
$headers = [];
$body = null;
if ($this->accessToken) {
$headers['Authorization'] = 'Bearer ' . $this->accessToken;
}
if ($params) {
$headers['Content-Type'] = 'application/json;charset=UTF-8';
$body = json_encode($params);
}
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
$result = json_decode($response['body'], true);
if ($response['code'] == 200) {
return $result;
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} else {
throw new Exception('请求失败HTTP状态码' . $response['code']);
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

113
app/lib/deploy/xp.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class xp 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('请填写面板地址和接口密钥');
$path = '/openApi/siteList';
$response = $this->request($path);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 1000) {
return true;
} else {
throw new Exception(isset($result['message']) ? $result['message'] : '面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$path = '/openApi/siteList';
$response = $this->request($path);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 1000) {
$sites = explode("\n", $config['sites']);
$sites = array_map('trim', $sites);
$success = 0;
$errmsg = null;
foreach ($result['data'] as $item) {
if (!in_array($item['name'], $sites)) {
continue;
}
try {
$this->deploySite($item['id'], $fullchain, $privatekey);
$this->log("网站 {$item['name']} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("网站 {$item['name']} 证书部署失败:" . $errmsg);
}
}
if ($success == 0) {
throw new Exception($errmsg ? $errmsg : '要部署的网站不存在');
}
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} else {
throw new Exception($response ? $response : '返回数据解析失败');
}
}
private function deploySite($id, $fullchain, $privatekey)
{
$path = '/openApi/setSSL';
$data = [
'id' => $id,
'key' => $privatekey,
'pem' => $fullchain,
];
$response = $this->request($path, $data);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 1000) {
return true;
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} 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 = null)
{
$url = $this->url . $path;
$headers = [
'XP-API-KEY' => $this->apikey,
];
$response = http_request($url, $params ? json_encode($params) : null, null, null, $headers, $this->proxy);
return $response['body'];
}
}

View File

@@ -29,7 +29,7 @@ class CertDeployService
$this->client = DeployHelper::getModel($this->aid);
if (!$this->client) throw new Exception('该自动部署任务类型不存在', 102);
$this->info = $task['info'] ? json_decode($task['info'], true) : null;
$this->info = $task['info'] ? json_decode($task['info'], true) : [];
}
public function process($isManual = false)

View File

@@ -11,15 +11,17 @@ class CertTaskService
public function execute()
{
$this->execute_deploy();
$this->execute_order();
(new ExpireNoticeService())->task();
config_set('certtask_time', date("Y-m-d H:i:s"));
echo 'done'.PHP_EOL;
if ($this->execute_deploy()) {
config_set('certdeploy_time', date("Y-m-d H:i:s"));
}
if ($this->execute_order()) {
config_set('certtask_time', date("Y-m-d H:i:s"));
}
}
private function execute_order()
{
echo '开始执行SSL证书签发任务...'.PHP_EOL;
$days = config_get('cert_renewdays', 7);
$list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime<NOW()) OR status=3 AND isauto=1 AND expiretime<:expiretime', ['expiretime' => date('Y-m-d H:i:s', time() + $days * 86400)])->select();
//print_r($list);exit;
@@ -55,6 +57,7 @@ class CertTaskService
if ($failcount >= 3) break;
sleep(1);
}
return true;
}
private function execute_deploy()
@@ -64,14 +67,15 @@ class CertTaskService
$hour = date('H');
if($start <= $end){
if($hour < $start || $hour > $end){
echo '不在部署任务运行时间范围内'.PHP_EOL; return;
echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
}
}else{
if($hour < $start && $hour > $end){
echo '不在部署任务运行时间范围内'.PHP_EOL; return;
echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
}
}
echo '开始执行SSL证书部署任务...'.PHP_EOL;
$list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('active=1 AND status IN (0,-1) AND (retrytime IS NULL OR retrytime<NOW())')->select();
//print_r($list);exit;
$count = 0;
@@ -95,5 +99,6 @@ class CertTaskService
if ($count >= 3) break;
sleep(1);
}
return true;
}
}

View File

@@ -26,6 +26,8 @@ class ExpireNoticeService
public function task()
{
echo '开始执行域名到期提醒任务...' . PHP_EOL;
config_set('domain_expire_time', date("Y-m-d H:i:s"));
$count = $this->refreshDomainList();
if ($count > 0) return;

View File

@@ -96,7 +96,15 @@ class OptimizeService
//批量执行优选任务
public function execute()
{
$minute = config_get('optimize_ip_min', '30');
$last = config_get('optimize_ip_time', null, true);
if ($last && strtotime($last) > time() - $minute * 60) {
return false;
}
$list = Db::name('optimizeip')->where('active', 1)->select();
if (count($list) == 0) {
return false;
}
echo '开始执行IP优选任务共获取到'.count($list).'个待执行任务'."\n";
foreach ($list as $row) {
try {
@@ -108,6 +116,8 @@ class OptimizeService
echo '优选任务'.$row['id'].'执行失败:'.$e->getMessage()."\n";
}
}
config_set('optimize_ip_time', date("Y-m-d H:i:s"));
return true;
}
//执行单个优选任务

View File

@@ -0,0 +1,118 @@
<?php
namespace app\service;
use Exception;
use think\facade\Db;
use app\lib\DnsHelper;
/**
* 域名定时切换解析
*/
class ScheduleService
{
public function execute()
{
$list = Db::name('sctask')->where('nexttime', '>', 0)->where('nexttime', '<=', time())->where('active', 1)->select();
if (count($list) == 0) {
return false;
}
echo '开始执行定时切换解析任务,共获取到' . count($list) . '个待执行任务' . "\n";
foreach ($list as $row) {
try {
$this->execute_one($row);
echo '定时切换任务' . $row['id'] . '执行成功' . "\n";
} catch (Exception $e) {
echo '定时切换任务' . $row['id'] . '执行失败,' . $e->getMessage() . "\n";
}
}
config_set('schedule_time', date("Y-m-d H:i:s"));
return true;
}
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();
if (!$drow) throw new Exception('域名不存在');
Db::name('sctask')->where('id', $row['id'])->update(['updatetime' => time()]);
$domain = $row['rr'] . '.' . $drow['name'];
$dns = DnsHelper::getModel2($drow);
if ($row['switchtype'] == 1) {
$res = $dns->setDomainRecordStatus($row['recordid'], '1');
if ($res) {
$this->add_log($domain, '启用解析', '定时启用解析成功');
} else {
$this->add_log($domain, '启用解析失败', $dns->getError());
}
} elseif ($row['switchtype'] == 2) {
$res = $dns->setDomainRecordStatus($row['recordid'], '0');
if ($res) {
$this->add_log($domain, '暂停解析', '定时暂停解析成功');
} else {
$this->add_log($domain, '暂停解析失败', $dns->getError());
}
} elseif ($row['switchtype'] == 3) {
$res = $dns->deleteDomainRecord($row['recordid']);
if ($res) {
$this->add_log($domain, '删除解析', '定时删除解析成功');
} else {
$this->add_log($domain, '删除解析失败', $dns->getError());
}
} else {
$recordinfo = json_decode($row['recordinfo'], true);
if ($drow['type'] == 'cloudflare' && !isNullOrEmpty($row['line'])) {
$recordinfo['Line'] = $row['line'];
}
$res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['value']), $row['value'], $recordinfo['Line'], $recordinfo['TTL']);
if ($res) {
$this->add_log($domain, '修改解析', $row['rr'].' ['.getDnsType($row['value']).'] '.$row['value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')');
} else {
$this->add_log($domain, '修改解析失败', $dns->getError());
}
}
$this->update_nexttime($row);
}
public function update_nexttime($row)
{
if ($row['type'] == 1) {
if ($row['cycle'] == 2) {
$date = intval($row['switchdate']);
$nexttime = strtotime(date('Y-m-') . $date . ' ' . $row['switchtime'] . ':00');
if ($nexttime <= time()) {
$nexttime = strtotime("+1 month", $nexttime);
}
} elseif ($row['cycle'] == 1) {
$weekday = intval($row['switchdate']); // 0-6, 0=周日
$nexttime = strtotime("last Sunday +{$weekday} days {$row['switchtime']}:00");
if ($nexttime <= time()) {
$nexttime = strtotime("+1 week", $nexttime);
if ($nexttime <= time()) {
$nexttime = strtotime("+1 week", $nexttime);
}
}
} else {
$nexttime = strtotime(date('Y-m-d') . ' ' . $row['switchtime'] . ':00');
if ($nexttime <= time()) {
$nexttime = strtotime("+1 day", $nexttime);
}
}
} else {
$nexttime = strtotime($row['switchtime'] . ':00');
if ($nexttime <= time()) {
$nexttime = 0;
}
}
Db::name('sctask')->where('id', $row['id'])->update(['nexttime' => $nexttime]);
}
private function add_log($domain, $action, $data)
{
if (strlen($data) > 500) $data = substr($data, 0, 500);
Db::name('log')->insert(['uid' => 0, 'domain' => $domain, 'action' => $action, 'data' => $data, 'addtime' => date("Y-m-d H:i:s")]);
}
}

View File

@@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `dnsmgr_config` VALUES ('version', '1033');
INSERT INTO `dnsmgr_config` VALUES ('version', '1040');
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');
@@ -230,4 +230,27 @@ CREATE TABLE `dnsmgr_cert_cname` (
`addtime` datetime DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_sctask`;
CREATE TABLE `dnsmgr_sctask` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`recordid` varchar(60) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`cycle` tinyint(1) NOT NULL DEFAULT 0,
`switchtype` tinyint(1) NOT NULL DEFAULT 0,
`switchdate` varchar(10) DEFAULT NULL,
`switchtime` varchar(20) DEFAULT NULL,
`value` varchar(128) DEFAULT NULL,
`line` varchar(20) DEFAULT NULL,
`addtime` int(11) NOT NULL DEFAULT 0,
`updatetime` int(11) NOT NULL DEFAULT 0,
`nexttime` int(11) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`recordinfo` varchar(200) DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -163,4 +163,26 @@ ADD COLUMN `regtime` datetime DEFAULT NULL,
ADD COLUMN `expiretime` datetime DEFAULT NULL,
ADD COLUMN `checktime` datetime DEFAULT NULL,
ADD COLUMN `noticetime` datetime DEFAULT NULL,
ADD COLUMN `checkstatus` tinyint(1) NOT NULL DEFAULT '0';
ADD COLUMN `checkstatus` tinyint(1) NOT NULL DEFAULT '0';
CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`recordid` varchar(60) NOT NULL,
`type` tinyint(1) NOT NULL DEFAULT 0,
`cycle` tinyint(1) NOT NULL DEFAULT 0,
`switchtype` tinyint(1) NOT NULL DEFAULT 0,
`switchdate` varchar(10) DEFAULT NULL,
`switchtime` varchar(20) DEFAULT NULL,
`value` varchar(128) DEFAULT NULL,
`line` varchar(20) DEFAULT NULL,
`addtime` int(11) NOT NULL DEFAULT 0,
`updatetime` int(11) NOT NULL DEFAULT 0,
`nexttime` int(11) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 0,
`recordinfo` varchar(200) DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -126,7 +126,11 @@ class CheckUtils
return ['status' => false, 'errmsg' => 'Invalid IP address', 'usetime' => 0];
}
$timeout = 1;
exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var);
if (str_contains($target, ':')) {
exec('ping -6 -c 1 -w '.$timeout.' '.$target, $output, $return_var);
} else {
exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var);
}
if (!empty($output[1])) {
if (strpos($output[1], '毫秒') !== false) {
$usetime = getSubstr($output[1], '时间=', ' 毫秒');

View File

@@ -182,7 +182,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>

View File

@@ -3,14 +3,6 @@
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
<p><li>计划任务将以下命令添加到计划任务1分钟1次</li></p>
<p><code>cd {:app()->getRootPath()} && php think certtask</code></p>
<p><li>上次运行时间:<font color="green">{:config_get('certtask_time', '未运行', true)}</font></li></p>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">自动续签设置</h3></div>
@@ -28,7 +20,7 @@
</form>
</div>
<div class="panel-footer">
<li>提示:只有已开启自动续签的证书,才会自动续签。</li>
<li>提示:只有已开启自动续签的证书,并添加<a href="/system/cronset">计划任务</a>才会自动续签。</li>
</div>
</div>

View File

@@ -102,7 +102,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}select2/4.0.13/js/select2.min.js"></script>
<script src="{$cdnpublic}select2/4.0.13/js/i18n/zh-CN.min.js"></script>

View File

@@ -77,7 +77,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>

View File

@@ -126,18 +126,8 @@
<li class="{:checkIfActive('task,taskform')}"><a href="/dmonitor/task"><i class="fa fa-circle-o"></i> 切换策略</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('opipset,opiplist,opipform')}">
<a href="javascript:;">
<i class="fa fa-globe fa-fw"></i>
<span>CF优选IP</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li class="{:checkIfActive('opipset')}"><a href="/optimizeip/opipset"><i class="fa fa-circle-o"></i> 优选设置</a></li>
<li class="{:checkIfActive('opiplist,opipform')}"><a href="/optimizeip/opiplist"><i class="fa fa-circle-o"></i> 任务管理</a></li>
</ul>
<li class="{:checkIfActive('stask,staskform')}">
<a href="/schedule/stask"><i class="fa fa-calendar fa-fw"></i> <span>定时切换</span></a>
</li>
<li class="treeview {:checkIfActive('certaccount,account_form,certorder,order_form,order_import,deployaccount,deploytask,deploy_form,certset,cname')}">
<a href="javascript:;">
@@ -153,10 +143,23 @@
<li class="{:checkIfActive('deployaccount')}"><a href="/cert/deployaccount"><i class="fa fa-circle-o"></i> 自动部署账户</a></li>
<li class="{:checkIfActive('deploytask,deploy_form')}"><a href="/cert/deploytask"><i class="fa fa-circle-o"></i> 自动部署任务</a></li>
<li class="{:checkIfActive('cname')}"><a href="/cert/cname"><i class="fa fa-circle-o"></i> CNAME代理</a></li>
<li class="{:checkIfActive('certset')}"><a href="/cert/certset"><i class="fa fa-circle-o"></i> 计划任务设置</a></li>
<li class="{:checkIfActive('certset')}"><a href="/cert/certset"><i class="fa fa-circle-o"></i> 自动续签设置</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('loginset,noticeset,proxyset')}">
<li class="treeview {:checkIfActive('opipset,opiplist,opipform')}">
<a href="javascript:;">
<i class="fa fa-globe fa-fw"></i>
<span>CF优选IP</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li class="{:checkIfActive('opipset')}"><a href="/optimizeip/opipset"><i class="fa fa-circle-o"></i> 优选设置</a></li>
<li class="{:checkIfActive('opiplist,opipform')}"><a href="/optimizeip/opiplist"><i class="fa fa-circle-o"></i> 任务管理</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('cronset,loginset,noticeset,proxyset')}">
<a href="javascript:;">
<i class="fa fa-cogs fa-fw"></i>
<span>系统设置</span>
@@ -165,6 +168,7 @@
</span>
</a>
<ul class="treeview-menu">
<li class="{:checkIfActive('cronset')}"><a href="/system/cronset"><i class="fa fa-circle-o"></i> 计划任务</a></li>
<li class="{:checkIfActive('loginset')}"><a href="/system/loginset"><i class="fa fa-circle-o"></i> 登录设置</a></li>
<li class="{:checkIfActive('noticeset')}"><a href="/system/noticeset"><i class="fa fa-circle-o"></i> 通知设置</a></li>
<li class="{:checkIfActive('proxyset')}"><a href="/system/proxyset"><i class="fa fa-circle-o"></i> 代理设置</a></li>

View File

@@ -160,7 +160,7 @@
<p>1、php需要安装swoole组件</p>
<p>2、在命令行执行以下命令启动进程</p>
<p><code>cd {:app()->getRootPath()} && php think dmtask</code></p>
<p>3、也可以使用进程守护管理器添加守护进程,运行目录:{:app()->getRootPath()},启动命令:php think dmtask</p>
<p>3、也可以使用进程守护管理器添加守护进程<br/>运行目录:<code>{:app()->getRootPath()}</code><br/>启动命令:<code>php think dmtask</code></p>
</div>
</div>
</div>

View File

@@ -2,7 +2,8 @@
{block name="title"}容灾切换策略{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(2){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
tbody tr>td:nth-child(3){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
tbody tr>td:nth-child(4){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:200px;}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
@@ -27,6 +28,10 @@ tbody tr>td:nth-child(2){overflow: hidden;text-overflow: ellipsis;white-space: n
<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="/dmonitor/task/add" 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="javascript:operation('open')">开启运行</a></li><li><a href="javascript:operation('close')">停止运行</a></li><li><a href="javascript:operation('retry')">立即重试</a></li><li><a href="javascript:operation('delete')">删除</a></li></ul>
</div>
</form>
<table id="listTable">
@@ -54,6 +59,10 @@ $(document).ready(function(){
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: '',
checkbox: true
},
{
field: 'id',
title: 'ID'
@@ -87,21 +96,28 @@ $(document).ready(function(){
}
}
},
{
field: 'checktype',
title: '检测协议',
formatter: function(value, row, index) {
if(row.type <= 2){
if(value == 1) {
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="'+row.tcpport+'端口" class="tips">TCP</span>';
} else if(value == 2) {
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="'+row.checkurl+'" class="tips">HTTP(S)</span>';
} else {
return 'PING';
}
} else {
return '无';
}
}
},
{
field: 'frequency',
title: '检测间隔',
formatter: function(value, row, index) {
if(row.type <= 2){
var checktype = 'PING';
if(row.checktype == 2){
checktype = row.checkurl;
}else if(row.checktype == 1){
checktype = 'TCP('+row.tcpport+'端口)';
}
}else{
var checktype = '';
}
return '<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="'+checktype+'" class="tips">' + value + '秒</span>';
return value + '秒';
}
},
{
@@ -127,11 +143,18 @@ $(document).ready(function(){
}
},
{
field: 'checktime',
title: '上次检测时间',
formatter: function(value, row, index) {
return value > 0 ? row.checktimestr : '未运行';
}
field: 'checktimestr',
title: '上次检测时间'
},
{
field: 'addtimestr',
title: '添加时间',
visible: false
},
{
field: 'remark',
title: '备注',
visible: false
},
{
field: 'action',
@@ -139,7 +162,7 @@ $(document).ready(function(){
formatter: function(value, row, index) {
var html = '<a href="/dmonitor/task/info/'+row.id+'" class="btn btn-info btn-xs">切换日志</a>&nbsp;&nbsp;';
html += '<a href="/dmonitor/task/edit?id='+row.id+'" class="btn btn-primary btn-xs">修改</a>&nbsp;&nbsp;';
html += '<a href="/record/'+row.did+'?keyword='+row.rr+'" class="btn btn-default btn-xs" target="_blank">解析</a>&nbsp;&nbsp;';
html += '<a href="/record/'+row.did+'?subdomain='+row.rr+'" class="btn btn-default btn-xs" target="_blank">解析</a>&nbsp;&nbsp;';
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
return html;
}
@@ -174,5 +197,37 @@ function delItem(id){
}, 'json');
});
}
function operation(action){
var rows = $("#listTable").bootstrapTable('getSelections');
if(rows.length == 0){
layer.msg('请选择要操作的策略');
return;
}
var ids = [];
for(var i in rows){
ids.push(rows[i].id);
}
if(action == 'delete'){
if(!confirm('确定要删除所选策略吗?')) return;
}
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/dmonitor/task/operation',
data : {act: action, 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});
}
}
});
}
</script>
{/block}

View File

@@ -55,7 +55,7 @@
<div class="form-group" v-show="set.type==2">
<label class="col-sm-3 control-label no-padding-right" is-required>备用解析记录</label>
<div class="col-sm-6">
<input type="text" name="backup_value" v-model="set.backup_value" placeholder="支持填写IPv4或CNAME地址" class="form-control" required>
<input type="text" name="backup_value" v-model="set.backup_value" placeholder="支持填写IP或CNAME地址" class="form-control" required>
</div>
</div>
<div class="form-group" v-show="set.type==2&&dnstype=='cloudflare'">
@@ -148,7 +148,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>

View File

@@ -90,7 +90,7 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
new Vue({

View File

@@ -84,7 +84,7 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
new Vue({

View File

@@ -170,7 +170,7 @@
var userLevel = "{:request()->user['level']}";
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const defaultPageSize = getCookie('domain_pagesize') ? getCookie('domain_pagesize') : 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
@@ -298,7 +298,12 @@ $(document).ready(function(){
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
}
},
onPageChange: function(number, size){
if(size != defaultPageSize){
setCookie('domain_pagesize', size, 24 * 3600 * 30);
}
},
})
$("#form-store select[name=aid]").change(function(){

View File

@@ -49,7 +49,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
new Vue({

View File

@@ -35,12 +35,8 @@
</div>
</form>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
<p>支持域名到期提醒+域名列表到期时间自动刷新。与SSL证书共用计划任务不需要单独添加计划任务。</p><p><a href="/cert/certset">查看计划任务说明</a></p>
<div class="panel-footer">
<p>需添加<a href="/system/cronset">计划任务</a>,支持域名到期提醒+域名列表到期时间自动刷新。</p>
</div>
</div>

View File

@@ -359,8 +359,7 @@ $(document).ready(function(){
],
onPageChange: function(number, size){
if(size != defaultPageSize){
defaultPageSize = size;
setCookie('record_pagesize', size);
setCookie('record_pagesize', size, 24 * 3600 * 30);
}
},
});

View File

@@ -100,7 +100,7 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>

View File

@@ -16,8 +16,7 @@
<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>接口密钥默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
<p><li>计划任务将以下命令添加到计划任务周期设置为15分钟以上</li></p>
<p><code>cd {:app()->getRootPath()} && php think opiptask</code></p>
<p><li>自动更新:可查看<a href="/system/cronset">计划任务设置</a></p>
</div>
</div>
</div>
@@ -43,6 +42,22 @@
</form>
</div>
</div>
<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">
<div class="form-group">
<label class="col-sm-3 control-label">自动更新时间间隔(分钟)</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_min" value="{:config_get('optimize_ip_min', '30')}" class="form-control" placeholder="单位:分钟"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,216 @@
{extend name="common/layout" /}
{block name="title"}定时切换策略{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(3){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
tbody tr>td:nth-child(5){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:200px;}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<select name="type" class="form-control"><option value="1">域名</option><option value="3">备用解析记录</option><option value="2">解析记录ID</option><option value="4">备注</option></select>
</div>
</div>
<div class="form-group">
<input type="text" class="form-control" name="kw" placeholder="">
</div>
<div class="form-group">
<div class="form-group">
<select name="stype" class="form-control"><option value="">执行方式</option><option value="0">单次执行</option><option value="1">周期执行</option></select>
</div>
</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="/schedule/stask/add" 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="javascript:operation('open')">开启运行</a></li><li><a href="javascript:operation('close')">停止运行</a></li><li><a href="javascript:operation('delete')">删除</a></li></ul>
</div>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/schedule/stask/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: '',
checkbox: true
},
{
field: 'id',
title: 'ID'
},
{
field: 'rr',
title: '域名',
formatter: function(value, row, index) {
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right">' + value + '.' + row.domain + '</span>';
}
},
{
field: 'type',
title: '时间设置',
formatter: function(value, row, index) {
if(value == 1){
var text = '<span class="label bg-purple">周期执行</span> ';
if(row.cycle == 1) {
weekday = ['日', '一', '二', '三', '四', '五', '六'];
text += '每周'+weekday[row.switchdate]+' ';
} else if(row.cycle == 2) {
text += '每月'+row.switchdate+'日 ';
} else {
text += '每天 ';
}
return text + row.switchtime;
}else{
return '<span class="label bg-aqua">单次执行</span> '+row.switchtime.replace('T', ' ');
}
}
},
{
field: 'switchtype',
title: '切换设置',
formatter: function(value, row, index) {
if(value == 1) {
return '启用解析';
} else if(value == 2) {
return '暂停解析';
} else if(value == 3) {
return '删除解析';
} else {
return '修改解析['+row.value+']';
}
}
},
{
field: 'active',
title: '运行开关',
formatter: function(value, row, index) {
if(value == 1){
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" checked onchange="setActive('+row.id+',0)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}else{
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" onchange="setActive('+row.id+',1)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}
}
},
{
field: 'updatetimestr',
title: '上次切换时间'
},
{
field: 'nexttimestr',
title: '下次切换时间',
visible: false
},
{
field: 'addtimestr',
title: '添加时间',
visible: false
},
{
field: 'remark',
title: '备注'
},
{
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var domain = row.rr + '.' + row.domain;
var html = '<a href="/log?uid=0&domain='+domain+'" class="btn btn-info btn-xs">切换日志</a>&nbsp;&nbsp;';
html += '<a href="/schedule/stask/edit?id='+row.id+'" class="btn btn-primary btn-xs">修改</a>&nbsp;&nbsp;';
html += '<a href="/record/'+row.did+'?subdomain='+row.rr+'" class="btn btn-default btn-xs" target="_blank">解析</a>&nbsp;&nbsp;';
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
return html;
}
},
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
}
})
})
function setActive(id, active){
$.post('/schedule/stask/setactive', {id: id, active: active}, function(data){
if(data.code == 0) {
layer.msg('修改成功', {icon: 1, time:800});
searchRefresh();
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
}
function delItem(id){
layer.confirm('确定要删除此切换策略吗?', {
btn: ['确定','取消']
}, function(){
$.post('/schedule/stask/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
searchRefresh();
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function operation(action){
var rows = $("#listTable").bootstrapTable('getSelections');
if(rows.length == 0){
layer.msg('请选择要操作的策略');
return;
}
var ids = [];
for(var i in rows){
ids.push(rows[i].id);
}
if(action == 'delete'){
if(!confirm('确定要删除所选策略吗?')) return;
}
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/schedule/stask/operation',
data : {act: action, 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});
}
}
});
}
</script>
{/block}

View File

@@ -0,0 +1,269 @@
{extend name="common/layout" /}
{block name="title"}定时切换策略{/block}
{block name="main"}
<style>
.dselect::before{
content: '.';
position: absolute;
left: 0;
}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
.tips{color: #f6a838; padding-left: 5px;}
</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="/schedule/stask" 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="taskform">
<div class="form-group">
<label class="col-sm-3 col-xs-12 control-label no-padding-right" is-required>域名选择</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" name="rr" v-model="set.rr" placeholder="主机记录" class="form-control" required>
<span class="input-group-addon">.</span>
<select name="did" v-model="set.did" class="form-control" required>
<option value="">--主域名--</option>
<option v-for="option in domainList" :value="option.id">{{option.name}}</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>解析记录</label>
<div class="col-sm-6"><div class="input-group">
<select name="recordid" v-model="set.recordid" id="recordid" class="form-control" required>
<option v-for="option in recordList" :value="option.RecordId">{{option.Value}} (线路:{{option.LineName}})</option>
</select>
<div class="input-group-btn">
<button type="button" @click="getRecordList" class="btn btn-info">点击获取</button>
</div>
</div></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>执行方式</label>
<div class="col-sm-6">
<label class="radio-inline">
<input type="radio" name="type" value="0" v-model="set.type"> 单次执行
</label>
<label class="radio-inline">
<input type="radio" name="type" value="1" v-model="set.type"> 周期执行
</label>
</div>
</div>
<div class="form-group" v-show="set.type==0">
<label class="col-sm-3 control-label no-padding-right" is-required>时间设置</label>
<div class="col-sm-6">
<input type="datetime-local" name="switchtime" v-model="set.switchtime" class="form-control" required>
</div>
</div>
<div class="form-group" v-show="set.type==1">
<label class="col-sm-3 control-label no-padding-right" is-required>时间设置</label>
<div class="col-sm-6">
<div class="input-group">
<select name="cycle" v-model="set.cycle" class="form-control" required>
<option value="0">每天</option>
<option value="1">每周</option>
<option value="2">每月</option>
</select>
<span class="input-group-addon" v-show="set.cycle!=0"></span>
<select name="switchdate" v-model="set.switchdate" class="form-control" required v-show="set.cycle==1">
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
<option value="6"></option>
</select>
<input type="number" name="switchdate" v-model="set.switchdate" class="form-control" required min="1" max="31" v-show="set.cycle==2" placeholder="日期1~31">
<span class="input-group-addon"></span>
<input type="time" name="switchtime" v-model="set.switchtime" class="form-control" required>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>切换设置</label>
<div class="col-sm-6">
<label class="radio-inline">
<input type="radio" name="switchtype" value="0" v-model="set.switchtype"> 修改解析
</label>
<label class="radio-inline">
<input type="radio" name="switchtype" value="1" v-model="set.switchtype"> 启用解析
</label>
<label class="radio-inline">
<input type="radio" name="switchtype" value="2" v-model="set.switchtype"> 暂停解析
</label>
<label class="radio-inline" v-show="set.type==0">
<input type="radio" name="switchtype" value="3" v-model="set.switchtype"> 删除解析
</label>
</div>
</div>
<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>
</div>
</div>
<div class="form-group" v-show="set.switchtype==0&&dnstype=='cloudflare'">
<label class="col-sm-3 control-label no-padding-right" is-required>线路</label>
<div class="col-sm-6">
<label class="radio-inline">
<input type="radio" name="line" value="" v-model="set.line"> 不修改
</label>
<label class="radio-inline">
<input type="radio" name="line" value="0" v-model="set.line"> 改为仅DNS模式
</label>
<label class="radio-inline">
<input type="radio" name="line" value="1" v-model="set.line"> 改为代理模式
</label>
</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">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
<div class="panel-footer">
<p>添加定时切换策略后,还需要配置好<a href="/system/cronset">计划任务</a>,才能自动切换。</p>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.7.16/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var action = '{$action}';
var info = {$info|json_encode|raw};
var domainList = {$domains|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
id: '',
remark: '',
rr: '',
did: '',
recordid: '',
recordinfo: '',
type: 0,
cycle: 0,
switchtype: 0,
switchdate: '',
switchtime: '',
value: '',
line: '',
},
dnstype: null,
domainList: domainList,
recordList: [],
},
watch: {
'set.recordid': function(val){
if(val == '') return;
var record = this.recordList.find(item => item.RecordId == val);
if(record){
this.set.recordinfo = JSON.stringify({Value:record.Value, Line:record.Line, LineName:record.LineName, TTL:record.TTL});
}
},
'set.did': function(val){
if(val == '') return;
this.dnstype = this.domainList.find(item => item.id == val).type;
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.$set(this.set, key, info[key])
})
var recordinfo = JSON.parse(this.set.recordinfo);
this.recordList = [{RecordId:this.set.recordid, Value:recordinfo.Value, Line:recordinfo.Line, LineName:recordinfo.LineName, TTL:recordinfo.TTL}];
}
$("#taskform").bootstrapValidator({
live: 'submitted',
});
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
getRecordList(){
var that = this;
if(this.set.did == ''){
layer.msg('请先选择域名', {time: 800});return;
}
if(this.set.rr == ''){
layer.msg('主机记录不能为空', {time: 800});return;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/record/list',
data : {id:this.set.did, rr:this.set.rr},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.msg('成功获取到'+data.data.length+'条解析记录', {icon:1, time:800});
that.recordList = data.data;
if(that.set.recordid){
var record = that.recordList.find(item => item.RecordId == that.set.recordid);
if(record){
that.set.recordinfo = JSON.stringify({Value:record.Value, Line:record.Line, LineName:record.LineName, TTL:record.TTL});
}
}
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
},
submit(){
var that=this;
$("#taskform").data("bootstrapValidator").validate();
if(!$("#taskform").data("bootstrapValidator").isValid()){
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(document.referrer.indexOf('task?') > 0)
window.location.href = document.referrer;
else
window.location.href = '/dmonitor/task';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
},
});
</script>
{/block}

View File

@@ -0,0 +1,130 @@
{extend name="common/layout" /}
{block name="title"}计划任务{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
{if config_get('cron_type', '0') == '1'}
<p><li>需定时访问以下URL频率1分钟1次</li></p>
<p><code>{$siteurl}/cron?key={:config_get('cron_key')}</code></p>
{else}
<p><li>将以下Shell命令添加到计划任务频率1分钟1次</li></p>
<p><code>cd {:app()->getRootPath()} && php think certtask</code></p>
{if $is_user_www}<p><li><b>计划任务执行用户必须选择www用户</b></li></p>{/if}
<p><li>采用Docker镜像部署的会自动添加计划任务无需手动添加。</li></p>
{/if}
</div>
</div>
<div class="panel panel-intro">
<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">
<div class="form-group">
<label class="col-sm-3 control-label">计划任务执行方式</label>
<div class="col-sm-9"><select class="form-control" name="cron_type" default="{:config_get('cron_type', '0')}"><option value="0">Shell命令推荐</option><option value="1">访问URL</option></select></div>
</div>
<div class="form-group" id="cron_url" {:config_get('cron_type', '0') == 0 ? 'style="display: none"' : ''}>
<label class="col-sm-3 control-label">访问密钥</label>
<div class="col-sm-9"><input type="text" name="cron_key" value="{:config_get('cron_key')}" class="form-control" requ/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<p>优先推荐使用Shell命令方式执行计划任务访问URL方式可能会请求超时导致执行失败。</p><p>如果是虚拟主机环境无法执行命令则可以使用访问URL方式。</p>
</div>
</div>
<div class="panel panel-success mt-3">
<div class="panel-heading"><h3 class="panel-title">计划任务运行状态</h3></div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>任务名称</th>
<th>上次运行时间</th>
</tr>
</thead>
<tbody>
<tr>
<td>SSL证书续签</td>
<td><font color="green">{:config_get('certtask_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>SSL证书部署</td>
<td><font color="green">{:config_get('certdeploy_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>域名到期提醒</td>
<td><font color="green">{:config_get('domain_expire_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>CF优选IP更新</td>
<td><font color="green">{:config_get('optimize_ip_time', '未运行', true)}</font></td>
</tr>
<tr>
<td>定时切换解析</td>
<td><font color="green">{:config_get('schedule_time', '未运行', true)}</font></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
function saveSetting(obj){
var cron_type = $("select[name='cron_type']").val();
var cron_key = $("input[name='cron_key']").val();
if(cron_type == 1 && cron_key == ''){
layer.alert('访问密钥不能为空!', {icon: 2});
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/system/set',
data : {cron_type:cron_type, cron_key:cron_key},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
$("select[name='cron_type']").change(function(){
if($(this).val() == 0){
$("#cron_url").hide();
}else{
$("#cron_url").show();
}
});
</script>
{/block}

View File

@@ -58,13 +58,14 @@
"symfony/polyfill-mbstring": "^1.32",
"symfony/polyfill-php81": "^1.32",
"symfony/polyfill-php82": "^1.32",
"symfony/yaml": "^7.3",
"topthink/framework": "^8.1.0",
"topthink/think-orm": "^4.0",
"topthink/think-view": "^2.0"
},
"require-dev": {
"symfony/var-dumper": "^7.3",
"topthink/think-trace":"^1.0",
"topthink/think-trace":"^2.0",
"swoole/ide-helper": "^6.0"
},
"autoload": {

333
composer.lock generated
View File

@@ -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": "39f34360e80abbce3e603a056ae6211a",
"content-hash": "f7c4abfaf4cb80cd99107e9e1763e75c",
"packages": [
{
"name": "cccyun/php-whois",
"version": "1.1",
"version": "1.2",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/php-whois.git",
"reference": "b5fe65c796c45973a8dcb14dc83ce8eeea2f906e"
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/b5fe65c796c45973a8dcb14dc83ce8eeea2f906e",
"reference": "b5fe65c796c45973a8dcb14dc83ce8eeea2f906e",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"shasum": ""
},
"require": {
@@ -62,9 +62,9 @@
"црщшы"
],
"support": {
"source": "https://github.com/netcccyun/php-whois/tree/1.1"
"source": "https://github.com/netcccyun/php-whois/tree/1.2"
},
"time": "2025-05-01T02:09:16+00:00"
"time": "2025-06-25T06:54:23+00:00"
},
{
"name": "cccyun/think-captcha",
@@ -120,22 +120,22 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.9.3",
"version": "7.10.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
"reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"guzzlehttp/promises": "^2.3",
"guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -226,7 +226,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.9.3"
"source": "https://github.com/guzzle/guzzle/tree/7.10.0"
},
"funding": [
{
@@ -242,20 +242,20 @@
"type": "tidelift"
}
],
"time": "2025-03-27T13:37:11+00:00"
"time": "2025-08-23T22:36:01+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.2.0",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
"reference": "481557b130ef3790cf82b713667b43030dc9c957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
"reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
"url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
"reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": ""
},
"require": {
@@ -263,7 +263,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"type": "library",
"extra": {
@@ -309,7 +309,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.2.0"
"source": "https://github.com/guzzle/promises/tree/2.3.0"
},
"funding": [
{
@@ -325,20 +325,20 @@
"type": "tidelift"
}
],
"time": "2025-03-27T13:27:01+00:00"
"time": "2025-08-22T14:34:08+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.7.1",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
"reference": "21dc724a0583619cd1652f673303492272778051"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
"reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
"reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": ""
},
"require": {
@@ -354,7 +354,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -425,7 +425,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.7.1"
"source": "https://github.com/guzzle/psr7/tree/2.8.0"
},
"funding": [
{
@@ -441,20 +441,20 @@
"type": "tidelift"
}
],
"time": "2025-03-27T12:30:47+00:00"
"time": "2025-08-23T21:21:41+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.10.0",
"version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
"shasum": ""
},
"require": {
@@ -475,6 +475,7 @@
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"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",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -514,7 +515,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.10.0"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
},
"funding": [
{
@@ -522,7 +523,7 @@
"type": "github"
}
],
"time": "2025-04-24T15:19:31+00:00"
"time": "2025-09-30T11:54:53+00:00"
},
{
"name": "psr/container",
@@ -949,9 +950,92 @@
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -1014,7 +1098,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -1025,6 +1109,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1034,7 +1122,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -1095,7 +1183,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -1106,6 +1194,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1115,7 +1207,7 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -1176,7 +1268,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -1187,6 +1279,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1196,7 +1292,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -1252,7 +1348,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
@@ -1263,6 +1359,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1272,7 +1372,7 @@
},
{
"name": "symfony/polyfill-php82",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php82.git",
@@ -1328,7 +1428,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php82/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
},
"funding": [
{
@@ -1339,6 +1439,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1347,17 +1451,93 @@
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "topthink/framework",
"version": "v8.1.2",
"name": "symfony/yaml",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/top-think/framework.git",
"reference": "8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c"
"url": "https://github.com/symfony/yaml.git",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/framework/zipball/8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c",
"reference": "8faec5c9b7a7f2a66ca3140a57e81bd6cd37567c",
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0"
},
"bin": [
"Resources/bin/yaml-lint"
],
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.3.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-08-27T11:34:33+00:00"
},
{
"name": "topthink/framework",
"version": "v8.1.3",
"source": {
"type": "git",
"url": "https://github.com/top-think/framework.git",
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/framework/zipball/e4207e98b66f92d26097ed6efd535930cba90e8f",
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f",
"shasum": ""
},
"require": {
@@ -1409,9 +1589,9 @@
],
"support": {
"issues": "https://github.com/top-think/framework/issues",
"source": "https://github.com/top-think/framework/tree/v8.1.2"
"source": "https://github.com/top-think/framework/tree/v8.1.3"
},
"time": "2025-01-14T08:04:03+00:00"
"time": "2025-07-14T03:48:44+00:00"
},
{
"name": "topthink/think-container",
@@ -1507,16 +1687,16 @@
},
{
"name": "topthink/think-orm",
"version": "v4.0.46",
"version": "v4.0.50",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-orm.git",
"reference": "4bb0a5679a97db8de1c0eb02bbbe179cb3afd901"
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/4bb0a5679a97db8de1c0eb02bbbe179cb3afd901",
"reference": "4bb0a5679a97db8de1c0eb02bbbe179cb3afd901",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
"shasum": ""
},
"require": {
@@ -1524,7 +1704,7 @@
"ext-pdo": "*",
"php": ">=8.0.0",
"psr/log": ">=1.0",
"psr/simple-cache": ">=1.0",
"psr/simple-cache": "^3.0",
"topthink/think-helper": "^3.1",
"topthink/think-validate": "^3.0"
},
@@ -1561,9 +1741,9 @@
],
"support": {
"issues": "https://github.com/top-think/think-orm/issues",
"source": "https://github.com/top-think/think-orm/tree/v4.0.46"
"source": "https://github.com/top-think/think-orm/tree/v4.0.50"
},
"time": "2025-06-26T06:05:35+00:00"
"time": "2025-08-26T05:32:22+00:00"
},
{
"name": "topthink/think-template",
@@ -1727,16 +1907,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.1",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42"
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"shasum": ""
},
"require": {
@@ -1748,7 +1928,6 @@
"symfony/console": "<6.4"
},
"require-dev": {
"ext-iconv": "*",
"symfony/console": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
@@ -1791,7 +1970,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.1"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
},
"funding": [
{
@@ -1802,30 +1981,34 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-27T19:55:54+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "topthink/think-trace",
"version": "v1.6",
"version": "v2.0",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-trace.git",
"reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142"
"reference": "4ba6da2945b37931d61900a6e55dc02b05e5a63f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-trace/zipball/136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
"reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
"url": "https://api.github.com/repos/top-think/think-trace/zipball/4ba6da2945b37931d61900a6e55dc02b05e5a63f",
"reference": "4ba6da2945b37931d61900a6e55dc02b05e5a63f",
"shasum": ""
},
"require": {
"php": ">=7.1.0",
"topthink/framework": "^6.0|^8.0"
"php": ">=8.0",
"topthink/framework": "^8.1"
},
"type": "library",
"extra": {
@@ -1856,9 +2039,9 @@
"description": "thinkphp debug trace",
"support": {
"issues": "https://github.com/top-think/think-trace/issues",
"source": "https://github.com/top-think/think-trace/tree/v1.6"
"source": "https://github.com/top-think/think-trace/tree/v2.0"
},
"time": "2023-02-07T08:36:32+00:00"
"time": "2025-06-12T09:18:19+00:00"
}
],
"aliases": [],

View File

@@ -31,7 +31,7 @@ return [
'show_error_msg' => true,
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
'version' => '1038',
'version' => '1041',
'dbversion' => '1033'
'dbversion' => '1040'
];

View File

@@ -6,7 +6,6 @@ return [
// 指令定义
'commands' => [
'dmtask' => 'app\command\Dmtask',
'opiptask' => 'app\command\Opiptask',
'certtask' => 'app\command\Certtask',
'reset' => 'app\command\Reset',
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/static/images/xp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -29,6 +29,7 @@ Route::get('/logout', 'auth/logout');
Route::any('/quicklogin', 'auth/quicklogin');
Route::any('/dmtask/status', 'dmonitor/status');
Route::any('/optimizeip/status', 'optimizeip/status');
Route::get('/cron', 'system/cron');
Route::group(function () {
Route::any('/', 'index/index');
@@ -78,7 +79,8 @@ Route::group(function () {
Route::post('/dmonitor/task/data', 'dmonitor/task_data');
Route::post('/dmonitor/task/log/data/:id', 'dmonitor/tasklog_data');
Route::get('/dmonitor/task/info/:id', 'dmonitor/taskinfo');
Route::any('/dmonitor/task/:action', 'dmonitor/taskform');
Route::post('/dmonitor/task/:action', 'dmonitor/task_op');
Route::get('/dmonitor/task/:action', 'dmonitor/taskform');
Route::get('/dmonitor/task', 'dmonitor/task');
Route::post('/dmonitor/clean', 'dmonitor/clean');
@@ -112,6 +114,11 @@ Route::group(function () {
Route::get('/cert/certset', 'cert/certset');
Route::post('/schedule/stask/data', 'schedule/stask_data');
Route::post('/schedule/stask/:action', 'schedule/stask_op');
Route::get('/schedule/stask/:action', 'schedule/staskform');
Route::get('/schedule/stask', 'schedule/stask');
Route::get('/system/loginset', 'system/loginset');
Route::get('/system/noticeset', 'system/noticeset');
Route::get('/system/proxyset', 'system/proxyset');
@@ -120,6 +127,7 @@ Route::group(function () {
Route::get('/system/tgbottest', 'system/tgbottest');
Route::get('/system/webhooktest', 'system/webhooktest');
Route::post('/system/proxytest', 'system/proxytest');
Route::get('/system/cronset', 'system/cronset');
})->middleware(CheckLogin::class)
->middleware(ViewOutput::class);

4
think
View File

@@ -2,6 +2,10 @@
<?php
namespace think;
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
die('require PHP >= 8.0 !');
}
// 命令行入口文件
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';